From db191234ad37ccedc04ded4ffdf4a130154e9452 Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Mon, 29 Jun 2026 13:48:23 +0900 Subject: [PATCH 01/83] =?UTF-8?q?chore:=20=EC=B4=88=EA=B8=B0=20=EC=84=B8?= =?UTF-8?q?=ED=8C=85=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + src/main/resources/application.properties | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 8d52aebc6..1d884b864 100644 --- a/build.gradle +++ b/build.gradle @@ -16,6 +16,7 @@ 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-validation' implementation 'dev.akkinoc.spring.boot:logback-access-spring-boot-starter:4.0.0' diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a0f33bbab..785b026b6 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -8,4 +8,5 @@ spring.datasource.url=jdbc:h2:mem:database #spring.jpa.ddl-auto=create-drop #spring.jpa.defer-datasource-initialization=true -#roomescape.auth.jwt.secret= Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E= \ No newline at end of file +roomescape.auth.jwt.secret-key=Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E= +roomescape.auth.jwt.expiration=900000 \ No newline at end of file From b6ba2cb39ec3edc9ce1de72dc10f3f271dbf95ec Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Mon, 29 Jun 2026 13:56:45 +0900 Subject: [PATCH 02/83] =?UTF-8?q?refactor:=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EB=8F=84=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/roomescape/ExceptionController.java | 14 ----------- .../exception/ApplicationException.java | 15 +++++++++++ .../java/roomescape/exception/ErrorCode.java | 9 +++++++ .../roomescape/exception/ErrorResponse.java | 4 +++ .../exception/GlobalExceptionHandler.java | 25 +++++++++++++++++++ 5 files changed, 53 insertions(+), 14 deletions(-) delete mode 100644 src/main/java/roomescape/ExceptionController.java create mode 100644 src/main/java/roomescape/exception/ApplicationException.java create mode 100644 src/main/java/roomescape/exception/ErrorCode.java create mode 100644 src/main/java/roomescape/exception/ErrorResponse.java create mode 100644 src/main/java/roomescape/exception/GlobalExceptionHandler.java diff --git a/src/main/java/roomescape/ExceptionController.java b/src/main/java/roomescape/ExceptionController.java deleted file mode 100644 index 4e2450f9e..000000000 --- a/src/main/java/roomescape/ExceptionController.java +++ /dev/null @@ -1,14 +0,0 @@ -package roomescape; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; - -@ControllerAdvice -public class ExceptionController { - @ExceptionHandler(Exception.class) - public ResponseEntity handleRuntimeException(Exception e) { - e.printStackTrace(); - return ResponseEntity.badRequest().build(); - } -} diff --git a/src/main/java/roomescape/exception/ApplicationException.java b/src/main/java/roomescape/exception/ApplicationException.java new file mode 100644 index 000000000..5177c2a3e --- /dev/null +++ b/src/main/java/roomescape/exception/ApplicationException.java @@ -0,0 +1,15 @@ +package roomescape.exception; + +public class ApplicationException extends RuntimeException { + + private final ErrorCode errorCode; + + public ApplicationException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } + + public ErrorCode getErrorCode() { + return errorCode; + } +} diff --git a/src/main/java/roomescape/exception/ErrorCode.java b/src/main/java/roomescape/exception/ErrorCode.java new file mode 100644 index 000000000..f4fd83d82 --- /dev/null +++ b/src/main/java/roomescape/exception/ErrorCode.java @@ -0,0 +1,9 @@ +package roomescape.exception; + +import org.springframework.http.HttpStatus; + +public interface ErrorCode { + + HttpStatus getHttpStatus(); + String getMessage(); +} diff --git a/src/main/java/roomescape/exception/ErrorResponse.java b/src/main/java/roomescape/exception/ErrorResponse.java new file mode 100644 index 000000000..ebe97cc4a --- /dev/null +++ b/src/main/java/roomescape/exception/ErrorResponse.java @@ -0,0 +1,4 @@ +package roomescape.exception; + +public record ErrorResponse(String message) { +} diff --git a/src/main/java/roomescape/exception/GlobalExceptionHandler.java b/src/main/java/roomescape/exception/GlobalExceptionHandler.java new file mode 100644 index 000000000..94a5a90de --- /dev/null +++ b/src/main/java/roomescape/exception/GlobalExceptionHandler.java @@ -0,0 +1,25 @@ +package roomescape.exception; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(ApplicationException.class) + public ResponseEntity handleRoomEscapeException(ApplicationException e) { + e.printStackTrace(); + + ErrorCode errorCode = e.getErrorCode(); + return ResponseEntity + .status(errorCode.getHttpStatus()) + .body(new ErrorResponse(errorCode.getMessage())); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleRuntimeException(Exception e) { + e.printStackTrace(); + return ResponseEntity.badRequest().build(); + } +} From 5e7b68797965407022518e549ee5f3897d3323a4 Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Mon, 29 Jun 2026 13:57:46 +0900 Subject: [PATCH 03/83] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9D=B8=EC=A6=9D=20=EC=A0=95=EB=B3=B4=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20(1=20&=202=EB=8B=A8=EA=B3=84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roomescape/RoomescapeApplication.java | 2 + .../java/roomescape/auth/AuthController.java | 35 ++++++++++++ .../java/roomescape/auth/AuthErrorCode.java | 28 ++++++++++ .../java/roomescape/auth/AuthService.java | 30 ++++++++++ .../roomescape/auth/JwtTokenProperty.java | 10 ++++ .../roomescape/auth/JwtTokenProvider.java | 56 +++++++++++++++++++ .../roomescape/auth/LoginCheckResponse.java | 6 ++ .../java/roomescape/auth/LoginMember.java | 9 +++ .../auth/LoginMemberArgumentResolver.java | 44 +++++++++++++++ .../java/roomescape/auth/LoginRequest.java | 12 ++++ .../java/roomescape/config/WebConfig.java | 23 ++++++++ .../java/roomescape/member/MemberDao.java | 38 ++++++++----- src/main/java/roomescape/util/CookieUtil.java | 32 +++++++++++ 13 files changed, 311 insertions(+), 14 deletions(-) create mode 100644 src/main/java/roomescape/auth/AuthController.java create mode 100644 src/main/java/roomescape/auth/AuthErrorCode.java create mode 100644 src/main/java/roomescape/auth/AuthService.java create mode 100644 src/main/java/roomescape/auth/JwtTokenProperty.java create mode 100644 src/main/java/roomescape/auth/JwtTokenProvider.java create mode 100644 src/main/java/roomescape/auth/LoginCheckResponse.java create mode 100644 src/main/java/roomescape/auth/LoginMember.java create mode 100644 src/main/java/roomescape/auth/LoginMemberArgumentResolver.java create mode 100644 src/main/java/roomescape/auth/LoginRequest.java create mode 100644 src/main/java/roomescape/config/WebConfig.java create mode 100644 src/main/java/roomescape/util/CookieUtil.java diff --git a/src/main/java/roomescape/RoomescapeApplication.java b/src/main/java/roomescape/RoomescapeApplication.java index 2ca0f743f..f23dea395 100644 --- a/src/main/java/roomescape/RoomescapeApplication.java +++ b/src/main/java/roomescape/RoomescapeApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; @SpringBootApplication +@ConfigurationPropertiesScan public class RoomescapeApplication { public static void main(String[] args) { SpringApplication.run(RoomescapeApplication.class, args); diff --git a/src/main/java/roomescape/auth/AuthController.java b/src/main/java/roomescape/auth/AuthController.java new file mode 100644 index 000000000..679bb0900 --- /dev/null +++ b/src/main/java/roomescape/auth/AuthController.java @@ -0,0 +1,35 @@ +package roomescape.auth; + +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import roomescape.util.CookieUtil; + +@RestController +public class AuthController { + + private final AuthService authService; + private final CookieUtil cookieUtil; + + public AuthController(AuthService authService, CookieUtil cookieUtil) { + this.authService = authService; + this.cookieUtil = cookieUtil; + } + + @PostMapping("/login") + public ResponseEntity login(@Valid @RequestBody LoginRequest loginRequest, + HttpServletResponse response) { + String token = authService.login(loginRequest); + cookieUtil.setCookie(response, token); + return ResponseEntity.ok().build(); + } + + @GetMapping("/login/check") + public ResponseEntity checkLogin(LoginMember loginMember) { + return ResponseEntity.ok(new LoginCheckResponse(loginMember.name())); + } +} diff --git a/src/main/java/roomescape/auth/AuthErrorCode.java b/src/main/java/roomescape/auth/AuthErrorCode.java new file mode 100644 index 000000000..78d3c64a5 --- /dev/null +++ b/src/main/java/roomescape/auth/AuthErrorCode.java @@ -0,0 +1,28 @@ +package roomescape.auth; + +import org.springframework.http.HttpStatus; +import roomescape.exception.ErrorCode; + +public enum AuthErrorCode implements ErrorCode { + ACCESS_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "토큰이 만료되었습니다."), + INVALID_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰입니다."), + UNAUTHENTICATED_ACCESS(HttpStatus.UNAUTHORIZED, "인증되지 않은 요청입니다."); + + private HttpStatus httpStatus; + private String message; + + AuthErrorCode(HttpStatus httpStatus, String message) { + this.httpStatus = httpStatus; + this.message = message; + } + + @Override + public HttpStatus getHttpStatus() { + return httpStatus; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/src/main/java/roomescape/auth/AuthService.java b/src/main/java/roomescape/auth/AuthService.java new file mode 100644 index 000000000..f1cde05ca --- /dev/null +++ b/src/main/java/roomescape/auth/AuthService.java @@ -0,0 +1,30 @@ +package roomescape.auth; + +import org.springframework.stereotype.Service; +import roomescape.exception.ApplicationException; +import roomescape.member.Member; +import roomescape.member.MemberDao; + +@Service +public class AuthService { + + private final MemberDao memberDao; + private final JwtTokenProvider tokenProvider; + + public AuthService(MemberDao memberDao, JwtTokenProvider tokenProvider) { + this.memberDao = memberDao; + this.tokenProvider = tokenProvider; + } + + public String login(LoginRequest loginRequest) { + Member member = memberDao.findByEmailAndPassword(loginRequest.email(), loginRequest.password()); + return tokenProvider.createAccessToken(member); + } + + public LoginMember findAuthenticatedMember(String token) { + Long memberId = tokenProvider.getLoginMemberId(token); + Member member = memberDao.findById(memberId) + .orElseThrow(() -> new ApplicationException(AuthErrorCode.UNAUTHENTICATED_ACCESS)); + return new LoginMember(member.getId(), member.getName(), member.getEmail(), member.getRole()); + } +} diff --git a/src/main/java/roomescape/auth/JwtTokenProperty.java b/src/main/java/roomescape/auth/JwtTokenProperty.java new file mode 100644 index 000000000..172d31142 --- /dev/null +++ b/src/main/java/roomescape/auth/JwtTokenProperty.java @@ -0,0 +1,10 @@ +package roomescape.auth; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties("roomescape.auth.jwt") +public record JwtTokenProperty( + String secretKey, + long expiration +) { +} diff --git a/src/main/java/roomescape/auth/JwtTokenProvider.java b/src/main/java/roomescape/auth/JwtTokenProvider.java new file mode 100644 index 000000000..705958f88 --- /dev/null +++ b/src/main/java/roomescape/auth/JwtTokenProvider.java @@ -0,0 +1,56 @@ +package roomescape.auth; + +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.SignatureException; +import org.springframework.stereotype.Component; +import roomescape.exception.ApplicationException; +import roomescape.member.Member; + +import java.security.Key; +import java.util.Base64; +import java.util.Date; + +@Component +public class JwtTokenProvider { + + private final Key secretKey; + private final long expiration; + + public JwtTokenProvider(JwtTokenProperty tokenProperty) { + this.secretKey = Keys.hmacShaKeyFor( + Base64.getDecoder().decode(tokenProperty.secretKey())); + this.expiration = tokenProperty.expiration(); + } + + public String createAccessToken(Member member) { + Date now = new Date(); + Date expiredTime = new Date(now.getTime() + expiration); + + return Jwts.builder() + .setSubject(member.getId().toString()) + .claim("name", member.getName()) + .claim("role", member.getRole()) + .setIssuedAt(now) + .setExpiration(expiredTime) + .signWith(secretKey) + .compact(); + } + + public Long getLoginMemberId(String accessToken) { + try { + return Long.valueOf(Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(accessToken) + .getBody().getSubject()); + } catch (SignatureException e) { + throw new ApplicationException(AuthErrorCode.INVALID_ACCESS_TOKEN); + } catch (ExpiredJwtException e) { + throw new ApplicationException(AuthErrorCode.ACCESS_TOKEN_EXPIRED); + } catch (IllegalArgumentException e) { + throw new ApplicationException(AuthErrorCode.UNAUTHENTICATED_ACCESS); + } + } +} diff --git a/src/main/java/roomescape/auth/LoginCheckResponse.java b/src/main/java/roomescape/auth/LoginCheckResponse.java new file mode 100644 index 000000000..32e011cb4 --- /dev/null +++ b/src/main/java/roomescape/auth/LoginCheckResponse.java @@ -0,0 +1,6 @@ +package roomescape.auth; + +public record LoginCheckResponse( + String name +) { +} diff --git a/src/main/java/roomescape/auth/LoginMember.java b/src/main/java/roomescape/auth/LoginMember.java new file mode 100644 index 000000000..57634f82c --- /dev/null +++ b/src/main/java/roomescape/auth/LoginMember.java @@ -0,0 +1,9 @@ +package roomescape.auth; + +public record LoginMember( + Long id, + String name, + String email, + String role +) { +} diff --git a/src/main/java/roomescape/auth/LoginMemberArgumentResolver.java b/src/main/java/roomescape/auth/LoginMemberArgumentResolver.java new file mode 100644 index 000000000..f2326dc75 --- /dev/null +++ b/src/main/java/roomescape/auth/LoginMemberArgumentResolver.java @@ -0,0 +1,44 @@ +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.exception.ApplicationException; +import roomescape.member.MemberDao; +import roomescape.util.CookieUtil; + +@Component +public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver { + + private final AuthService authService; + private final CookieUtil cookieUtil; + + public LoginMemberArgumentResolver(AuthService authService, CookieUtil cookieUtil) { + this.authService = authService; + this.cookieUtil = cookieUtil; + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + 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); + + Cookie[] cookies = request.getCookies(); + if (cookies == null) { + throw new ApplicationException(AuthErrorCode.UNAUTHENTICATED_ACCESS); + } + + String token = cookieUtil.extractToken(cookies); + return authService.findAuthenticatedMember(token); + } +} diff --git a/src/main/java/roomescape/auth/LoginRequest.java b/src/main/java/roomescape/auth/LoginRequest.java new file mode 100644 index 000000000..d3602fbd3 --- /dev/null +++ b/src/main/java/roomescape/auth/LoginRequest.java @@ -0,0 +1,12 @@ +package roomescape.auth; + +import jakarta.validation.constraints.NotBlank; + +public record LoginRequest( + @NotBlank(message = "로그인 email은 비어있을 수 없습니다.") + String email, + + @NotBlank(message = "로그인 password는 비어있을 수 없습니다.") + String password +) { +} diff --git a/src/main/java/roomescape/config/WebConfig.java b/src/main/java/roomescape/config/WebConfig.java new file mode 100644 index 000000000..21ffccd81 --- /dev/null +++ b/src/main/java/roomescape/config/WebConfig.java @@ -0,0 +1,23 @@ +package roomescape.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import roomescape.auth.LoginMemberArgumentResolver; + +import java.util.List; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + private final LoginMemberArgumentResolver loginMemberArgumentResolver; + + public WebConfig(LoginMemberArgumentResolver loginMemberArgumentResolver) { + this.loginMemberArgumentResolver = loginMemberArgumentResolver; + } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(loginMemberArgumentResolver); + } +} diff --git a/src/main/java/roomescape/member/MemberDao.java b/src/main/java/roomescape/member/MemberDao.java index 81f77f4cd..c748984c4 100644 --- a/src/main/java/roomescape/member/MemberDao.java +++ b/src/main/java/roomescape/member/MemberDao.java @@ -1,10 +1,13 @@ package roomescape.member; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.support.GeneratedKeyHolder; import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Repository; +import java.util.Optional; + @Repository public class MemberDao { private JdbcTemplate jdbcTemplate; @@ -27,29 +30,36 @@ public Member save(Member member) { return new Member(keyHolder.getKey().longValue(), member.getName(), member.getEmail(), "USER"); } + public Optional findById(Long id) { + return jdbcTemplate.query( + "SELECT * FROM member WHERE id = ?", + rowMapper(), + id + ).stream().findFirst(); + } + public Member findByEmailAndPassword(String email, String password) { return jdbcTemplate.queryForObject( "SELECT id, name, email, role FROM member WHERE email = ? AND password = ?", - (rs, rowNum) -> new Member( - rs.getLong("id"), - rs.getString("name"), - rs.getString("email"), - rs.getString("role") - ), + rowMapper(), email, password ); } - public Member findByName(String name) { - return jdbcTemplate.queryForObject( + public Optional findByName(String name) { + return jdbcTemplate.query( "SELECT id, name, email, role FROM member WHERE name = ?", - (rs, rowNum) -> new Member( - rs.getLong("id"), - rs.getString("name"), - rs.getString("email"), - rs.getString("role") - ), + rowMapper(), name + ).stream().findFirst(); + } + + private RowMapper rowMapper() { + return (resultSet, rowNum) -> new Member( + resultSet.getLong("id"), + resultSet.getString("name"), + resultSet.getString("email"), + resultSet.getString("role") ); } } diff --git a/src/main/java/roomescape/util/CookieUtil.java b/src/main/java/roomescape/util/CookieUtil.java new file mode 100644 index 000000000..20a36a907 --- /dev/null +++ b/src/main/java/roomescape/util/CookieUtil.java @@ -0,0 +1,32 @@ +package roomescape.util; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import roomescape.auth.AuthErrorCode; +import roomescape.exception.ApplicationException; + +@Component +public class CookieUtil { + + public void setCookie(HttpServletResponse response, String accessToken) { + Cookie cookie = new Cookie("token", accessToken); + cookie.setHttpOnly(true); + cookie.setPath("/"); + response.addCookie(cookie); + } + + public String extractToken(Cookie[] cookies) { + if (cookies == null) { + throw new ApplicationException(AuthErrorCode.UNAUTHENTICATED_ACCESS); + } + + for (Cookie cookie : cookies) { + if (cookie.getName().equals("token")) { + return cookie.getValue(); + } + } + + throw new ApplicationException(AuthErrorCode.UNAUTHENTICATED_ACCESS); + } +} From ec6ee6d152f51b7fac7a63e5da14bd94c2a74f79 Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Mon, 29 Jun 2026 13:58:10 +0900 Subject: [PATCH 04/83] =?UTF-8?q?feat:=20=EC=9D=B8=EC=A6=9D=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EA=B8=B0=EB=B0=98=20=EC=98=88=EC=95=BD=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20(1=20&=202=EB=8B=A8=EA=B3=84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/ReservationController.java | 8 ++++--- .../reservation/ReservationDao.java | 6 ++--- .../reservation/ReservationService.java | 24 +++++++++++++++---- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/main/java/roomescape/reservation/ReservationController.java b/src/main/java/roomescape/reservation/ReservationController.java index b3bef3990..39ff338d5 100644 --- a/src/main/java/roomescape/reservation/ReservationController.java +++ b/src/main/java/roomescape/reservation/ReservationController.java @@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +import roomescape.auth.LoginMember; import java.net.URI; import java.util.List; @@ -26,14 +27,15 @@ public List list() { } @PostMapping("/reservations") - public ResponseEntity create(@RequestBody ReservationRequest reservationRequest) { - if (reservationRequest.getName() == null + public ResponseEntity create(@RequestBody ReservationRequest reservationRequest, LoginMember loginMember) { + if ((reservationRequest.getName() == null && loginMember == null) || reservationRequest.getDate() == null || reservationRequest.getTheme() == null || reservationRequest.getTime() == null) { return ResponseEntity.badRequest().build(); } - ReservationResponse reservation = reservationService.save(reservationRequest); + + ReservationResponse reservation = reservationService.save(reservationRequest, loginMember); return ResponseEntity.created(URI.create("/reservations/" + reservation.getId())).body(reservation); } diff --git a/src/main/java/roomescape/reservation/ReservationDao.java b/src/main/java/roomescape/reservation/ReservationDao.java index a4972430c..b029b348c 100644 --- a/src/main/java/roomescape/reservation/ReservationDao.java +++ b/src/main/java/roomescape/reservation/ReservationDao.java @@ -43,12 +43,12 @@ public List findAll() { ))); } - public Reservation save(ReservationRequest reservationRequest) { + public Reservation save(ReservationRequest reservationRequest, String name) { KeyHolder keyHolder = new GeneratedKeyHolder(); jdbcTemplate.update(connection -> { PreparedStatement ps = connection.prepareStatement("INSERT INTO reservation(date, name, theme_id, time_id) VALUES (?, ?, ?, ?)", new String[]{"id"}); ps.setString(1, reservationRequest.getDate()); - ps.setString(2, reservationRequest.getName()); + ps.setString(2, name); ps.setLong(3, reservationRequest.getTheme()); ps.setLong(4, reservationRequest.getTime()); return ps; @@ -64,7 +64,7 @@ public Reservation save(ReservationRequest reservationRequest) { return new Reservation( keyHolder.getKey().longValue(), - reservationRequest.getName(), + name, reservationRequest.getDate(), time, theme diff --git a/src/main/java/roomescape/reservation/ReservationService.java b/src/main/java/roomescape/reservation/ReservationService.java index bd3313328..857c6cb75 100644 --- a/src/main/java/roomescape/reservation/ReservationService.java +++ b/src/main/java/roomescape/reservation/ReservationService.java @@ -1,21 +1,28 @@ package roomescape.reservation; import org.springframework.stereotype.Service; +import roomescape.auth.LoginMember; +import roomescape.member.Member; +import roomescape.member.MemberDao; import java.util.List; @Service public class ReservationService { private ReservationDao reservationDao; + private MemberDao memberDao; - public ReservationService(ReservationDao reservationDao) { + public ReservationService(ReservationDao reservationDao, MemberDao memberDao) { this.reservationDao = reservationDao; + this.memberDao = memberDao; } - public ReservationResponse save(ReservationRequest reservationRequest) { - Reservation reservation = reservationDao.save(reservationRequest); + public ReservationResponse save(ReservationRequest reservationRequest, LoginMember loginMember) { + String name = resolveName(reservationRequest, loginMember); - return new ReservationResponse(reservation.getId(), reservationRequest.getName(), reservation.getTheme().getName(), reservation.getDate(), reservation.getTime().getValue()); + Reservation reservation = reservationDao.save(reservationRequest, name); + + return new ReservationResponse(reservation.getId(), name , reservation.getTheme().getName(), reservation.getDate(), reservation.getTime().getValue()); } public void deleteById(Long id) { @@ -27,4 +34,13 @@ public List findAll() { .map(it -> new ReservationResponse(it.getId(), it.getName(), it.getTheme().getName(), it.getDate(), it.getTime().getValue())) .toList(); } + + private String resolveName(ReservationRequest reservationRequest, LoginMember loginMember) { + if (reservationRequest.getName() != null) { + memberDao.findByName(reservationRequest.getName()).orElseThrow(); + return reservationRequest.getName(); + } + + return loginMember.name(); + } } From b17debb962914732fc16d39d23177b161aef3eff Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Mon, 29 Jun 2026 13:58:23 +0900 Subject: [PATCH 05/83] =?UTF-8?q?test:=201,=202=EB=8B=A8=EA=B3=84=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/roomescape/MissionStepTest.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/test/java/roomescape/MissionStepTest.java b/src/test/java/roomescape/MissionStepTest.java index 6add784bd..66581d827 100644 --- a/src/test/java/roomescape/MissionStepTest.java +++ b/src/test/java/roomescape/MissionStepTest.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.DirtiesContext; +import roomescape.reservation.ReservationResponse; import java.util.HashMap; import java.util.Map; @@ -35,4 +36,52 @@ public class MissionStepTest { assertThat(token).isNotBlank(); } + + @Test + void 이단계() { + String token = createToken("admin@email.com", "password"); // 일단계에서 토큰을 추출하는 로직을 메서드로 따로 만들어서 활용하세요. + + Map params = new HashMap<>(); + params.put("date", "2024-03-01"); + params.put("time", "1"); + params.put("theme", "1"); + + ExtractableResponse response = RestAssured.given().log().all() + .body(params) + .cookie("token", token) + .contentType(ContentType.JSON) + .post("/reservations") + .then().log().all() + .extract(); + + assertThat(response.statusCode()).isEqualTo(201); + assertThat(response.as(ReservationResponse.class).getName()).isEqualTo("어드민"); + + params.put("name", "브라운"); + + ExtractableResponse adminResponse = RestAssured.given().log().all() + .body(params) + .cookie("token", token) + .contentType(ContentType.JSON) + .post("/reservations") + .then().log().all() + .extract(); + + assertThat(adminResponse.statusCode()).isEqualTo(201); + assertThat(adminResponse.as(ReservationResponse.class).getName()).isEqualTo("브라운"); + } + + private String createToken(String email, String password) { + Map params = new HashMap<>(); + params.put("email", email); + params.put("password", password); + + ExtractableResponse response = RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(params) + .when().post("/login") + .then().extract(); + + return response.headers().get("Set-Cookie").getValue().split(";")[0].split("=")[1]; + } } \ No newline at end of file From 6e1c47513e4e2a39bd15c51943c9ec3eefed23b7 Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Mon, 29 Jun 2026 14:04:04 +0900 Subject: [PATCH 06/83] =?UTF-8?q?chore:=20=EB=AF=B8=EC=82=AC=EC=9A=A9=20im?= =?UTF-8?q?port=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=ED=8F=AC=EB=A7=B7=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/roomescape/auth/LoginMember.java | 8 ++++---- .../java/roomescape/auth/LoginMemberArgumentResolver.java | 1 - src/main/java/roomescape/exception/ErrorCode.java | 1 + src/main/java/roomescape/member/MemberController.java | 2 -- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main/java/roomescape/auth/LoginMember.java b/src/main/java/roomescape/auth/LoginMember.java index 57634f82c..c12d9d3df 100644 --- a/src/main/java/roomescape/auth/LoginMember.java +++ b/src/main/java/roomescape/auth/LoginMember.java @@ -1,9 +1,9 @@ package roomescape.auth; public record LoginMember( - Long id, - String name, - String email, - String role + Long id, + String name, + String email, + String role ) { } diff --git a/src/main/java/roomescape/auth/LoginMemberArgumentResolver.java b/src/main/java/roomescape/auth/LoginMemberArgumentResolver.java index f2326dc75..4571177f5 100644 --- a/src/main/java/roomescape/auth/LoginMemberArgumentResolver.java +++ b/src/main/java/roomescape/auth/LoginMemberArgumentResolver.java @@ -9,7 +9,6 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; import roomescape.exception.ApplicationException; -import roomescape.member.MemberDao; import roomescape.util.CookieUtil; @Component diff --git a/src/main/java/roomescape/exception/ErrorCode.java b/src/main/java/roomescape/exception/ErrorCode.java index f4fd83d82..9b076f749 100644 --- a/src/main/java/roomescape/exception/ErrorCode.java +++ b/src/main/java/roomescape/exception/ErrorCode.java @@ -5,5 +5,6 @@ public interface ErrorCode { HttpStatus getHttpStatus(); + String getMessage(); } diff --git a/src/main/java/roomescape/member/MemberController.java b/src/main/java/roomescape/member/MemberController.java index 881ae5e0d..fe009d062 100644 --- a/src/main/java/roomescape/member/MemberController.java +++ b/src/main/java/roomescape/member/MemberController.java @@ -1,10 +1,8 @@ package roomescape.member; import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; From ba98c27a4964f0ec1d974e4103ff8e7dd525fafd Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Mon, 29 Jun 2026 14:11:14 +0900 Subject: [PATCH 07/83] =?UTF-8?q?refactor:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20auth=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=EC=9C=BC=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/roomescape/auth/AuthController.java | 7 +++++++ .../java/roomescape/member/MemberController.java | 12 ------------ src/main/java/roomescape/util/CookieUtil.java | 8 ++++++++ 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/main/java/roomescape/auth/AuthController.java b/src/main/java/roomescape/auth/AuthController.java index 679bb0900..30e2962d4 100644 --- a/src/main/java/roomescape/auth/AuthController.java +++ b/src/main/java/roomescape/auth/AuthController.java @@ -1,5 +1,6 @@ package roomescape.auth; +import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; @@ -32,4 +33,10 @@ public ResponseEntity login(@Valid @RequestBody LoginRequest loginRequest, public ResponseEntity checkLogin(LoginMember loginMember) { return ResponseEntity.ok(new LoginCheckResponse(loginMember.name())); } + + @PostMapping("/logout") + public ResponseEntity logout(HttpServletResponse response) { + cookieUtil.expireCookie(response); + return ResponseEntity.ok().build(); + } } diff --git a/src/main/java/roomescape/member/MemberController.java b/src/main/java/roomescape/member/MemberController.java index fe009d062..43c1dcd63 100644 --- a/src/main/java/roomescape/member/MemberController.java +++ b/src/main/java/roomescape/member/MemberController.java @@ -1,7 +1,5 @@ package roomescape.member; -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -22,14 +20,4 @@ public ResponseEntity createMember(@RequestBody MemberRequest memberRequest) { MemberResponse member = memberService.createMember(memberRequest); return ResponseEntity.created(URI.create("/members/" + member.getId())).body(member); } - - @PostMapping("/logout") - public ResponseEntity logout(HttpServletResponse response) { - Cookie cookie = new Cookie("token", ""); - cookie.setHttpOnly(true); - cookie.setPath("/"); - cookie.setMaxAge(0); - response.addCookie(cookie); - return ResponseEntity.ok().build(); - } } diff --git a/src/main/java/roomescape/util/CookieUtil.java b/src/main/java/roomescape/util/CookieUtil.java index 20a36a907..36a10239d 100644 --- a/src/main/java/roomescape/util/CookieUtil.java +++ b/src/main/java/roomescape/util/CookieUtil.java @@ -16,6 +16,14 @@ public void setCookie(HttpServletResponse response, String accessToken) { response.addCookie(cookie); } + public void expireCookie(HttpServletResponse response) { + Cookie cookie = new Cookie("token", ""); + cookie.setHttpOnly(true); + cookie.setPath("/"); + cookie.setMaxAge(0); + response.addCookie(cookie); + } + public String extractToken(Cookie[] cookies) { if (cookies == null) { throw new ApplicationException(AuthErrorCode.UNAUTHENTICATED_ACCESS); From 2b95ab2841d72812e60b6c0f13f2870c8ee6ec88 Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Mon, 29 Jun 2026 14:12:17 +0900 Subject: [PATCH 08/83] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=B2=98=EB=A6=AC=EB=A5=BC=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?MemberErrorCode=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roomescape/member/MemberErrorCode.java | 26 +++++++++++++++++++ .../reservation/ReservationService.java | 8 +++--- 2 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 src/main/java/roomescape/member/MemberErrorCode.java diff --git a/src/main/java/roomescape/member/MemberErrorCode.java b/src/main/java/roomescape/member/MemberErrorCode.java new file mode 100644 index 000000000..b2ff5223a --- /dev/null +++ b/src/main/java/roomescape/member/MemberErrorCode.java @@ -0,0 +1,26 @@ +package roomescape.member; + +import org.springframework.http.HttpStatus; +import roomescape.exception.ErrorCode; + +public enum MemberErrorCode implements ErrorCode { + MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 회원입니다."); + + private final HttpStatus httpStatus; + private final String message; + + MemberErrorCode(HttpStatus httpStatus, String message) { + this.httpStatus = httpStatus; + this.message = message; + } + + @Override + public HttpStatus getHttpStatus() { + return httpStatus; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/src/main/java/roomescape/reservation/ReservationService.java b/src/main/java/roomescape/reservation/ReservationService.java index 857c6cb75..4ac03fe99 100644 --- a/src/main/java/roomescape/reservation/ReservationService.java +++ b/src/main/java/roomescape/reservation/ReservationService.java @@ -2,8 +2,9 @@ import org.springframework.stereotype.Service; import roomescape.auth.LoginMember; -import roomescape.member.Member; +import roomescape.exception.ApplicationException; import roomescape.member.MemberDao; +import roomescape.member.MemberErrorCode; import java.util.List; @@ -22,7 +23,7 @@ public ReservationResponse save(ReservationRequest reservationRequest, LoginMemb Reservation reservation = reservationDao.save(reservationRequest, name); - return new ReservationResponse(reservation.getId(), name , reservation.getTheme().getName(), reservation.getDate(), reservation.getTime().getValue()); + return new ReservationResponse(reservation.getId(), name, reservation.getTheme().getName(), reservation.getDate(), reservation.getTime().getValue()); } public void deleteById(Long id) { @@ -37,7 +38,8 @@ public List findAll() { private String resolveName(ReservationRequest reservationRequest, LoginMember loginMember) { if (reservationRequest.getName() != null) { - memberDao.findByName(reservationRequest.getName()).orElseThrow(); + memberDao.findByName(reservationRequest.getName()) + .orElseThrow(() -> new ApplicationException(MemberErrorCode.MEMBER_NOT_FOUND)); return reservationRequest.getName(); } From 1c8cfde2fbd2403f19531c54700a0aea89bf5d15 Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Mon, 29 Jun 2026 15:34:11 +0900 Subject: [PATCH 09/83] =?UTF-8?q?fix:=20SQL=20=EB=AC=B8=EB=B2=95=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/roomescape/reservation/ReservationDao.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/roomescape/reservation/ReservationDao.java b/src/main/java/roomescape/reservation/ReservationDao.java index b029b348c..0a9d2b0c7 100644 --- a/src/main/java/roomescape/reservation/ReservationDao.java +++ b/src/main/java/roomescape/reservation/ReservationDao.java @@ -82,7 +82,7 @@ public List findReservationsByDateAndTheme(String date, Long themeI "ti.id AS time_id, ti.time_value AS time_value " + "FROM reservation r " + "JOIN theme t ON r.theme_id = t.id " + - "JOIN time ti ON r.time_id = ti.id" + + "JOIN time ti ON r.time_id = ti.id " + "WHERE r.date = ? AND r.theme_id = ?", new Object[]{date, themeId}, (rs, rowNum) -> new Reservation( From ab7f70c6b6221c29a6b20bcc6a8b4386f50f9553 Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Mon, 29 Jun 2026 16:42:54 +0900 Subject: [PATCH 10/83] =?UTF-8?q?docs:=20README.md=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 000000000..2e441ff08 --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +# 🚪 방탈출 예약 시스템 + +## 프로젝트 개요 + +방탈출 예약 관리를 위한 어드민 웹 애플리케이션입니다. +로그인부터 예약 추가까지 REST API로 구현하며, 권한 없는 요청에 대한 예외 처리를 포함합니다. + +--- + +## 주요 기능 + +### 인증 / 인가 +- 이메일·비밀번호로 로그인하면 JWT를 발급해 `token` 쿠키로 내려줍니다. +- 쿠키의 토큰으로 로그인 사용자 정보를 조회합니다 (`GET /login/check`). +- 로그아웃 시 `token` 쿠키를 만료시킵니다. +- `HandlerMethodArgumentResolver`(`LoginMemberArgumentResolver`)로 컨트롤러에 로그인 사용자(`LoginMember`)를 주입합니다. + +### 예외 처리 +- `ErrorCode` 인터페이스로 예외 상황별 HTTP 상태 코드와 메시지를 관리합니다. (`AuthErrorCode`, `MemberErrorCode` 구현) +- 공통 비즈니스 예외 `ApplicationException`을 던지고, `@ControllerAdvice`인 `GlobalExceptionHandler`가 일괄 처리합니다. +- 현재 처리 예외: 인증되지 않은 요청 / 유효하지 않거나 만료된 토큰(401), 존재하지 않는 회원(404), 기타 예외(400). + +## 프로젝트 구조 +``` +roomescape/ +├── RoomescapeApplication.java # 메인 진입점 +├── PageController.java # 뷰(HTML) 페이지 라우팅 +│ +├── auth/ # 인증/인가 (JWT) +│ ├── AuthController.java # 로그인/로그아웃/로그인체크 API +│ ├── AuthService.java +│ ├── AuthErrorCode.java +│ ├── JwtTokenProvider.java # JWT 토큰 생성/검증 +│ ├── JwtTokenProperty.java # JWT 설정값 (@ConfigurationProperties) +│ ├── LoginMemberArgumentResolver.java # @LoginMember 주입 +│ ├── LoginMember.java # 인증된 사용자 정보 +│ ├── LoginRequest.java +│ └── LoginCheckResponse.java +│ +├── member/ # 회원 +│ ├── Member.java # 도메인/엔티티 +│ ├── MemberController.java +│ ├── MemberService.java +│ ├── MemberDao.java # JdbcTemplate 기반 DAO +│ ├── MemberRequest.java +│ └── MemberResponse.java +│ +├── reservation/ # 예약 (핵심 도메인) +│ ├── Reservation.java +│ ├── ReservationController.java +│ ├── ReservationService.java +│ ├── ReservationDao.java +│ ├── ReservationRequest.java +│ └── ReservationResponse.java +│ +├── theme/ # 테마 +│ ├── Theme.java +│ ├── ThemeController.java +│ └── ThemeDao.java # ※ Service 없음 +│ +├── time/ # 예약 시간 +│ ├── Time.java +│ ├── AvailableTime.java # 예약 가능 시간 표현 +│ ├── TimeController.java +│ ├── TimeService.java +│ └── TimeDao.java +│ +├── config/ +│ └── WebConfig.java # ArgumentResolver 등록 +│ +├── exception/ # 전역 예외 처리 +│ ├── GlobalExceptionHandler.java # @RestControllerAdvice +│ ├── ApplicationException.java +│ ├── ErrorCode.java # 인터페이스 (auth의 AuthErrorCode가 구현) +│ └── ErrorResponse.java +│ +└── util/ +└── CookieUtil.java # 쿠키 추출/생성 헬퍼 + +``` \ No newline at end of file From eae5e9a5477f076d7608cde3de2529983493a43e Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Thu, 2 Jul 2026 16:43:40 +0900 Subject: [PATCH 11/83] =?UTF-8?q?refactor:=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EB=A1=9C=EA=B9=85=20=EB=B0=A9=EC=8B=9D=EC=9D=84=20printStackTr?= =?UTF-8?q?ace=EC=97=90=EC=84=9C=20Logger=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 --- .../exception/GlobalExceptionHandler.java | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/main/java/roomescape/exception/GlobalExceptionHandler.java b/src/main/java/roomescape/exception/GlobalExceptionHandler.java index 94a5a90de..ce0875b2b 100644 --- a/src/main/java/roomescape/exception/GlobalExceptionHandler.java +++ b/src/main/java/roomescape/exception/GlobalExceptionHandler.java @@ -1,25 +1,42 @@ package roomescape.exception; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; +import java.util.stream.Collectors; + @ControllerAdvice public class GlobalExceptionHandler { + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + @ExceptionHandler(ApplicationException.class) public ResponseEntity handleRoomEscapeException(ApplicationException e) { - e.printStackTrace(); - ErrorCode errorCode = e.getErrorCode(); + log.warn("비즈니스 예외 발생: code={}, message={}", errorCode, e.getMessage()); return ResponseEntity .status(errorCode.getHttpStatus()) .body(new ErrorResponse(errorCode.getMessage())); } + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity processValidationException(MethodArgumentNotValidException e) { + String message = e.getBindingResult().getAllErrors().stream() + .map(DefaultMessageSourceResolvable::getDefaultMessage) + .collect(Collectors.joining(", ")); + log.warn("입력값 검증 실패: message={}", message); + + return ResponseEntity.badRequest().body(new ErrorResponse(message)); + } + @ExceptionHandler(Exception.class) public ResponseEntity handleRuntimeException(Exception e) { - e.printStackTrace(); + log.error("예상하지 못한 예외 발생", e); return ResponseEntity.badRequest().build(); } } From bf6ac7b330a225a8c9f2c97197b4a9937f91d376 Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Thu, 2 Jul 2026 16:48:29 +0900 Subject: [PATCH 12/83] =?UTF-8?q?refactor:=20MalformedJwtException=20Unsup?= =?UTF-8?q?portedJwtException=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=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/auth/JwtTokenProvider.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/roomescape/auth/JwtTokenProvider.java b/src/main/java/roomescape/auth/JwtTokenProvider.java index 705958f88..0a3ca34fc 100644 --- a/src/main/java/roomescape/auth/JwtTokenProvider.java +++ b/src/main/java/roomescape/auth/JwtTokenProvider.java @@ -2,6 +2,8 @@ import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.UnsupportedJwtException; import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.SignatureException; import org.springframework.stereotype.Component; @@ -45,7 +47,7 @@ public Long getLoginMemberId(String accessToken) { .build() .parseClaimsJws(accessToken) .getBody().getSubject()); - } catch (SignatureException e) { + } catch (SignatureException | MalformedJwtException | UnsupportedJwtException e) { throw new ApplicationException(AuthErrorCode.INVALID_ACCESS_TOKEN); } catch (ExpiredJwtException e) { throw new ApplicationException(AuthErrorCode.ACCESS_TOKEN_EXPIRED); From fd847c1e75f23b308d3675a3a1b99774de47a6ce Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Thu, 2 Jul 2026 18:26:43 +0900 Subject: [PATCH 13/83] =?UTF-8?q?feat:=20=EC=9D=B8=EC=A6=9D=20=ED=95=84?= =?UTF-8?q?=EC=9A=94=EB=A5=BC=20=EA=B0=80=EB=A6=AC=ED=82=A4=EB=8A=94=20?= =?UTF-8?q?=EC=BB=A4=EC=8A=A4=ED=85=80=20=EC=96=B4=EB=85=B8=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=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/auth/AuthController.java | 2 +- src/main/java/roomescape/auth/Login.java | 11 +++++++++++ .../roomescape/auth/LoginMemberArgumentResolver.java | 5 ++++- .../roomescape/reservation/ReservationController.java | 10 ++-------- 4 files changed, 18 insertions(+), 10 deletions(-) create mode 100644 src/main/java/roomescape/auth/Login.java diff --git a/src/main/java/roomescape/auth/AuthController.java b/src/main/java/roomescape/auth/AuthController.java index 30e2962d4..cbc5b6426 100644 --- a/src/main/java/roomescape/auth/AuthController.java +++ b/src/main/java/roomescape/auth/AuthController.java @@ -30,7 +30,7 @@ public ResponseEntity login(@Valid @RequestBody LoginRequest loginRequest, } @GetMapping("/login/check") - public ResponseEntity checkLogin(LoginMember loginMember) { + public ResponseEntity checkLogin(@Login LoginMember loginMember) { return ResponseEntity.ok(new LoginCheckResponse(loginMember.name())); } diff --git a/src/main/java/roomescape/auth/Login.java b/src/main/java/roomescape/auth/Login.java new file mode 100644 index 000000000..e691f5ee9 --- /dev/null +++ b/src/main/java/roomescape/auth/Login.java @@ -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.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface Login { +} diff --git a/src/main/java/roomescape/auth/LoginMemberArgumentResolver.java b/src/main/java/roomescape/auth/LoginMemberArgumentResolver.java index 4571177f5..ff05b4f5b 100644 --- a/src/main/java/roomescape/auth/LoginMemberArgumentResolver.java +++ b/src/main/java/roomescape/auth/LoginMemberArgumentResolver.java @@ -24,7 +24,10 @@ public LoginMemberArgumentResolver(AuthService authService, CookieUtil cookieUti @Override public boolean supportsParameter(MethodParameter parameter) { - return parameter.getParameterType().equals(LoginMember.class); + boolean isLoginMember = parameter.getParameterType().equals(LoginMember.class); + boolean hasLoginAnno = parameter.hasParameterAnnotation(Login.class); + + return isLoginMember && hasLoginAnno; } @Override diff --git a/src/main/java/roomescape/reservation/ReservationController.java b/src/main/java/roomescape/reservation/ReservationController.java index 39ff338d5..468a1db14 100644 --- a/src/main/java/roomescape/reservation/ReservationController.java +++ b/src/main/java/roomescape/reservation/ReservationController.java @@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +import roomescape.auth.Login; import roomescape.auth.LoginMember; import java.net.URI; @@ -27,14 +28,7 @@ public List list() { } @PostMapping("/reservations") - public ResponseEntity create(@RequestBody ReservationRequest reservationRequest, LoginMember loginMember) { - if ((reservationRequest.getName() == null && loginMember == null) - || reservationRequest.getDate() == null - || reservationRequest.getTheme() == null - || reservationRequest.getTime() == null) { - return ResponseEntity.badRequest().build(); - } - + public ResponseEntity create(@RequestBody ReservationRequest reservationRequest, @Login LoginMember loginMember) { ReservationResponse reservation = reservationService.save(reservationRequest, loginMember); return ResponseEntity.created(URI.create("/reservations/" + reservation.getId())).body(reservation); From 8acaa21e80a71bcc263a17da70adfa4dae292961 Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Thu, 2 Jul 2026 18:27:47 +0900 Subject: [PATCH 14/83] =?UTF-8?q?refactor:=20bean=20validation=EC=9D=84=20?= =?UTF-8?q?=ED=86=B5=ED=95=B4=20=EA=B2=80=EC=A6=9D=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/roomescape/member/MemberController.java | 3 ++- src/main/java/roomescape/member/MemberRequest.java | 11 ++++++++++- .../roomescape/reservation/ReservationController.java | 3 ++- .../roomescape/reservation/ReservationRequest.java | 11 +++++++++++ 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/main/java/roomescape/member/MemberController.java b/src/main/java/roomescape/member/MemberController.java index 43c1dcd63..bdd764c1e 100644 --- a/src/main/java/roomescape/member/MemberController.java +++ b/src/main/java/roomescape/member/MemberController.java @@ -1,5 +1,6 @@ package roomescape.member; +import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -16,7 +17,7 @@ public MemberController(MemberService memberService) { } @PostMapping("/members") - public ResponseEntity createMember(@RequestBody MemberRequest memberRequest) { + public ResponseEntity createMember(@Valid @RequestBody MemberRequest memberRequest) { MemberResponse member = memberService.createMember(memberRequest); return ResponseEntity.created(URI.create("/members/" + member.getId())).body(member); } diff --git a/src/main/java/roomescape/member/MemberRequest.java b/src/main/java/roomescape/member/MemberRequest.java index cafb79f14..1dbe192cb 100644 --- a/src/main/java/roomescape/member/MemberRequest.java +++ b/src/main/java/roomescape/member/MemberRequest.java @@ -1,10 +1,19 @@ package roomescape.member; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + public class MemberRequest { + + @NotBlank(message = "이름은 필수입니다.") private String name; + + @NotBlank(message = "이메일은 필수입니다.") + @Email(message = "이메일 형식이 올바르지 않습니다.") private String email; - private String password; + @NotBlank(message = "비밀번호는 필수입니다.") + private String password; public String getName() { return name; } diff --git a/src/main/java/roomescape/reservation/ReservationController.java b/src/main/java/roomescape/reservation/ReservationController.java index 468a1db14..9eb866664 100644 --- a/src/main/java/roomescape/reservation/ReservationController.java +++ b/src/main/java/roomescape/reservation/ReservationController.java @@ -1,5 +1,6 @@ package roomescape.reservation; +import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -28,7 +29,7 @@ public List list() { } @PostMapping("/reservations") - public ResponseEntity create(@RequestBody ReservationRequest reservationRequest, @Login LoginMember loginMember) { + public ResponseEntity create(@Valid @RequestBody ReservationRequest reservationRequest, @Login LoginMember loginMember) { ReservationResponse reservation = reservationService.save(reservationRequest, loginMember); return ResponseEntity.created(URI.create("/reservations/" + reservation.getId())).body(reservation); diff --git a/src/main/java/roomescape/reservation/ReservationRequest.java b/src/main/java/roomescape/reservation/ReservationRequest.java index 19f441246..fdedb125f 100644 --- a/src/main/java/roomescape/reservation/ReservationRequest.java +++ b/src/main/java/roomescape/reservation/ReservationRequest.java @@ -1,9 +1,20 @@ package roomescape.reservation; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + public class ReservationRequest { + + @NotBlank(message = "이름은 필수입니다.") private String name; + + @NotBlank(message = "날짜는 필수입니다.") private String date; + + @NotNull(message = "테마는 필수입니다.") private Long theme; + + @NotNull(message = "시간은 필수입니다.") private Long time; public String getName() { From 787f1a671c62a0bb531cd15b08ade61d61ddbb33 Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Thu, 2 Jul 2026 18:28:31 +0900 Subject: [PATCH 15/83] =?UTF-8?q?refactor:=20CookieUtil=EC=9D=98=20?= =?UTF-8?q?=EC=B1=85=EC=9E=84=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/LoginMemberArgumentResolver.java | 4 +++- src/main/java/roomescape/util/CookieUtil.java | 22 +++++++------------ 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/main/java/roomescape/auth/LoginMemberArgumentResolver.java b/src/main/java/roomescape/auth/LoginMemberArgumentResolver.java index ff05b4f5b..3b36fa78b 100644 --- a/src/main/java/roomescape/auth/LoginMemberArgumentResolver.java +++ b/src/main/java/roomescape/auth/LoginMemberArgumentResolver.java @@ -40,7 +40,9 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m throw new ApplicationException(AuthErrorCode.UNAUTHENTICATED_ACCESS); } - String token = cookieUtil.extractToken(cookies); + String token = cookieUtil.extractToken(cookies) + .orElseThrow(() -> new ApplicationException(AuthErrorCode.UNAUTHENTICATED_ACCESS)); + return authService.findAuthenticatedMember(token); } } diff --git a/src/main/java/roomescape/util/CookieUtil.java b/src/main/java/roomescape/util/CookieUtil.java index 36a10239d..7b17ab7b5 100644 --- a/src/main/java/roomescape/util/CookieUtil.java +++ b/src/main/java/roomescape/util/CookieUtil.java @@ -3,8 +3,9 @@ import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; import org.springframework.stereotype.Component; -import roomescape.auth.AuthErrorCode; -import roomescape.exception.ApplicationException; + +import java.util.Arrays; +import java.util.Optional; @Component public class CookieUtil { @@ -24,17 +25,10 @@ public void expireCookie(HttpServletResponse response) { response.addCookie(cookie); } - public String extractToken(Cookie[] cookies) { - if (cookies == null) { - throw new ApplicationException(AuthErrorCode.UNAUTHENTICATED_ACCESS); - } - - for (Cookie cookie : cookies) { - if (cookie.getName().equals("token")) { - return cookie.getValue(); - } - } - - throw new ApplicationException(AuthErrorCode.UNAUTHENTICATED_ACCESS); + public Optional extractToken(Cookie[] cookies) { + return Arrays.stream(cookies) + .filter(cookie -> cookie.getName().equals("token")) + .map(Cookie::getValue) + .findFirst(); } } From 6d4cc45b5b959236e20ffa1acfeafff259596078 Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Thu, 2 Jul 2026 18:29:17 +0900 Subject: [PATCH 16/83] =?UTF-8?q?fix:=20=ED=8C=8C=EC=9D=BC=20=EB=81=9D?= =?UTF-8?q?=EC=97=90=20=EA=B0=9C=ED=96=89=20=EB=AC=B8=EC=9E=90=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.properties | 2 +- src/test/java/roomescape/MissionStepTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 785b026b6..b5b755bcc 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -9,4 +9,4 @@ spring.datasource.url=jdbc:h2:mem:database #spring.jpa.defer-datasource-initialization=true roomescape.auth.jwt.secret-key=Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E= -roomescape.auth.jwt.expiration=900000 \ No newline at end of file +roomescape.auth.jwt.expiration=900000 diff --git a/src/test/java/roomescape/MissionStepTest.java b/src/test/java/roomescape/MissionStepTest.java index 66581d827..4dbae86c6 100644 --- a/src/test/java/roomescape/MissionStepTest.java +++ b/src/test/java/roomescape/MissionStepTest.java @@ -84,4 +84,4 @@ private String createToken(String email, String password) { return response.headers().get("Set-Cookie").getValue().split(";")[0].split("=")[1]; } -} \ No newline at end of file +} From 219413cd8022a4a26b2402cda93f1c1d7dac7e79 Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Thu, 2 Jul 2026 18:29:52 +0900 Subject: [PATCH 17/83] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20import=EB=AC=B8=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/roomescape/auth/AuthController.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/roomescape/auth/AuthController.java b/src/main/java/roomescape/auth/AuthController.java index cbc5b6426..0b5e90e57 100644 --- a/src/main/java/roomescape/auth/AuthController.java +++ b/src/main/java/roomescape/auth/AuthController.java @@ -1,6 +1,5 @@ package roomescape.auth; -import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; From 5d94f7ec5d1535ca94e5501a63ad01b6d756f261 Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Thu, 2 Jul 2026 19:10:38 +0900 Subject: [PATCH 18/83] =?UTF-8?q?refactor:=20=EC=98=88=EC=95=BD=EC=9E=90?= =?UTF-8?q?=20name=20=EA=B2=80=EC=A6=9D=EC=9D=84=20DTO=EC=97=90=EC=84=9C?= =?UTF-8?q?=20service=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/roomescape/reservation/ReservationRequest.java | 1 - src/main/java/roomescape/reservation/ReservationService.java | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/roomescape/reservation/ReservationRequest.java b/src/main/java/roomescape/reservation/ReservationRequest.java index fdedb125f..f23202eef 100644 --- a/src/main/java/roomescape/reservation/ReservationRequest.java +++ b/src/main/java/roomescape/reservation/ReservationRequest.java @@ -5,7 +5,6 @@ public class ReservationRequest { - @NotBlank(message = "이름은 필수입니다.") private String name; @NotBlank(message = "날짜는 필수입니다.") diff --git a/src/main/java/roomescape/reservation/ReservationService.java b/src/main/java/roomescape/reservation/ReservationService.java index 4ac03fe99..979229616 100644 --- a/src/main/java/roomescape/reservation/ReservationService.java +++ b/src/main/java/roomescape/reservation/ReservationService.java @@ -37,7 +37,7 @@ public List findAll() { } private String resolveName(ReservationRequest reservationRequest, LoginMember loginMember) { - if (reservationRequest.getName() != null) { + if (reservationRequest.getName() != null && !reservationRequest.getName().isBlank()) { memberDao.findByName(reservationRequest.getName()) .orElseThrow(() -> new ApplicationException(MemberErrorCode.MEMBER_NOT_FOUND)); return reservationRequest.getName(); From 7daa481598ee759cf2450c48e3ee949452893847 Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Fri, 3 Jul 2026 15:26:10 +0900 Subject: [PATCH 19/83] =?UTF-8?q?refactor:=20=EB=8F=84=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=EB=B3=84=20=ED=8C=A8=ED=82=A4=EC=A7=80=EB=A5=BC=20=EA=B3=84?= =?UTF-8?q?=EC=B8=B5=20=EA=B5=AC=EC=A1=B0=EB=A1=9C=20=EC=9E=AC=EA=B5=AC?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/{ => controller}/AuthController.java | 7 ++++++- .../roomescape/auth/{ => domain}/LoginMember.java | 2 +- .../auth/{ => dto}/LoginCheckResponse.java | 2 +- .../java/roomescape/auth/{ => dto}/LoginRequest.java | 2 +- .../auth/{ => exception}/AuthErrorCode.java | 2 +- .../roomescape/auth/{ => jwt}/JwtTokenProperty.java | 2 +- .../roomescape/auth/{ => jwt}/JwtTokenProvider.java | 5 +++-- .../roomescape/auth/{ => service}/AuthService.java | 10 +++++++--- src/main/java/roomescape/auth/{ => web}/Login.java | 2 +- .../auth/{ => web}/LoginMemberArgumentResolver.java | 5 ++++- src/main/java/roomescape/config/WebConfig.java | 3 ++- .../member/{ => controller}/MemberController.java | 5 ++++- .../java/roomescape/member/{ => domain}/Member.java | 2 +- .../roomescape/member/{ => dto}/MemberRequest.java | 3 ++- .../roomescape/member/{ => dto}/MemberResponse.java | 2 +- .../member/{ => exception}/MemberErrorCode.java | 2 +- .../member/{ => repository}/MemberDao.java | 3 ++- .../member/{ => service}/MemberService.java | 4 ++++ .../{ => controller}/ReservationController.java | 9 ++++++--- .../reservation/{ => domain}/Reservation.java | 6 +++--- .../reservation/{ => dto}/ReservationRequest.java | 2 +- .../reservation/{ => dto}/ReservationResponse.java | 2 +- .../reservation/{ => repository}/ReservationDao.java | 8 +++++--- .../{ => service}/ReservationService.java | 12 ++++++++---- .../theme/{ => controller}/ThemeController.java | 4 +++- .../java/roomescape/theme/{ => domain}/Theme.java | 2 +- .../roomescape/theme/{ => repository}/ThemeDao.java | 3 ++- .../time/{ => controller}/TimeController.java | 5 ++++- src/main/java/roomescape/time/{ => domain}/Time.java | 2 +- .../roomescape/time/{ => dto}/AvailableTime.java | 2 +- .../roomescape/time/{ => repository}/TimeDao.java | 3 ++- .../roomescape/time/{ => service}/TimeService.java | 9 ++++++--- 32 files changed, 87 insertions(+), 45 deletions(-) rename src/main/java/roomescape/auth/{ => controller}/AuthController.java (85%) rename src/main/java/roomescape/auth/{ => domain}/LoginMember.java (78%) rename src/main/java/roomescape/auth/{ => dto}/LoginCheckResponse.java (67%) rename src/main/java/roomescape/auth/{ => dto}/LoginRequest.java (91%) rename src/main/java/roomescape/auth/{ => exception}/AuthErrorCode.java (95%) rename src/main/java/roomescape/auth/{ => jwt}/JwtTokenProperty.java (88%) rename src/main/java/roomescape/auth/{ => jwt}/JwtTokenProvider.java (94%) rename src/main/java/roomescape/auth/{ => service}/AuthService.java (77%) rename src/main/java/roomescape/auth/{ => web}/Login.java (89%) rename src/main/java/roomescape/auth/{ => web}/LoginMemberArgumentResolver.java (92%) rename src/main/java/roomescape/member/{ => controller}/MemberController.java (81%) rename src/main/java/roomescape/member/{ => domain}/Member.java (96%) rename src/main/java/roomescape/member/{ => dto}/MemberRequest.java (95%) rename src/main/java/roomescape/member/{ => dto}/MemberResponse.java (93%) rename src/main/java/roomescape/member/{ => exception}/MemberErrorCode.java (94%) rename src/main/java/roomescape/member/{ => repository}/MemberDao.java (96%) rename src/main/java/roomescape/member/{ => service}/MemberService.java (75%) rename src/main/java/roomescape/reservation/{ => controller}/ReservationController.java (83%) rename src/main/java/roomescape/reservation/{ => domain}/Reservation.java (88%) rename src/main/java/roomescape/reservation/{ => dto}/ReservationRequest.java (94%) rename src/main/java/roomescape/reservation/{ => dto}/ReservationResponse.java (94%) rename src/main/java/roomescape/reservation/{ => repository}/ReservationDao.java (96%) rename src/main/java/roomescape/reservation/{ => service}/ReservationService.java (80%) rename src/main/java/roomescape/theme/{ => controller}/ThemeController.java (90%) rename src/main/java/roomescape/theme/{ => domain}/Theme.java (94%) rename src/main/java/roomescape/theme/{ => repository}/ThemeDao.java (94%) rename src/main/java/roomescape/time/{ => controller}/TimeController.java (91%) rename src/main/java/roomescape/time/{ => domain}/Time.java (92%) rename src/main/java/roomescape/time/{ => dto}/AvailableTime.java (93%) rename src/main/java/roomescape/time/{ => repository}/TimeDao.java (94%) rename src/main/java/roomescape/time/{ => service}/TimeService.java (81%) diff --git a/src/main/java/roomescape/auth/AuthController.java b/src/main/java/roomescape/auth/controller/AuthController.java similarity index 85% rename from src/main/java/roomescape/auth/AuthController.java rename to src/main/java/roomescape/auth/controller/AuthController.java index 0b5e90e57..bbde68d14 100644 --- a/src/main/java/roomescape/auth/AuthController.java +++ b/src/main/java/roomescape/auth/controller/AuthController.java @@ -1,4 +1,4 @@ -package roomescape.auth; +package roomescape.auth.controller; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; @@ -7,6 +7,11 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +import roomescape.auth.domain.LoginMember; +import roomescape.auth.dto.LoginCheckResponse; +import roomescape.auth.dto.LoginRequest; +import roomescape.auth.service.AuthService; +import roomescape.auth.web.Login; import roomescape.util.CookieUtil; @RestController diff --git a/src/main/java/roomescape/auth/LoginMember.java b/src/main/java/roomescape/auth/domain/LoginMember.java similarity index 78% rename from src/main/java/roomescape/auth/LoginMember.java rename to src/main/java/roomescape/auth/domain/LoginMember.java index c12d9d3df..c75367953 100644 --- a/src/main/java/roomescape/auth/LoginMember.java +++ b/src/main/java/roomescape/auth/domain/LoginMember.java @@ -1,4 +1,4 @@ -package roomescape.auth; +package roomescape.auth.domain; public record LoginMember( Long id, diff --git a/src/main/java/roomescape/auth/LoginCheckResponse.java b/src/main/java/roomescape/auth/dto/LoginCheckResponse.java similarity index 67% rename from src/main/java/roomescape/auth/LoginCheckResponse.java rename to src/main/java/roomescape/auth/dto/LoginCheckResponse.java index 32e011cb4..8aa31879f 100644 --- a/src/main/java/roomescape/auth/LoginCheckResponse.java +++ b/src/main/java/roomescape/auth/dto/LoginCheckResponse.java @@ -1,4 +1,4 @@ -package roomescape.auth; +package roomescape.auth.dto; public record LoginCheckResponse( String name diff --git a/src/main/java/roomescape/auth/LoginRequest.java b/src/main/java/roomescape/auth/dto/LoginRequest.java similarity index 91% rename from src/main/java/roomescape/auth/LoginRequest.java rename to src/main/java/roomescape/auth/dto/LoginRequest.java index d3602fbd3..3ff68eb30 100644 --- a/src/main/java/roomescape/auth/LoginRequest.java +++ b/src/main/java/roomescape/auth/dto/LoginRequest.java @@ -1,4 +1,4 @@ -package roomescape.auth; +package roomescape.auth.dto; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/roomescape/auth/AuthErrorCode.java b/src/main/java/roomescape/auth/exception/AuthErrorCode.java similarity index 95% rename from src/main/java/roomescape/auth/AuthErrorCode.java rename to src/main/java/roomescape/auth/exception/AuthErrorCode.java index 78d3c64a5..377741d2b 100644 --- a/src/main/java/roomescape/auth/AuthErrorCode.java +++ b/src/main/java/roomescape/auth/exception/AuthErrorCode.java @@ -1,4 +1,4 @@ -package roomescape.auth; +package roomescape.auth.exception; import org.springframework.http.HttpStatus; import roomescape.exception.ErrorCode; diff --git a/src/main/java/roomescape/auth/JwtTokenProperty.java b/src/main/java/roomescape/auth/jwt/JwtTokenProperty.java similarity index 88% rename from src/main/java/roomescape/auth/JwtTokenProperty.java rename to src/main/java/roomescape/auth/jwt/JwtTokenProperty.java index 172d31142..1336fe9ce 100644 --- a/src/main/java/roomescape/auth/JwtTokenProperty.java +++ b/src/main/java/roomescape/auth/jwt/JwtTokenProperty.java @@ -1,4 +1,4 @@ -package roomescape.auth; +package roomescape.auth.jwt; import org.springframework.boot.context.properties.ConfigurationProperties; diff --git a/src/main/java/roomescape/auth/JwtTokenProvider.java b/src/main/java/roomescape/auth/jwt/JwtTokenProvider.java similarity index 94% rename from src/main/java/roomescape/auth/JwtTokenProvider.java rename to src/main/java/roomescape/auth/jwt/JwtTokenProvider.java index 0a3ca34fc..db4365870 100644 --- a/src/main/java/roomescape/auth/JwtTokenProvider.java +++ b/src/main/java/roomescape/auth/jwt/JwtTokenProvider.java @@ -1,4 +1,4 @@ -package roomescape.auth; +package roomescape.auth.jwt; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; @@ -7,8 +7,9 @@ import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.SignatureException; import org.springframework.stereotype.Component; +import roomescape.auth.exception.AuthErrorCode; import roomescape.exception.ApplicationException; -import roomescape.member.Member; +import roomescape.member.domain.Member; import java.security.Key; import java.util.Base64; diff --git a/src/main/java/roomescape/auth/AuthService.java b/src/main/java/roomescape/auth/service/AuthService.java similarity index 77% rename from src/main/java/roomescape/auth/AuthService.java rename to src/main/java/roomescape/auth/service/AuthService.java index f1cde05ca..dddda8f33 100644 --- a/src/main/java/roomescape/auth/AuthService.java +++ b/src/main/java/roomescape/auth/service/AuthService.java @@ -1,9 +1,13 @@ -package roomescape.auth; +package roomescape.auth.service; import org.springframework.stereotype.Service; +import roomescape.auth.domain.LoginMember; +import roomescape.auth.dto.LoginRequest; +import roomescape.auth.exception.AuthErrorCode; +import roomescape.auth.jwt.JwtTokenProvider; import roomescape.exception.ApplicationException; -import roomescape.member.Member; -import roomescape.member.MemberDao; +import roomescape.member.domain.Member; +import roomescape.member.repository.MemberDao; @Service public class AuthService { diff --git a/src/main/java/roomescape/auth/Login.java b/src/main/java/roomescape/auth/web/Login.java similarity index 89% rename from src/main/java/roomescape/auth/Login.java rename to src/main/java/roomescape/auth/web/Login.java index e691f5ee9..50e5bbdeb 100644 --- a/src/main/java/roomescape/auth/Login.java +++ b/src/main/java/roomescape/auth/web/Login.java @@ -1,4 +1,4 @@ -package roomescape.auth; +package roomescape.auth.web; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/src/main/java/roomescape/auth/LoginMemberArgumentResolver.java b/src/main/java/roomescape/auth/web/LoginMemberArgumentResolver.java similarity index 92% rename from src/main/java/roomescape/auth/LoginMemberArgumentResolver.java rename to src/main/java/roomescape/auth/web/LoginMemberArgumentResolver.java index 3b36fa78b..00ed4d25e 100644 --- a/src/main/java/roomescape/auth/LoginMemberArgumentResolver.java +++ b/src/main/java/roomescape/auth/web/LoginMemberArgumentResolver.java @@ -1,4 +1,4 @@ -package roomescape.auth; +package roomescape.auth.web; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; @@ -8,6 +8,9 @@ import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; +import roomescape.auth.domain.LoginMember; +import roomescape.auth.exception.AuthErrorCode; +import roomescape.auth.service.AuthService; import roomescape.exception.ApplicationException; import roomescape.util.CookieUtil; diff --git a/src/main/java/roomescape/config/WebConfig.java b/src/main/java/roomescape/config/WebConfig.java index 21ffccd81..a91b83841 100644 --- a/src/main/java/roomescape/config/WebConfig.java +++ b/src/main/java/roomescape/config/WebConfig.java @@ -3,7 +3,8 @@ import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import roomescape.auth.LoginMemberArgumentResolver; +import roomescape.auth.web.CheckAdminInterceptor; +import roomescape.auth.web.LoginMemberArgumentResolver; import java.util.List; diff --git a/src/main/java/roomescape/member/MemberController.java b/src/main/java/roomescape/member/controller/MemberController.java similarity index 81% rename from src/main/java/roomescape/member/MemberController.java rename to src/main/java/roomescape/member/controller/MemberController.java index bdd764c1e..7ce0827d7 100644 --- a/src/main/java/roomescape/member/MemberController.java +++ b/src/main/java/roomescape/member/controller/MemberController.java @@ -1,10 +1,13 @@ -package roomescape.member; +package roomescape.member.controller; import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +import roomescape.member.dto.MemberRequest; +import roomescape.member.dto.MemberResponse; +import roomescape.member.service.MemberService; import java.net.URI; diff --git a/src/main/java/roomescape/member/Member.java b/src/main/java/roomescape/member/domain/Member.java similarity index 96% rename from src/main/java/roomescape/member/Member.java rename to src/main/java/roomescape/member/domain/Member.java index 903aaa9b0..ae754e154 100644 --- a/src/main/java/roomescape/member/Member.java +++ b/src/main/java/roomescape/member/domain/Member.java @@ -1,4 +1,4 @@ -package roomescape.member; +package roomescape.member.domain; public class Member { private Long id; diff --git a/src/main/java/roomescape/member/MemberRequest.java b/src/main/java/roomescape/member/dto/MemberRequest.java similarity index 95% rename from src/main/java/roomescape/member/MemberRequest.java rename to src/main/java/roomescape/member/dto/MemberRequest.java index 1dbe192cb..309b8a2b7 100644 --- a/src/main/java/roomescape/member/MemberRequest.java +++ b/src/main/java/roomescape/member/dto/MemberRequest.java @@ -1,4 +1,4 @@ -package roomescape.member; +package roomescape.member.dto; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; @@ -14,6 +14,7 @@ public class MemberRequest { @NotBlank(message = "비밀번호는 필수입니다.") private String password; + public String getName() { return name; } diff --git a/src/main/java/roomescape/member/MemberResponse.java b/src/main/java/roomescape/member/dto/MemberResponse.java similarity index 93% rename from src/main/java/roomescape/member/MemberResponse.java rename to src/main/java/roomescape/member/dto/MemberResponse.java index b9fa3b97a..b3235d2c3 100644 --- a/src/main/java/roomescape/member/MemberResponse.java +++ b/src/main/java/roomescape/member/dto/MemberResponse.java @@ -1,4 +1,4 @@ -package roomescape.member; +package roomescape.member.dto; public class MemberResponse { private Long id; diff --git a/src/main/java/roomescape/member/MemberErrorCode.java b/src/main/java/roomescape/member/exception/MemberErrorCode.java similarity index 94% rename from src/main/java/roomescape/member/MemberErrorCode.java rename to src/main/java/roomescape/member/exception/MemberErrorCode.java index b2ff5223a..d6c663751 100644 --- a/src/main/java/roomescape/member/MemberErrorCode.java +++ b/src/main/java/roomescape/member/exception/MemberErrorCode.java @@ -1,4 +1,4 @@ -package roomescape.member; +package roomescape.member.exception; import org.springframework.http.HttpStatus; import roomescape.exception.ErrorCode; diff --git a/src/main/java/roomescape/member/MemberDao.java b/src/main/java/roomescape/member/repository/MemberDao.java similarity index 96% rename from src/main/java/roomescape/member/MemberDao.java rename to src/main/java/roomescape/member/repository/MemberDao.java index c748984c4..6c19d9eb1 100644 --- a/src/main/java/roomescape/member/MemberDao.java +++ b/src/main/java/roomescape/member/repository/MemberDao.java @@ -1,10 +1,11 @@ -package roomescape.member; +package roomescape.member.repository; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.support.GeneratedKeyHolder; import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Repository; +import roomescape.member.domain.Member; import java.util.Optional; diff --git a/src/main/java/roomescape/member/MemberService.java b/src/main/java/roomescape/member/service/MemberService.java similarity index 75% rename from src/main/java/roomescape/member/MemberService.java rename to src/main/java/roomescape/member/service/MemberService.java index ccaa8cba5..4d4b73098 100644 --- a/src/main/java/roomescape/member/MemberService.java +++ b/src/main/java/roomescape/member/service/MemberService.java @@ -1,6 +1,10 @@ package roomescape.member; import org.springframework.stereotype.Service; +import roomescape.member.domain.Member; +import roomescape.member.dto.MemberRequest; +import roomescape.member.dto.MemberResponse; +import roomescape.member.repository.MemberDao; @Service public class MemberService { diff --git a/src/main/java/roomescape/reservation/ReservationController.java b/src/main/java/roomescape/reservation/controller/ReservationController.java similarity index 83% rename from src/main/java/roomescape/reservation/ReservationController.java rename to src/main/java/roomescape/reservation/controller/ReservationController.java index 9eb866664..98259fd07 100644 --- a/src/main/java/roomescape/reservation/ReservationController.java +++ b/src/main/java/roomescape/reservation/controller/ReservationController.java @@ -1,4 +1,4 @@ -package roomescape.reservation; +package roomescape.reservation.controller; import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; @@ -8,8 +8,11 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; -import roomescape.auth.Login; -import roomescape.auth.LoginMember; +import roomescape.auth.domain.LoginMember; +import roomescape.auth.web.Login; +import roomescape.reservation.dto.ReservationRequest; +import roomescape.reservation.dto.ReservationResponse; +import roomescape.reservation.service.ReservationService; import java.net.URI; import java.util.List; diff --git a/src/main/java/roomescape/reservation/Reservation.java b/src/main/java/roomescape/reservation/domain/Reservation.java similarity index 88% rename from src/main/java/roomescape/reservation/Reservation.java rename to src/main/java/roomescape/reservation/domain/Reservation.java index 83a7edf1b..75b1b538f 100644 --- a/src/main/java/roomescape/reservation/Reservation.java +++ b/src/main/java/roomescape/reservation/domain/Reservation.java @@ -1,7 +1,7 @@ -package roomescape.reservation; +package roomescape.reservation.domain; -import roomescape.theme.Theme; -import roomescape.time.Time; +import roomescape.theme.domain.Theme; +import roomescape.time.domain.Time; public class Reservation { private Long id; diff --git a/src/main/java/roomescape/reservation/ReservationRequest.java b/src/main/java/roomescape/reservation/dto/ReservationRequest.java similarity index 94% rename from src/main/java/roomescape/reservation/ReservationRequest.java rename to src/main/java/roomescape/reservation/dto/ReservationRequest.java index f23202eef..56f6ec41a 100644 --- a/src/main/java/roomescape/reservation/ReservationRequest.java +++ b/src/main/java/roomescape/reservation/dto/ReservationRequest.java @@ -1,4 +1,4 @@ -package roomescape.reservation; +package roomescape.reservation.dto; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; diff --git a/src/main/java/roomescape/reservation/ReservationResponse.java b/src/main/java/roomescape/reservation/dto/ReservationResponse.java similarity index 94% rename from src/main/java/roomescape/reservation/ReservationResponse.java rename to src/main/java/roomescape/reservation/dto/ReservationResponse.java index 41360a363..2c219d018 100644 --- a/src/main/java/roomescape/reservation/ReservationResponse.java +++ b/src/main/java/roomescape/reservation/dto/ReservationResponse.java @@ -1,4 +1,4 @@ -package roomescape.reservation; +package roomescape.reservation.dto; public class ReservationResponse { private Long id; diff --git a/src/main/java/roomescape/reservation/ReservationDao.java b/src/main/java/roomescape/reservation/repository/ReservationDao.java similarity index 96% rename from src/main/java/roomescape/reservation/ReservationDao.java rename to src/main/java/roomescape/reservation/repository/ReservationDao.java index 0a9d2b0c7..c92dbba35 100644 --- a/src/main/java/roomescape/reservation/ReservationDao.java +++ b/src/main/java/roomescape/reservation/repository/ReservationDao.java @@ -1,11 +1,13 @@ -package roomescape.reservation; +package roomescape.reservation.repository; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.support.GeneratedKeyHolder; import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Repository; -import roomescape.theme.Theme; -import roomescape.time.Time; +import roomescape.reservation.domain.Reservation; +import roomescape.reservation.dto.ReservationRequest; +import roomescape.theme.domain.Theme; +import roomescape.time.domain.Time; import java.sql.PreparedStatement; import java.util.List; diff --git a/src/main/java/roomescape/reservation/ReservationService.java b/src/main/java/roomescape/reservation/service/ReservationService.java similarity index 80% rename from src/main/java/roomescape/reservation/ReservationService.java rename to src/main/java/roomescape/reservation/service/ReservationService.java index 979229616..34c8d9532 100644 --- a/src/main/java/roomescape/reservation/ReservationService.java +++ b/src/main/java/roomescape/reservation/service/ReservationService.java @@ -1,10 +1,14 @@ -package roomescape.reservation; +package roomescape.reservation.service; import org.springframework.stereotype.Service; -import roomescape.auth.LoginMember; +import roomescape.auth.domain.LoginMember; import roomescape.exception.ApplicationException; -import roomescape.member.MemberDao; -import roomescape.member.MemberErrorCode; +import roomescape.member.exception.MemberErrorCode; +import roomescape.member.repository.MemberDao; +import roomescape.reservation.domain.Reservation; +import roomescape.reservation.dto.ReservationRequest; +import roomescape.reservation.dto.ReservationResponse; +import roomescape.reservation.repository.ReservationDao; import java.util.List; diff --git a/src/main/java/roomescape/theme/ThemeController.java b/src/main/java/roomescape/theme/controller/ThemeController.java similarity index 90% rename from src/main/java/roomescape/theme/ThemeController.java rename to src/main/java/roomescape/theme/controller/ThemeController.java index 03bca41a6..dd83f58f7 100644 --- a/src/main/java/roomescape/theme/ThemeController.java +++ b/src/main/java/roomescape/theme/controller/ThemeController.java @@ -1,4 +1,4 @@ -package roomescape.theme; +package roomescape.theme.controller; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; @@ -7,6 +7,8 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +import roomescape.theme.domain.Theme; +import roomescape.theme.repository.ThemeDao; import java.net.URI; import java.util.List; diff --git a/src/main/java/roomescape/theme/Theme.java b/src/main/java/roomescape/theme/domain/Theme.java similarity index 94% rename from src/main/java/roomescape/theme/Theme.java rename to src/main/java/roomescape/theme/domain/Theme.java index 430a6239c..9063a4e2c 100644 --- a/src/main/java/roomescape/theme/Theme.java +++ b/src/main/java/roomescape/theme/domain/Theme.java @@ -1,4 +1,4 @@ -package roomescape.theme; +package roomescape.theme.domain; public class Theme { private Long id; diff --git a/src/main/java/roomescape/theme/ThemeDao.java b/src/main/java/roomescape/theme/repository/ThemeDao.java similarity index 94% rename from src/main/java/roomescape/theme/ThemeDao.java rename to src/main/java/roomescape/theme/repository/ThemeDao.java index 945341d8d..5d681c408 100644 --- a/src/main/java/roomescape/theme/ThemeDao.java +++ b/src/main/java/roomescape/theme/repository/ThemeDao.java @@ -1,9 +1,10 @@ -package roomescape.theme; +package roomescape.theme.repository; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.support.GeneratedKeyHolder; import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Repository; +import roomescape.theme.domain.Theme; import java.util.List; diff --git a/src/main/java/roomescape/time/TimeController.java b/src/main/java/roomescape/time/controller/TimeController.java similarity index 91% rename from src/main/java/roomescape/time/TimeController.java rename to src/main/java/roomescape/time/controller/TimeController.java index 2343114d1..edf7da7d3 100644 --- a/src/main/java/roomescape/time/TimeController.java +++ b/src/main/java/roomescape/time/controller/TimeController.java @@ -1,4 +1,4 @@ -package roomescape.time; +package roomescape.time.controller; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; @@ -8,6 +8,9 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import roomescape.time.dto.AvailableTime; +import roomescape.time.domain.Time; +import roomescape.time.service.TimeService; import java.net.URI; import java.util.List; diff --git a/src/main/java/roomescape/time/Time.java b/src/main/java/roomescape/time/domain/Time.java similarity index 92% rename from src/main/java/roomescape/time/Time.java rename to src/main/java/roomescape/time/domain/Time.java index 008ed93cf..c2bfefc84 100644 --- a/src/main/java/roomescape/time/Time.java +++ b/src/main/java/roomescape/time/domain/Time.java @@ -1,4 +1,4 @@ -package roomescape.time; +package roomescape.time.domain; public class Time { private Long id; diff --git a/src/main/java/roomescape/time/AvailableTime.java b/src/main/java/roomescape/time/dto/AvailableTime.java similarity index 93% rename from src/main/java/roomescape/time/AvailableTime.java rename to src/main/java/roomescape/time/dto/AvailableTime.java index 33acef7e3..189a23f22 100644 --- a/src/main/java/roomescape/time/AvailableTime.java +++ b/src/main/java/roomescape/time/dto/AvailableTime.java @@ -1,4 +1,4 @@ -package roomescape.time; +package roomescape.time.dto; public class AvailableTime { private Long timeId; diff --git a/src/main/java/roomescape/time/TimeDao.java b/src/main/java/roomescape/time/repository/TimeDao.java similarity index 94% rename from src/main/java/roomescape/time/TimeDao.java rename to src/main/java/roomescape/time/repository/TimeDao.java index f39a9a328..5b94347a7 100644 --- a/src/main/java/roomescape/time/TimeDao.java +++ b/src/main/java/roomescape/time/repository/TimeDao.java @@ -1,9 +1,10 @@ -package roomescape.time; +package roomescape.time.repository; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.support.GeneratedKeyHolder; import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Repository; +import roomescape.time.domain.Time; import java.sql.PreparedStatement; import java.util.List; diff --git a/src/main/java/roomescape/time/TimeService.java b/src/main/java/roomescape/time/service/TimeService.java similarity index 81% rename from src/main/java/roomescape/time/TimeService.java rename to src/main/java/roomescape/time/service/TimeService.java index 7d0cb9b09..759102101 100644 --- a/src/main/java/roomescape/time/TimeService.java +++ b/src/main/java/roomescape/time/service/TimeService.java @@ -1,8 +1,11 @@ -package roomescape.time; +package roomescape.time.service; import org.springframework.stereotype.Service; -import roomescape.reservation.Reservation; -import roomescape.reservation.ReservationDao; +import roomescape.reservation.domain.Reservation; +import roomescape.reservation.repository.ReservationDao; +import roomescape.time.dto.AvailableTime; +import roomescape.time.domain.Time; +import roomescape.time.repository.TimeDao; import java.util.List; From 1e3fdb3da070aba890f4c7f8867ede1ab85bbbc5 Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Mon, 6 Jul 2026 13:23:03 +0900 Subject: [PATCH 20/83] =?UTF-8?q?refactor:=20=EB=8F=84=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=EB=B3=84=20=ED=8C=A8=ED=82=A4=EC=A7=80=EB=A5=BC=20=EA=B3=84?= =?UTF-8?q?=EC=B8=B5=20=EA=B5=AC=EC=A1=B0=EB=A1=9C=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EC=9E=AC=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/roomescape/member/service/MemberService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/roomescape/member/service/MemberService.java b/src/main/java/roomescape/member/service/MemberService.java index 4d4b73098..8b66546f1 100644 --- a/src/main/java/roomescape/member/service/MemberService.java +++ b/src/main/java/roomescape/member/service/MemberService.java @@ -1,4 +1,4 @@ -package roomescape.member; +package roomescape.member.service; import org.springframework.stereotype.Service; import roomescape.member.domain.Member; From aac8aa349521b53fff6b46d22041092078d1621a Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Mon, 6 Jul 2026 13:24:42 +0900 Subject: [PATCH 21/83] =?UTF-8?q?refactor:=20Member=20=EA=B6=8C=ED=95=9C(R?= =?UTF-8?q?ole)=EC=9D=84=20String=EC=97=90=EC=84=9C=20enum=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/roomescape/auth/domain/LoginMember.java | 5 +++++ .../java/roomescape/auth/service/AuthService.java | 2 +- src/main/java/roomescape/member/domain/Member.java | 8 ++++---- src/main/java/roomescape/member/domain/Role.java | 11 +++++++++++ .../java/roomescape/member/repository/MemberDao.java | 7 ++++--- .../java/roomescape/member/service/MemberService.java | 3 ++- 6 files changed, 27 insertions(+), 9 deletions(-) create mode 100644 src/main/java/roomescape/member/domain/Role.java diff --git a/src/main/java/roomescape/auth/domain/LoginMember.java b/src/main/java/roomescape/auth/domain/LoginMember.java index c75367953..182de5f22 100644 --- a/src/main/java/roomescape/auth/domain/LoginMember.java +++ b/src/main/java/roomescape/auth/domain/LoginMember.java @@ -1,9 +1,14 @@ package roomescape.auth.domain; +import roomescape.member.domain.Role; + public record LoginMember( Long id, String name, String email, String role ) { + public boolean isAdmin() { + return this.role.equals(Role.ADMIN.name()); + } } diff --git a/src/main/java/roomescape/auth/service/AuthService.java b/src/main/java/roomescape/auth/service/AuthService.java index dddda8f33..558224d46 100644 --- a/src/main/java/roomescape/auth/service/AuthService.java +++ b/src/main/java/roomescape/auth/service/AuthService.java @@ -29,6 +29,6 @@ public LoginMember findAuthenticatedMember(String token) { Long memberId = tokenProvider.getLoginMemberId(token); Member member = memberDao.findById(memberId) .orElseThrow(() -> new ApplicationException(AuthErrorCode.UNAUTHENTICATED_ACCESS)); - return new LoginMember(member.getId(), member.getName(), member.getEmail(), member.getRole()); + return new LoginMember(member.getId(), member.getName(), member.getEmail(), member.getRole().name()); } } diff --git a/src/main/java/roomescape/member/domain/Member.java b/src/main/java/roomescape/member/domain/Member.java index ae754e154..29d3e90eb 100644 --- a/src/main/java/roomescape/member/domain/Member.java +++ b/src/main/java/roomescape/member/domain/Member.java @@ -5,16 +5,16 @@ public class Member { private String name; private String email; private String password; - private String role; + private Role role; - public Member(Long id, String name, String email, String role) { + public Member(Long id, String name, String email, Role role) { this.id = id; this.name = name; this.email = email; this.role = role; } - public Member(String name, String email, String password, String role) { + public Member(String name, String email, String password, Role role) { this.name = name; this.email = email; this.password = password; @@ -37,7 +37,7 @@ public String getPassword() { return password; } - public String getRole() { + public Role getRole() { return role; } } diff --git a/src/main/java/roomescape/member/domain/Role.java b/src/main/java/roomescape/member/domain/Role.java new file mode 100644 index 000000000..dd3f5e6c6 --- /dev/null +++ b/src/main/java/roomescape/member/domain/Role.java @@ -0,0 +1,11 @@ +package roomescape.member.domain; + +public enum Role { + + USER, + ADMIN; + + public boolean isAdmin() { + return this == ADMIN; + } +} diff --git a/src/main/java/roomescape/member/repository/MemberDao.java b/src/main/java/roomescape/member/repository/MemberDao.java index 6c19d9eb1..59c5842fe 100644 --- a/src/main/java/roomescape/member/repository/MemberDao.java +++ b/src/main/java/roomescape/member/repository/MemberDao.java @@ -6,6 +6,7 @@ import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Repository; import roomescape.member.domain.Member; +import roomescape.member.domain.Role; import java.util.Optional; @@ -24,11 +25,11 @@ public Member save(Member member) { ps.setString(1, member.getName()); ps.setString(2, member.getEmail()); ps.setString(3, member.getPassword()); - ps.setString(4, member.getRole()); + ps.setString(4, member.getRole().name()); return ps; }, keyHolder); - return new Member(keyHolder.getKey().longValue(), member.getName(), member.getEmail(), "USER"); + return new Member(keyHolder.getKey().longValue(), member.getName(), member.getEmail(), Role.ADMIN); } public Optional findById(Long id) { @@ -60,7 +61,7 @@ private RowMapper rowMapper() { resultSet.getLong("id"), resultSet.getString("name"), resultSet.getString("email"), - resultSet.getString("role") + Role.valueOf(resultSet.getString("role")) ); } } diff --git a/src/main/java/roomescape/member/service/MemberService.java b/src/main/java/roomescape/member/service/MemberService.java index 8b66546f1..f7d938b01 100644 --- a/src/main/java/roomescape/member/service/MemberService.java +++ b/src/main/java/roomescape/member/service/MemberService.java @@ -2,6 +2,7 @@ import org.springframework.stereotype.Service; import roomescape.member.domain.Member; +import roomescape.member.domain.Role; import roomescape.member.dto.MemberRequest; import roomescape.member.dto.MemberResponse; import roomescape.member.repository.MemberDao; @@ -15,7 +16,7 @@ public MemberService(MemberDao memberDao) { } public MemberResponse createMember(MemberRequest memberRequest) { - Member member = memberDao.save(new Member(memberRequest.getName(), memberRequest.getEmail(), memberRequest.getPassword(), "USER")); + Member member = memberDao.save(new Member(memberRequest.getName(), memberRequest.getEmail(), memberRequest.getPassword(), Role.USER)); return new MemberResponse(member.getId(), member.getName(), member.getEmail()); } } From 4093c0baefd076860f0e106dd64c9e9602abbf9b Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Mon, 6 Jul 2026 13:25:17 +0900 Subject: [PATCH 22/83] =?UTF-8?q?feat:=20=EA=B6=8C=ED=95=9C=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EC=9D=B8=EA=B0=80=20?= =?UTF-8?q?Interceptor=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/exception/AuthErrorCode.java | 4 +- .../auth/web/CheckAdminInterceptor.java | 42 +++++++++++++++++++ .../java/roomescape/config/WebConfig.java | 11 ++++- 3 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 src/main/java/roomescape/auth/web/CheckAdminInterceptor.java diff --git a/src/main/java/roomescape/auth/exception/AuthErrorCode.java b/src/main/java/roomescape/auth/exception/AuthErrorCode.java index 377741d2b..597de9bca 100644 --- a/src/main/java/roomescape/auth/exception/AuthErrorCode.java +++ b/src/main/java/roomescape/auth/exception/AuthErrorCode.java @@ -6,7 +6,9 @@ public enum AuthErrorCode implements ErrorCode { ACCESS_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "토큰이 만료되었습니다."), INVALID_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰입니다."), - UNAUTHENTICATED_ACCESS(HttpStatus.UNAUTHORIZED, "인증되지 않은 요청입니다."); + UNAUTHENTICATED_ACCESS(HttpStatus.UNAUTHORIZED, "인증되지 않은 요청입니다."), + + UNAUTHORIZED_REQUEST(HttpStatus.FORBIDDEN, "권한이 없습니다."); private HttpStatus httpStatus; private String message; diff --git a/src/main/java/roomescape/auth/web/CheckAdminInterceptor.java b/src/main/java/roomescape/auth/web/CheckAdminInterceptor.java new file mode 100644 index 000000000..ac11e83c7 --- /dev/null +++ b/src/main/java/roomescape/auth/web/CheckAdminInterceptor.java @@ -0,0 +1,42 @@ +package roomescape.auth.web; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; +import roomescape.auth.domain.LoginMember; +import roomescape.auth.exception.AuthErrorCode; +import roomescape.auth.service.AuthService; +import roomescape.exception.ApplicationException; +import roomescape.util.CookieUtil; + +@Component +public class CheckAdminInterceptor implements HandlerInterceptor { + + private final CookieUtil cookieUtil; + private final AuthService authService; + + public CheckAdminInterceptor(CookieUtil cookieUtil, AuthService authService) { + this.cookieUtil = cookieUtil; + this.authService = authService; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + Cookie[] cookies = request.getCookies(); + if (cookies == null) { + throw new ApplicationException(AuthErrorCode.UNAUTHENTICATED_ACCESS); + } + + String token = cookieUtil.extractToken(cookies) + .orElseThrow(() -> new ApplicationException(AuthErrorCode.UNAUTHENTICATED_ACCESS)); + + LoginMember member = authService.findAuthenticatedMember(token); + if (member == null || !member.isAdmin()) { + throw new ApplicationException(AuthErrorCode.UNAUTHORIZED_REQUEST); + } + + return true; + } +} diff --git a/src/main/java/roomescape/config/WebConfig.java b/src/main/java/roomescape/config/WebConfig.java index a91b83841..44332017b 100644 --- a/src/main/java/roomescape/config/WebConfig.java +++ b/src/main/java/roomescape/config/WebConfig.java @@ -2,6 +2,7 @@ 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.web.CheckAdminInterceptor; import roomescape.auth.web.LoginMemberArgumentResolver; @@ -12,13 +13,21 @@ public class WebConfig implements WebMvcConfigurer { private final LoginMemberArgumentResolver loginMemberArgumentResolver; + private final CheckAdminInterceptor checkAdminInterceptor; - public WebConfig(LoginMemberArgumentResolver loginMemberArgumentResolver) { + public WebConfig(LoginMemberArgumentResolver loginMemberArgumentResolver, CheckAdminInterceptor checkAdminInterceptor) { this.loginMemberArgumentResolver = loginMemberArgumentResolver; + this.checkAdminInterceptor = checkAdminInterceptor; } @Override public void addArgumentResolvers(List resolvers) { resolvers.add(loginMemberArgumentResolver); } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(checkAdminInterceptor) + .addPathPatterns("/admin/**"); + } } From 557971dc27eeba189c81669a4bb2ab2366662725 Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Mon, 6 Jul 2026 13:25:29 +0900 Subject: [PATCH 23/83] =?UTF-8?q?test:=203=EB=8B=A8=EA=B3=84=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/roomescape/MissionStepTest.java | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/test/java/roomescape/MissionStepTest.java b/src/test/java/roomescape/MissionStepTest.java index 4dbae86c6..e3016bfe7 100644 --- a/src/test/java/roomescape/MissionStepTest.java +++ b/src/test/java/roomescape/MissionStepTest.java @@ -7,7 +7,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.DirtiesContext; -import roomescape.reservation.ReservationResponse; +import roomescape.reservation.dto.ReservationResponse; import java.util.HashMap; import java.util.Map; @@ -71,6 +71,25 @@ public class MissionStepTest { assertThat(adminResponse.as(ReservationResponse.class).getName()).isEqualTo("브라운"); } + @Test + void 삼단계() { + String brownToken = createToken("brown@email.com", "password"); + + RestAssured.given().log().all() + .cookie("token", brownToken) + .get("/admin") + .then().log().all() + .statusCode(403); + + String adminToken = createToken("admin@email.com", "password"); + + RestAssured.given().log().all() + .cookie("token", adminToken) + .get("/admin") + .then().log().all() + .statusCode(200); + } + private String createToken(String email, String password) { Map params = new HashMap<>(); params.put("email", email); From 1d58693dcf9ea2e5020bfeabacc18f12f2a336b2 Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Mon, 6 Jul 2026 13:26:19 +0900 Subject: [PATCH 24/83] =?UTF-8?q?test:=20import=EB=AC=B8=20=EC=9E=AC?= =?UTF-8?q?=EC=A0=95=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/roomescape/time/controller/TimeController.java | 2 +- src/main/java/roomescape/time/service/TimeService.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/roomescape/time/controller/TimeController.java b/src/main/java/roomescape/time/controller/TimeController.java index edf7da7d3..7a589acb6 100644 --- a/src/main/java/roomescape/time/controller/TimeController.java +++ b/src/main/java/roomescape/time/controller/TimeController.java @@ -8,8 +8,8 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import roomescape.time.dto.AvailableTime; import roomescape.time.domain.Time; +import roomescape.time.dto.AvailableTime; import roomescape.time.service.TimeService; import java.net.URI; diff --git a/src/main/java/roomescape/time/service/TimeService.java b/src/main/java/roomescape/time/service/TimeService.java index 759102101..f03001c76 100644 --- a/src/main/java/roomescape/time/service/TimeService.java +++ b/src/main/java/roomescape/time/service/TimeService.java @@ -3,8 +3,8 @@ import org.springframework.stereotype.Service; import roomescape.reservation.domain.Reservation; import roomescape.reservation.repository.ReservationDao; -import roomescape.time.dto.AvailableTime; import roomescape.time.domain.Time; +import roomescape.time.dto.AvailableTime; import roomescape.time.repository.TimeDao; import java.util.List; From a5fa4263e6a674ef9373da306e229c01a7bc5942 Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Mon, 6 Jul 2026 15:41:53 +0900 Subject: [PATCH 25/83] =?UTF-8?q?style:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EA=B0=9C=ED=96=89=20=EB=AC=B8=EC=9E=90=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/roomescape/reservation/dto/ReservationRequest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/roomescape/reservation/dto/ReservationRequest.java b/src/main/java/roomescape/reservation/dto/ReservationRequest.java index b56cef959..56f6ec41a 100644 --- a/src/main/java/roomescape/reservation/dto/ReservationRequest.java +++ b/src/main/java/roomescape/reservation/dto/ReservationRequest.java @@ -1,6 +1,5 @@ package roomescape.reservation.dto; - import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; From 50a30f4054996d581d21b4c275a6d25621f48750 Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Mon, 6 Jul 2026 17:05:35 +0900 Subject: [PATCH 26/83] =?UTF-8?q?refactor:=20JwtTokenProvider=EC=97=90=20n?= =?UTF-8?q?ame/role=20=EC=B6=94=EC=B6=9C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=ED=8C=8C=EC=8B=B1=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roomescape/auth/jwt/JwtTokenProvider.java | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/main/java/roomescape/auth/jwt/JwtTokenProvider.java b/src/main/java/roomescape/auth/jwt/JwtTokenProvider.java index db4365870..7477e5429 100644 --- a/src/main/java/roomescape/auth/jwt/JwtTokenProvider.java +++ b/src/main/java/roomescape/auth/jwt/JwtTokenProvider.java @@ -1,5 +1,6 @@ package roomescape.auth.jwt; +import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.MalformedJwtException; @@ -41,13 +42,27 @@ public String createAccessToken(Member member) { .compact(); } - public Long getLoginMemberId(String accessToken) { + public Long getLoginMemberId(String token) { + return Long.valueOf( + parseClaims(token).getSubject() + ); + } + + public String getLoginMemberName(String token) { + return parseClaims(token).get("name", String.class); + } + + public String getLoginMemberRole(String token) { + return parseClaims(token).get("role", String.class); + } + + private Claims parseClaims(String token) { try { - return Long.valueOf(Jwts.parserBuilder() + return Jwts.parserBuilder() .setSigningKey(secretKey) .build() - .parseClaimsJws(accessToken) - .getBody().getSubject()); + .parseClaimsJws(token) + .getBody(); } catch (SignatureException | MalformedJwtException | UnsupportedJwtException e) { throw new ApplicationException(AuthErrorCode.INVALID_ACCESS_TOKEN); } catch (ExpiredJwtException e) { From 31bb1149f5486524e7a50a7e43a8f9bbc11b871c Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Mon, 6 Jul 2026 17:07:13 +0900 Subject: [PATCH 27/83] =?UTF-8?q?refactor:=20LoginMember=EC=97=90=EC=84=9C?= =?UTF-8?q?=20email=20=ED=95=84=EB=93=9C=20=EC=A0=9C=EA=B1=B0,=20JWT=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/roomescape/auth/domain/LoginMember.java | 1 - src/main/java/roomescape/auth/service/AuthService.java | 9 ++++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main/java/roomescape/auth/domain/LoginMember.java b/src/main/java/roomescape/auth/domain/LoginMember.java index 182de5f22..704cd2d6e 100644 --- a/src/main/java/roomescape/auth/domain/LoginMember.java +++ b/src/main/java/roomescape/auth/domain/LoginMember.java @@ -5,7 +5,6 @@ public record LoginMember( Long id, String name, - String email, String role ) { public boolean isAdmin() { diff --git a/src/main/java/roomescape/auth/service/AuthService.java b/src/main/java/roomescape/auth/service/AuthService.java index 558224d46..285604f7c 100644 --- a/src/main/java/roomescape/auth/service/AuthService.java +++ b/src/main/java/roomescape/auth/service/AuthService.java @@ -3,9 +3,7 @@ import org.springframework.stereotype.Service; import roomescape.auth.domain.LoginMember; import roomescape.auth.dto.LoginRequest; -import roomescape.auth.exception.AuthErrorCode; import roomescape.auth.jwt.JwtTokenProvider; -import roomescape.exception.ApplicationException; import roomescape.member.domain.Member; import roomescape.member.repository.MemberDao; @@ -27,8 +25,9 @@ public String login(LoginRequest loginRequest) { public LoginMember findAuthenticatedMember(String token) { Long memberId = tokenProvider.getLoginMemberId(token); - Member member = memberDao.findById(memberId) - .orElseThrow(() -> new ApplicationException(AuthErrorCode.UNAUTHENTICATED_ACCESS)); - return new LoginMember(member.getId(), member.getName(), member.getEmail(), member.getRole().name()); + String memberName = tokenProvider.getLoginMemberName(token); + String memberRole = tokenProvider.getLoginMemberRole(token); + + return new LoginMember(memberId, memberName, memberRole); } } From 4975da3b18de30752f6e470ed8afabb38ed0ad0b Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Tue, 7 Jul 2026 14:05:36 +0900 Subject: [PATCH 28/83] =?UTF-8?q?docs:=20README=EC=97=90=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=ED=8E=B8?= =?UTF-8?q?=EA=B3=BC=20=EA=B4=80=EB=A6=AC=EC=9E=90=20=EA=B6=8C=ED=95=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 114 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 72 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 2e441ff08..ca4d5fd7e 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ - 쿠키의 토큰으로 로그인 사용자 정보를 조회합니다 (`GET /login/check`). - 로그아웃 시 `token` 쿠키를 만료시킵니다. - `HandlerMethodArgumentResolver`(`LoginMemberArgumentResolver`)로 컨트롤러에 로그인 사용자(`LoginMember`)를 주입합니다. +- '/admin/**'은 ADMIN 권한을 가진 사용자만 접근 가능합니다. ### 예외 처리 - `ErrorCode` 인터페이스로 예외 상황별 HTTP 상태 코드와 메시지를 관리합니다. (`AuthErrorCode`, `MemberErrorCode` 구현) @@ -23,58 +24,87 @@ ## 프로젝트 구조 ``` roomescape/ -├── RoomescapeApplication.java # 메인 진입점 -├── PageController.java # 뷰(HTML) 페이지 라우팅 +├── RoomescapeApplication.java # 메인 진입점 +├── PageController.java # 뷰(HTML) 페이지 라우팅 │ -├── auth/ # 인증/인가 (JWT) -│ ├── AuthController.java # 로그인/로그아웃/로그인체크 API -│ ├── AuthService.java -│ ├── AuthErrorCode.java -│ ├── JwtTokenProvider.java # JWT 토큰 생성/검증 -│ ├── JwtTokenProperty.java # JWT 설정값 (@ConfigurationProperties) -│ ├── LoginMemberArgumentResolver.java # @LoginMember 주입 -│ ├── LoginMember.java # 인증된 사용자 정보 -│ ├── LoginRequest.java -│ └── LoginCheckResponse.java +├── auth/ # 인증/인가 (JWT) +│ ├── controller/ +│ │ └── AuthController.java # 로그인/로그아웃/로그인체크 API +│ ├── service/ +│ │ └── AuthService.java +│ ├── domain/ +│ │ └── LoginMember.java # 인증된 사용자 정보 +│ ├── dto/ +│ │ ├── LoginRequest.java +│ │ └── LoginCheckResponse.java +│ ├── exception/ +│ │ └── AuthErrorCode.java +│ ├── jwt/ # JWT 토큰 메커니즘 +│ │ ├── JwtTokenProvider.java # 토큰 생성/검증 +│ │ └── JwtTokenProperty.java # JWT 설정값 (@ConfigurationProperties) +│ └── web/ # 웹 계층 확장(인증/인가 글루) +│ ├── Login.java # @Login 커스텀 애노테이션 +│ ├── LoginMemberArgumentResolver.java # @Login LoginMember 주입 +│ └── CheckAdminInterceptor.java # 관리자 권한 검증 인터셉터 │ -├── member/ # 회원 -│ ├── Member.java # 도메인/엔티티 -│ ├── MemberController.java -│ ├── MemberService.java -│ ├── MemberDao.java # JdbcTemplate 기반 DAO -│ ├── MemberRequest.java -│ └── MemberResponse.java +├── member/ # 회원 +│ ├── controller/ +│ │ └── MemberController.java +│ ├── service/ +│ │ └── MemberService.java +│ ├── domain/ +│ │ ├── Member.java # 도메인/엔티티 +│ │ └── Role.java # 권한 enum (ADMIN/USER, isAdmin()) +│ ├── dto/ +│ │ ├── MemberRequest.java +│ │ └── MemberResponse.java +│ ├── exception/ +│ │ └── MemberErrorCode.java +│ └── repository/ +│ └── MemberDao.java │ -├── reservation/ # 예약 (핵심 도메인) -│ ├── Reservation.java -│ ├── ReservationController.java -│ ├── ReservationService.java -│ ├── ReservationDao.java -│ ├── ReservationRequest.java -│ └── ReservationResponse.java +├── reservation/ # 예약 (핵심 도메인) +│ ├── controller/ +│ │ └── ReservationController.java +│ ├── service/ +│ │ └── ReservationService.java +│ ├── domain/ +│ │ └── Reservation.java +│ ├── dto/ +│ │ ├── ReservationRequest.java +│ │ └── ReservationResponse.java +│ └── repository/ +│ └── ReservationDao.java │ -├── theme/ # 테마 -│ ├── Theme.java -│ ├── ThemeController.java -│ └── ThemeDao.java # ※ Service 없음 +├── theme/ # 테마 +│ ├── controller/ +│ │ └── ThemeController.java +│ ├── domain/ +│ │ └── Theme.java +│ └── repository/ +│ └── ThemeDao.java │ -├── time/ # 예약 시간 -│ ├── Time.java -│ ├── AvailableTime.java # 예약 가능 시간 표현 -│ ├── TimeController.java -│ ├── TimeService.java -│ └── TimeDao.java +├── time/ # 예약 시간 +│ ├── controller/ +│ │ └── TimeController.java +│ ├── service/ +│ │ └── TimeService.java +│ ├── domain/ +│ │ └── Time.java +│ ├── dto/ +│ │ └── AvailableTime.java # 예약 가능 시간 응답 모델 +│ └── repository/ +│ └── TimeDao.java │ ├── config/ -│ └── WebConfig.java # ArgumentResolver 등록 +│ └── WebConfig.java # ArgumentResolver + Interceptor 등록 │ -├── exception/ # 전역 예외 처리 -│ ├── GlobalExceptionHandler.java # @RestControllerAdvice +├── exception/ # 전역 예외 처리 (공통) +│ ├── GlobalExceptionHandler.java # @RestControllerAdvice │ ├── ApplicationException.java -│ ├── ErrorCode.java # 인터페이스 (auth의 AuthErrorCode가 구현) +│ ├── ErrorCode.java # 인터페이스 (AuthErrorCode/MemberErrorCode가 구현) │ └── ErrorResponse.java │ └── util/ -└── CookieUtil.java # 쿠키 추출/생성 헬퍼 - + └── CookieUtil.java # 쿠키 추출/생성 헬퍼 ``` \ No newline at end of file From 5bc54ca2f916191cbc825bd975453f5b6f588d5f Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Wed, 8 Jul 2026 21:18:41 +0900 Subject: [PATCH 29/83] =?UTF-8?q?fix:=20=ED=9A=8C=EC=9B=90=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EC=8B=9C=20Role=EC=9D=B4=20ADMIN=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=EA=B3=A0=EC=A0=95=EB=90=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/roomescape/member/repository/MemberDao.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/roomescape/member/repository/MemberDao.java b/src/main/java/roomescape/member/repository/MemberDao.java index 59c5842fe..3636cd379 100644 --- a/src/main/java/roomescape/member/repository/MemberDao.java +++ b/src/main/java/roomescape/member/repository/MemberDao.java @@ -29,7 +29,7 @@ public Member save(Member member) { return ps; }, keyHolder); - return new Member(keyHolder.getKey().longValue(), member.getName(), member.getEmail(), Role.ADMIN); + return new Member(keyHolder.getKey().longValue(), member.getName(), member.getEmail(), member.getRole()); } public Optional findById(Long id) { From 6eb6095556a9b79f2928f21f32dd0467cf1e6066 Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Wed, 8 Jul 2026 21:22:32 +0900 Subject: [PATCH 30/83] =?UTF-8?q?refactor:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=EC=8B=A4=ED=8C=A8=20=EC=8B=9C=20EmptyResultDataAccessExcept?= =?UTF-8?q?ion=20=EB=8C=80=EC=8B=A0=20401=20=EC=9D=91=EB=8B=B5=20=EB=B0=98?= =?UTF-8?q?=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/roomescape/auth/service/AuthService.java | 5 ++++- .../java/roomescape/member/exception/MemberErrorCode.java | 3 ++- src/main/java/roomescape/member/repository/MemberDao.java | 6 +++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/java/roomescape/auth/service/AuthService.java b/src/main/java/roomescape/auth/service/AuthService.java index 285604f7c..6faf77b57 100644 --- a/src/main/java/roomescape/auth/service/AuthService.java +++ b/src/main/java/roomescape/auth/service/AuthService.java @@ -4,7 +4,9 @@ import roomescape.auth.domain.LoginMember; import roomescape.auth.dto.LoginRequest; import roomescape.auth.jwt.JwtTokenProvider; +import roomescape.exception.ApplicationException; import roomescape.member.domain.Member; +import roomescape.member.exception.MemberErrorCode; import roomescape.member.repository.MemberDao; @Service @@ -19,7 +21,8 @@ public AuthService(MemberDao memberDao, JwtTokenProvider tokenProvider) { } public String login(LoginRequest loginRequest) { - Member member = memberDao.findByEmailAndPassword(loginRequest.email(), loginRequest.password()); + Member member = memberDao.findByEmailAndPassword(loginRequest.email(), loginRequest.password()) + .orElseThrow(() -> new ApplicationException(MemberErrorCode.LOGIN_FAILED)); return tokenProvider.createAccessToken(member); } diff --git a/src/main/java/roomescape/member/exception/MemberErrorCode.java b/src/main/java/roomescape/member/exception/MemberErrorCode.java index d6c663751..d476aa0e5 100644 --- a/src/main/java/roomescape/member/exception/MemberErrorCode.java +++ b/src/main/java/roomescape/member/exception/MemberErrorCode.java @@ -4,7 +4,8 @@ import roomescape.exception.ErrorCode; public enum MemberErrorCode implements ErrorCode { - MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 회원입니다."); + MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 회원입니다."), + LOGIN_FAILED(HttpStatus.UNAUTHORIZED, "email 또는 비밀번호가 틀렸습니다."); private final HttpStatus httpStatus; private final String message; diff --git a/src/main/java/roomescape/member/repository/MemberDao.java b/src/main/java/roomescape/member/repository/MemberDao.java index 3636cd379..e6fe4cd23 100644 --- a/src/main/java/roomescape/member/repository/MemberDao.java +++ b/src/main/java/roomescape/member/repository/MemberDao.java @@ -40,12 +40,12 @@ public Optional findById(Long id) { ).stream().findFirst(); } - public Member findByEmailAndPassword(String email, String password) { - return jdbcTemplate.queryForObject( + public Optional findByEmailAndPassword(String email, String password) { + return jdbcTemplate.query( "SELECT id, name, email, role FROM member WHERE email = ? AND password = ?", rowMapper(), email, password - ); + ).stream().findFirst(); } public Optional findByName(String name) { From b400cf242452c4e37179c2e79d2d7226cca802f1 Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Wed, 8 Jul 2026 21:23:29 +0900 Subject: [PATCH 31/83] =?UTF-8?q?refactor:=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=B6=94=EC=B6=9C=20=EA=B3=B5=ED=86=B5=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=EC=9D=84=20TokenExtractor=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/web/CheckAdminInterceptor.java | 18 +++++---------- .../auth/web/LoginMemberArgumentResolver.java | 20 +++++------------ .../roomescape/auth/web/TokenExtractor.java | 22 +++++++++++++++++++ src/main/java/roomescape/util/CookieUtil.java | 12 ++++++---- 4 files changed, 42 insertions(+), 30 deletions(-) create mode 100644 src/main/java/roomescape/auth/web/TokenExtractor.java diff --git a/src/main/java/roomescape/auth/web/CheckAdminInterceptor.java b/src/main/java/roomescape/auth/web/CheckAdminInterceptor.java index ac11e83c7..e954d431b 100644 --- a/src/main/java/roomescape/auth/web/CheckAdminInterceptor.java +++ b/src/main/java/roomescape/auth/web/CheckAdminInterceptor.java @@ -1,6 +1,5 @@ package roomescape.auth.web; -import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.stereotype.Component; @@ -9,28 +8,23 @@ import roomescape.auth.exception.AuthErrorCode; import roomescape.auth.service.AuthService; import roomescape.exception.ApplicationException; -import roomescape.util.CookieUtil; @Component public class CheckAdminInterceptor implements HandlerInterceptor { - private final CookieUtil cookieUtil; + private final TokenExtractor tokenExtractor; private final AuthService authService; - public CheckAdminInterceptor(CookieUtil cookieUtil, AuthService authService) { - this.cookieUtil = cookieUtil; + public CheckAdminInterceptor(TokenExtractor tokenExtractor, AuthService authService) { + this.tokenExtractor = tokenExtractor; this.authService = authService; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - Cookie[] cookies = request.getCookies(); - if (cookies == null) { - throw new ApplicationException(AuthErrorCode.UNAUTHENTICATED_ACCESS); - } - - String token = cookieUtil.extractToken(cookies) - .orElseThrow(() -> new ApplicationException(AuthErrorCode.UNAUTHENTICATED_ACCESS)); + String token = tokenExtractor.extractToken( + request.getCookies() + ); LoginMember member = authService.findAuthenticatedMember(token); if (member == null || !member.isAdmin()) { diff --git a/src/main/java/roomescape/auth/web/LoginMemberArgumentResolver.java b/src/main/java/roomescape/auth/web/LoginMemberArgumentResolver.java index 00ed4d25e..aa3039a3f 100644 --- a/src/main/java/roomescape/auth/web/LoginMemberArgumentResolver.java +++ b/src/main/java/roomescape/auth/web/LoginMemberArgumentResolver.java @@ -1,6 +1,5 @@ package roomescape.auth.web; -import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import org.springframework.core.MethodParameter; import org.springframework.stereotype.Component; @@ -9,20 +8,17 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; import roomescape.auth.domain.LoginMember; -import roomescape.auth.exception.AuthErrorCode; import roomescape.auth.service.AuthService; -import roomescape.exception.ApplicationException; -import roomescape.util.CookieUtil; @Component public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver { private final AuthService authService; - private final CookieUtil cookieUtil; + private final TokenExtractor tokenExtractor; - public LoginMemberArgumentResolver(AuthService authService, CookieUtil cookieUtil) { + public LoginMemberArgumentResolver(AuthService authService, TokenExtractor tokenExtractor) { this.authService = authService; - this.cookieUtil = cookieUtil; + this.tokenExtractor = tokenExtractor; } @Override @@ -38,13 +34,9 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); - Cookie[] cookies = request.getCookies(); - if (cookies == null) { - throw new ApplicationException(AuthErrorCode.UNAUTHENTICATED_ACCESS); - } - - String token = cookieUtil.extractToken(cookies) - .orElseThrow(() -> new ApplicationException(AuthErrorCode.UNAUTHENTICATED_ACCESS)); + String token = tokenExtractor.extractToken( + request.getCookies() + ); return authService.findAuthenticatedMember(token); } diff --git a/src/main/java/roomescape/auth/web/TokenExtractor.java b/src/main/java/roomescape/auth/web/TokenExtractor.java new file mode 100644 index 000000000..e87ba3dbe --- /dev/null +++ b/src/main/java/roomescape/auth/web/TokenExtractor.java @@ -0,0 +1,22 @@ +package roomescape.auth.web; + +import jakarta.servlet.http.Cookie; +import org.springframework.stereotype.Component; +import roomescape.auth.exception.AuthErrorCode; +import roomescape.exception.ApplicationException; +import roomescape.util.CookieUtil; + +@Component +public class TokenExtractor { + + private final CookieUtil cookieUtil; + + public TokenExtractor(CookieUtil cookieUtil) { + this.cookieUtil = cookieUtil; + } + + public String extractToken(Cookie[] cookies) { + return cookieUtil.extractToken(cookies) + .orElseThrow(() -> new ApplicationException(AuthErrorCode.UNAUTHENTICATED_ACCESS)); + } +} diff --git a/src/main/java/roomescape/util/CookieUtil.java b/src/main/java/roomescape/util/CookieUtil.java index 7b17ab7b5..efe3d263e 100644 --- a/src/main/java/roomescape/util/CookieUtil.java +++ b/src/main/java/roomescape/util/CookieUtil.java @@ -10,15 +10,17 @@ @Component public class CookieUtil { + private static final String TOKEN = "token"; + public void setCookie(HttpServletResponse response, String accessToken) { - Cookie cookie = new Cookie("token", accessToken); + Cookie cookie = new Cookie(TOKEN, accessToken); cookie.setHttpOnly(true); cookie.setPath("/"); response.addCookie(cookie); } public void expireCookie(HttpServletResponse response) { - Cookie cookie = new Cookie("token", ""); + Cookie cookie = new Cookie(TOKEN, ""); cookie.setHttpOnly(true); cookie.setPath("/"); cookie.setMaxAge(0); @@ -26,8 +28,10 @@ public void expireCookie(HttpServletResponse response) { } public Optional extractToken(Cookie[] cookies) { - return Arrays.stream(cookies) - .filter(cookie -> cookie.getName().equals("token")) + Cookie[] safeCookies = Optional.ofNullable(cookies).orElse(new Cookie[0]); + + return Arrays.stream(safeCookies) + .filter(cookie -> cookie.getName().equals(TOKEN)) .map(Cookie::getValue) .findFirst(); } From 753f82ab70e6e317b12571882c6a3597479a3253 Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Thu, 9 Jul 2026 16:44:00 +0900 Subject: [PATCH 32/83] =?UTF-8?q?feat:=20refresh=20token=20=EB=B0=9C?= =?UTF-8?q?=EA=B8=89=C2=B7=EC=A0=80=EC=9E=A5=20=ED=99=98=EA=B2=BD=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roomescape/auth/domain/RefreshToken.java | 29 ++++++++++ .../roomescape/auth/jwt/JwtTokenProperty.java | 3 +- .../roomescape/auth/jwt/JwtTokenProvider.java | 22 ++++++-- .../auth/repository/RefreshTokenDao.java | 54 +++++++++++++++++++ src/main/resources/application.properties | 3 +- src/main/resources/schema.sql | 7 +++ 6 files changed, 112 insertions(+), 6 deletions(-) create mode 100644 src/main/java/roomescape/auth/domain/RefreshToken.java create mode 100644 src/main/java/roomescape/auth/repository/RefreshTokenDao.java diff --git a/src/main/java/roomescape/auth/domain/RefreshToken.java b/src/main/java/roomescape/auth/domain/RefreshToken.java new file mode 100644 index 000000000..ee1377673 --- /dev/null +++ b/src/main/java/roomescape/auth/domain/RefreshToken.java @@ -0,0 +1,29 @@ +package roomescape.auth.domain; + +public class RefreshToken { + private Long id; + private Long memberId; + private String token; + + public RefreshToken(Long id, Long memberId, String token) { + this.id = id; + this.memberId = memberId; + this.token = token; + } + + public RefreshToken(Long memberId, String token) { + this(null, memberId, token); + } + + public Long getId() { + return id; + } + + public Long getMemberId() { + return memberId; + } + + public String getToken() { + return token; + } +} diff --git a/src/main/java/roomescape/auth/jwt/JwtTokenProperty.java b/src/main/java/roomescape/auth/jwt/JwtTokenProperty.java index 1336fe9ce..59cf4bb19 100644 --- a/src/main/java/roomescape/auth/jwt/JwtTokenProperty.java +++ b/src/main/java/roomescape/auth/jwt/JwtTokenProperty.java @@ -5,6 +5,7 @@ @ConfigurationProperties("roomescape.auth.jwt") public record JwtTokenProperty( String secretKey, - long expiration + long accessTokenExpiration, + long refreshTokenExpiration ) { } diff --git a/src/main/java/roomescape/auth/jwt/JwtTokenProvider.java b/src/main/java/roomescape/auth/jwt/JwtTokenProvider.java index 7477e5429..a2a0d6609 100644 --- a/src/main/java/roomescape/auth/jwt/JwtTokenProvider.java +++ b/src/main/java/roomescape/auth/jwt/JwtTokenProvider.java @@ -20,17 +20,19 @@ public class JwtTokenProvider { private final Key secretKey; - private final long expiration; + private final long accessTokenExpiration; + private final long refreshTokenExpiration; public JwtTokenProvider(JwtTokenProperty tokenProperty) { this.secretKey = Keys.hmacShaKeyFor( Base64.getDecoder().decode(tokenProperty.secretKey())); - this.expiration = tokenProperty.expiration(); + this.accessTokenExpiration = tokenProperty.accessTokenExpiration(); + this.refreshTokenExpiration = tokenProperty.refreshTokenExpiration(); } public String createAccessToken(Member member) { Date now = new Date(); - Date expiredTime = new Date(now.getTime() + expiration); + Date expiredTime = new Date(now.getTime() + accessTokenExpiration); return Jwts.builder() .setSubject(member.getId().toString()) @@ -42,6 +44,18 @@ public String createAccessToken(Member member) { .compact(); } + public String createRefreshToken(Member member) { + Date now = new Date(); + Date expiredTime = new Date(now.getTime() + refreshTokenExpiration); + + return Jwts.builder() + .setSubject(member.getId().toString()) + .setIssuedAt(now) + .setExpiration(expiredTime) + .signWith(secretKey) + .compact(); + } + public Long getLoginMemberId(String token) { return Long.valueOf( parseClaims(token).getSubject() @@ -64,7 +78,7 @@ private Claims parseClaims(String token) { .parseClaimsJws(token) .getBody(); } catch (SignatureException | MalformedJwtException | UnsupportedJwtException e) { - throw new ApplicationException(AuthErrorCode.INVALID_ACCESS_TOKEN); + throw new ApplicationException(AuthErrorCode.INVALID_TOKEN); } catch (ExpiredJwtException e) { throw new ApplicationException(AuthErrorCode.ACCESS_TOKEN_EXPIRED); } catch (IllegalArgumentException e) { diff --git a/src/main/java/roomescape/auth/repository/RefreshTokenDao.java b/src/main/java/roomescape/auth/repository/RefreshTokenDao.java new file mode 100644 index 000000000..f1e59fc47 --- /dev/null +++ b/src/main/java/roomescape/auth/repository/RefreshTokenDao.java @@ -0,0 +1,54 @@ +package roomescape.auth.repository; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; +import roomescape.auth.domain.RefreshToken; + +import java.util.Optional; + +@Repository +public class RefreshTokenDao { + + private JdbcTemplate jdbcTemplate; + + public RefreshTokenDao(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public void save(RefreshToken refreshToken) { + jdbcTemplate.update( + "INSERT INTO refresh_token(member_id, token) VALUES (?, ?)", + refreshToken.getMemberId(), + refreshToken.getToken() + ); + } + + public Optional findByToken(String token) { + return jdbcTemplate.query( + "SELECT id, member_id, token FROM refresh_token WHERE token = ?", + rowMapper(), + token + ).stream().findFirst(); + } + + public Optional findByMemberId(Long memberId) { + return jdbcTemplate.query( + "SELECT id, member_id, token FROM refresh_token WHERE member_id = ?", + rowMapper(), + memberId + ).stream().findFirst(); + } + + public void deleteByMemberId(Long memberId) { + jdbcTemplate.update("DELETE FROM refresh_token WHERE member_id = ?", memberId); + } + + private RowMapper rowMapper() { + return (resultSet, rowNum) -> new RefreshToken( + resultSet.getLong("id"), + resultSet.getLong("member_id"), + resultSet.getString("token") + ); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index b5b755bcc..73c36e3c7 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -9,4 +9,5 @@ spring.datasource.url=jdbc:h2:mem:database #spring.jpa.defer-datasource-initialization=true roomescape.auth.jwt.secret-key=Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E= -roomescape.auth.jwt.expiration=900000 +roomescape.auth.jwt.access-token-expiration=900000 +roomescape.auth.jwt.refresh-token-expiration=1209600000 diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 75c947a5a..8d2ea4e10 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -37,6 +37,13 @@ CREATE TABLE reservation FOREIGN KEY (theme_id) REFERENCES theme (id) ); +CREATE TABLE refresh_token ( + id BIGINT NOT NULL AUTO_INCREMENT, + member_id BIGINT NOT NULL, + token VARCHAR(500) NOT NULL, + PRIMARY KEY (id) +); + INSERT INTO member (name, email, password, role) VALUES ('어드민', 'admin@email.com', 'password', 'ADMIN'), ('브라운', 'brown@email.com', 'password', 'USER'); From dded03255817fe956032986bc4e45268a4813e6b Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Thu, 9 Jul 2026 16:44:45 +0900 Subject: [PATCH 33/83] =?UTF-8?q?refactor:=20CookieUtil=EC=9D=84=20access/?= =?UTF-8?q?refresh=20=EC=BF=A0=ED=82=A4=20=EC=A7=80=EC=9B=90=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=EB=A1=9C=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/web/CheckAdminInterceptor.java | 2 +- .../auth/web/LoginMemberArgumentResolver.java | 2 +- .../roomescape/auth/web/TokenExtractor.java | 9 +++- src/main/java/roomescape/util/CookieUtil.java | 53 +++++++++++++++---- 4 files changed, 52 insertions(+), 14 deletions(-) diff --git a/src/main/java/roomescape/auth/web/CheckAdminInterceptor.java b/src/main/java/roomescape/auth/web/CheckAdminInterceptor.java index e954d431b..931778af1 100644 --- a/src/main/java/roomescape/auth/web/CheckAdminInterceptor.java +++ b/src/main/java/roomescape/auth/web/CheckAdminInterceptor.java @@ -22,7 +22,7 @@ public CheckAdminInterceptor(TokenExtractor tokenExtractor, AuthService authServ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - String token = tokenExtractor.extractToken( + String token = tokenExtractor.extractAccessToken( request.getCookies() ); diff --git a/src/main/java/roomescape/auth/web/LoginMemberArgumentResolver.java b/src/main/java/roomescape/auth/web/LoginMemberArgumentResolver.java index aa3039a3f..b134bfd7e 100644 --- a/src/main/java/roomescape/auth/web/LoginMemberArgumentResolver.java +++ b/src/main/java/roomescape/auth/web/LoginMemberArgumentResolver.java @@ -34,7 +34,7 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); - String token = tokenExtractor.extractToken( + String token = tokenExtractor.extractAccessToken( request.getCookies() ); diff --git a/src/main/java/roomescape/auth/web/TokenExtractor.java b/src/main/java/roomescape/auth/web/TokenExtractor.java index e87ba3dbe..234c48223 100644 --- a/src/main/java/roomescape/auth/web/TokenExtractor.java +++ b/src/main/java/roomescape/auth/web/TokenExtractor.java @@ -15,8 +15,13 @@ public TokenExtractor(CookieUtil cookieUtil) { this.cookieUtil = cookieUtil; } - public String extractToken(Cookie[] cookies) { - return cookieUtil.extractToken(cookies) + public String extractAccessToken(Cookie[] cookies) { + return cookieUtil.extractAccessToken(cookies) + .orElseThrow(() -> new ApplicationException(AuthErrorCode.UNAUTHENTICATED_ACCESS)); + } + + public String extractRefreshToken(Cookie[] cookies) { + return cookieUtil.extractRefreshToken(cookies) .orElseThrow(() -> new ApplicationException(AuthErrorCode.UNAUTHENTICATED_ACCESS)); } } diff --git a/src/main/java/roomescape/util/CookieUtil.java b/src/main/java/roomescape/util/CookieUtil.java index efe3d263e..89ee6c65f 100644 --- a/src/main/java/roomescape/util/CookieUtil.java +++ b/src/main/java/roomescape/util/CookieUtil.java @@ -10,28 +10,61 @@ @Component public class CookieUtil { - private static final String TOKEN = "token"; + private static final String ACCESS_TOKEN = "access-token"; + private static final String REFRESH_TOKEN = "refresh-token"; + private static final String ACCESS_TOKEN_PATH = "/"; + private static final String REFRESH_TOKEN_PATH = "/login/refresh"; - public void setCookie(HttpServletResponse response, String accessToken) { - Cookie cookie = new Cookie(TOKEN, accessToken); + + public void setAccessTokenCookie(HttpServletResponse response, String accessToken) { + response.addCookie( + createCookie(ACCESS_TOKEN, accessToken, ACCESS_TOKEN_PATH) + ); + } + + public void setRefreshTokenCookie(HttpServletResponse response, String refreshToken) { + response.addCookie( + createCookie(REFRESH_TOKEN, refreshToken, REFRESH_TOKEN_PATH) + ); + } + + public void expireAccessTokenCookie(HttpServletResponse response) { + response.addCookie(expireCookie(ACCESS_TOKEN)); + } + + public void expireRefreshTokenCookie(HttpServletResponse response) { + response.addCookie(expireCookie(REFRESH_TOKEN)); + } + + public Optional extractAccessToken(Cookie[] cookies) { + return extractToken(cookies, ACCESS_TOKEN); + } + + public Optional extractRefreshToken(Cookie[] cookies) { + return extractToken(cookies, REFRESH_TOKEN); + } + + private Cookie createCookie(String name, String value, String path) { + Cookie cookie = new Cookie(name, value); cookie.setHttpOnly(true); - cookie.setPath("/"); - response.addCookie(cookie); + cookie.setPath(path); + return cookie; } - public void expireCookie(HttpServletResponse response) { - Cookie cookie = new Cookie(TOKEN, ""); + private Cookie expireCookie(String name) { + Cookie cookie = new Cookie(name, ""); cookie.setHttpOnly(true); cookie.setPath("/"); cookie.setMaxAge(0); - response.addCookie(cookie); + + return cookie; } - public Optional extractToken(Cookie[] cookies) { + private Optional extractToken(Cookie[] cookies, String name) { Cookie[] safeCookies = Optional.ofNullable(cookies).orElse(new Cookie[0]); return Arrays.stream(safeCookies) - .filter(cookie -> cookie.getName().equals(TOKEN)) + .filter(cookie -> cookie.getName().equals(name)) .map(Cookie::getValue) .findFirst(); } From 5e0c8a7fb61bae76d7f47dde8cdf81fd8fd8f888 Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Thu, 9 Jul 2026 16:45:50 +0900 Subject: [PATCH 34/83] =?UTF-8?q?feat:=20refresh=20token=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=EC=9E=AC=EB=B0=9C=EA=B8=89=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 29 ++++++++++++++--- .../roomescape/auth/dto/TokenResponse.java | 7 ++++ .../auth/exception/AuthErrorCode.java | 2 +- .../roomescape/auth/service/AuthService.java | 32 +++++++++++++++++-- 4 files changed, 62 insertions(+), 8 deletions(-) create mode 100644 src/main/java/roomescape/auth/dto/TokenResponse.java diff --git a/src/main/java/roomescape/auth/controller/AuthController.java b/src/main/java/roomescape/auth/controller/AuthController.java index bbde68d14..11882261a 100644 --- a/src/main/java/roomescape/auth/controller/AuthController.java +++ b/src/main/java/roomescape/auth/controller/AuthController.java @@ -1,5 +1,6 @@ package roomescape.auth.controller; +import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; @@ -10,8 +11,10 @@ import roomescape.auth.domain.LoginMember; import roomescape.auth.dto.LoginCheckResponse; import roomescape.auth.dto.LoginRequest; +import roomescape.auth.dto.TokenResponse; import roomescape.auth.service.AuthService; import roomescape.auth.web.Login; +import roomescape.auth.web.TokenExtractor; import roomescape.util.CookieUtil; @RestController @@ -19,17 +22,31 @@ public class AuthController { private final AuthService authService; private final CookieUtil cookieUtil; + private final TokenExtractor tokenExtractor; - public AuthController(AuthService authService, CookieUtil cookieUtil) { + public AuthController(AuthService authService, CookieUtil cookieUtil, TokenExtractor tokenExtractor) { this.authService = authService; this.cookieUtil = cookieUtil; + this.tokenExtractor = tokenExtractor; } @PostMapping("/login") public ResponseEntity login(@Valid @RequestBody LoginRequest loginRequest, HttpServletResponse response) { - String token = authService.login(loginRequest); - cookieUtil.setCookie(response, token); + TokenResponse tokenResponse = authService.login(loginRequest); + cookieUtil.setAccessTokenCookie(response, tokenResponse.accessToken()); + cookieUtil.setRefreshTokenCookie(response, tokenResponse.refreshToken()); + return ResponseEntity.ok().build(); + } + + @PostMapping("/login/refresh") + public ResponseEntity loginByRefreshToken(HttpServletRequest request, + HttpServletResponse response) { + String refreshToken = tokenExtractor.extractRefreshToken(request.getCookies()); + String newAccessToken = authService.reissue(refreshToken); + + cookieUtil.setAccessTokenCookie(response, newAccessToken); + return ResponseEntity.ok().build(); } @@ -39,8 +56,10 @@ public ResponseEntity checkLogin(@Login LoginMember loginMem } @PostMapping("/logout") - public ResponseEntity logout(HttpServletResponse response) { - cookieUtil.expireCookie(response); + public ResponseEntity logout(@Login LoginMember loginMember, HttpServletResponse response) { + authService.logout(loginMember.id()); + cookieUtil.expireAccessTokenCookie(response); + cookieUtil.expireRefreshTokenCookie(response); return ResponseEntity.ok().build(); } } diff --git a/src/main/java/roomescape/auth/dto/TokenResponse.java b/src/main/java/roomescape/auth/dto/TokenResponse.java new file mode 100644 index 000000000..57a73e871 --- /dev/null +++ b/src/main/java/roomescape/auth/dto/TokenResponse.java @@ -0,0 +1,7 @@ +package roomescape.auth.dto; + +public record TokenResponse( + String accessToken, + String refreshToken +) { +} diff --git a/src/main/java/roomescape/auth/exception/AuthErrorCode.java b/src/main/java/roomescape/auth/exception/AuthErrorCode.java index 597de9bca..1aa8bd3b9 100644 --- a/src/main/java/roomescape/auth/exception/AuthErrorCode.java +++ b/src/main/java/roomescape/auth/exception/AuthErrorCode.java @@ -5,7 +5,7 @@ public enum AuthErrorCode implements ErrorCode { ACCESS_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "토큰이 만료되었습니다."), - INVALID_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰입니다."), + INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰입니다."), UNAUTHENTICATED_ACCESS(HttpStatus.UNAUTHORIZED, "인증되지 않은 요청입니다."), UNAUTHORIZED_REQUEST(HttpStatus.FORBIDDEN, "권한이 없습니다."); diff --git a/src/main/java/roomescape/auth/service/AuthService.java b/src/main/java/roomescape/auth/service/AuthService.java index 6faf77b57..19538d502 100644 --- a/src/main/java/roomescape/auth/service/AuthService.java +++ b/src/main/java/roomescape/auth/service/AuthService.java @@ -2,8 +2,12 @@ import org.springframework.stereotype.Service; import roomescape.auth.domain.LoginMember; +import roomescape.auth.domain.RefreshToken; import roomescape.auth.dto.LoginRequest; +import roomescape.auth.dto.TokenResponse; +import roomescape.auth.exception.AuthErrorCode; import roomescape.auth.jwt.JwtTokenProvider; +import roomescape.auth.repository.RefreshTokenDao; import roomescape.exception.ApplicationException; import roomescape.member.domain.Member; import roomescape.member.exception.MemberErrorCode; @@ -13,17 +17,41 @@ public class AuthService { private final MemberDao memberDao; + private final RefreshTokenDao refreshTokenDao; private final JwtTokenProvider tokenProvider; - public AuthService(MemberDao memberDao, JwtTokenProvider tokenProvider) { + public AuthService(MemberDao memberDao, RefreshTokenDao refreshTokenDao, JwtTokenProvider tokenProvider) { this.memberDao = memberDao; + this.refreshTokenDao = refreshTokenDao; this.tokenProvider = tokenProvider; } - public String login(LoginRequest loginRequest) { + public TokenResponse login(LoginRequest loginRequest) { Member member = memberDao.findByEmailAndPassword(loginRequest.email(), loginRequest.password()) .orElseThrow(() -> new ApplicationException(MemberErrorCode.LOGIN_FAILED)); + + String accessToken = tokenProvider.createAccessToken(member); + String refreshToken = tokenProvider.createRefreshToken(member); + refreshTokenDao.save(new RefreshToken(member.getId(), refreshToken)); + + return new TokenResponse(accessToken, refreshToken); + } + + public String reissue(String refreshToken) { + refreshTokenDao.findByToken(refreshToken) + .orElseThrow(() -> new ApplicationException(AuthErrorCode.INVALID_TOKEN)); + + Long memberId = tokenProvider.getLoginMemberId(refreshToken); + + Member member = memberDao.findById(memberId) + .orElseThrow(() -> new ApplicationException(MemberErrorCode.MEMBER_NOT_FOUND)); + return tokenProvider.createAccessToken(member); + + } + + public void logout(Long memberId) { + refreshTokenDao.deleteByMemberId(memberId); } public LoginMember findAuthenticatedMember(String token) { From ccb028c147e4c97813dd4dd1df61da458b0e706f Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Thu, 9 Jul 2026 16:46:41 +0900 Subject: [PATCH 35/83] =?UTF-8?q?test:=20createToken=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=EA=B0=80=20access=20token=EC=9D=84=20=EC=B6=94?= =?UTF-8?q?=EC=B6=9C=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/roomescape/MissionStepTest.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/test/java/roomescape/MissionStepTest.java b/src/test/java/roomescape/MissionStepTest.java index e3016bfe7..d13699d2d 100644 --- a/src/test/java/roomescape/MissionStepTest.java +++ b/src/test/java/roomescape/MissionStepTest.java @@ -18,6 +18,8 @@ @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) public class MissionStepTest { + private static String ACCESS_TOKEN = "access-token"; + @Test void 일단계() { Map params = new HashMap<>(); @@ -48,7 +50,7 @@ public class MissionStepTest { ExtractableResponse response = RestAssured.given().log().all() .body(params) - .cookie("token", token) + .cookie(ACCESS_TOKEN, token) .contentType(ContentType.JSON) .post("/reservations") .then().log().all() @@ -61,7 +63,7 @@ public class MissionStepTest { ExtractableResponse adminResponse = RestAssured.given().log().all() .body(params) - .cookie("token", token) + .cookie(ACCESS_TOKEN, token) .contentType(ContentType.JSON) .post("/reservations") .then().log().all() @@ -76,7 +78,7 @@ public class MissionStepTest { String brownToken = createToken("brown@email.com", "password"); RestAssured.given().log().all() - .cookie("token", brownToken) + .cookie(ACCESS_TOKEN, brownToken) .get("/admin") .then().log().all() .statusCode(403); @@ -84,7 +86,7 @@ public class MissionStepTest { String adminToken = createToken("admin@email.com", "password"); RestAssured.given().log().all() - .cookie("token", adminToken) + .cookie(ACCESS_TOKEN, adminToken) .get("/admin") .then().log().all() .statusCode(200); @@ -101,6 +103,6 @@ private String createToken(String email, String password) { .when().post("/login") .then().extract(); - return response.headers().get("Set-Cookie").getValue().split(";")[0].split("=")[1]; + return response.cookie(ACCESS_TOKEN); } } From e1b65c3a4c77fba7c2d24bab2ad61f041c1bb3b7 Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Sun, 12 Jul 2026 10:58:12 +0900 Subject: [PATCH 36/83] =?UTF-8?q?build:=20Spring=20Data=20JPA=20=EB=8F=84?= =?UTF-8?q?=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 +- src/main/resources/application.properties | 8 ++-- src/main/resources/schema.sql | 53 ++++------------------- 3 files changed, 14 insertions(+), 49 deletions(-) diff --git a/build.gradle b/build.gradle index 1d884b864..375e97132 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,7 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' - implementation 'org.springframework.boot:spring-boot-starter-jdbc' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'dev.akkinoc.spring.boot:logback-access-spring-boot-starter:4.0.0' diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 73c36e3c7..27881b372 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -3,10 +3,10 @@ spring.h2.console.enabled=true spring.h2.console.path=/h2-console spring.datasource.url=jdbc:h2:mem:database -#spring.jpa.show-sql=true -#spring.jpa.properties.hibernate.format_sql=true -#spring.jpa.ddl-auto=create-drop -#spring.jpa.defer-datasource-initialization=true +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true +spring.jpa.ddl-auto=create-drop +spring.jpa.defer-datasource-initialization=true roomescape.auth.jwt.secret-key=Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E= roomescape.auth.jwt.access-token-expiration=900000 diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 8d2ea4e10..8536b0f6e 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -1,47 +1,9 @@ -CREATE TABLE time -( - id BIGINT NOT NULL AUTO_INCREMENT, - time_value VARCHAR(20) NOT NULL, - deleted BOOLEAN NOT NULL DEFAULT FALSE, - PRIMARY KEY (id) -); - -CREATE TABLE theme -( - id BIGINT NOT NULL AUTO_INCREMENT, - name VARCHAR(255) NOT NULL, - description VARCHAR(255) NOT NULL, - deleted BOOLEAN NOT NULL DEFAULT FALSE, - PRIMARY KEY (id) -); - -CREATE TABLE member -( - id BIGINT NOT NULL AUTO_INCREMENT, - name VARCHAR(255) NOT NULL, - email VARCHAR(255) UNIQUE NOT NULL, - password VARCHAR(255) NOT NULL, - role VARCHAR(255) NOT NULL, - PRIMARY KEY (id) -); - -CREATE TABLE reservation -( - id BIGINT NOT NULL AUTO_INCREMENT, - date VARCHAR(255) NOT NULL, - name VARCHAR(255) NOT NULL, - time_id BIGINT, - theme_id BIGINT, - PRIMARY KEY (id), - FOREIGN KEY (time_id) REFERENCES time (id), - FOREIGN KEY (theme_id) REFERENCES theme (id) -); - CREATE TABLE refresh_token ( id BIGINT NOT NULL AUTO_INCREMENT, member_id BIGINT NOT NULL, token VARCHAR(500) NOT NULL, - PRIMARY KEY (id) + PRIMARY KEY (id), + FOREIGN KEY (member_id) REFERENCES member (id) ); INSERT INTO member (name, email, password, role) @@ -61,7 +23,10 @@ VALUES ('10:00'), ('18:00'), ('20:00'); -INSERT INTO reservation (name, date, time_id, theme_id) -VALUES ('어드민', '2024-03-01', 1, 1), - ('어드민', '2024-03-01', 2, 2), - ('어드민', '2024-03-01', 3, 3); \ No newline at end of file +INSERT INTO reservation (member_id, date, time_id, theme_id) +VALUES (1, '2024-03-01', 1, 1), + (1, '2024-03-01', 2, 2), + (1, '2024-03-01', 3, 3); + +INSERT INTO reservation (member_id, date, time_id, theme_id) +VALUES (2, '2024-03-01', 1, 2); \ No newline at end of file From ce5ad9a7933bd82268f921de881ef953d0af8848 Mon Sep 17 00:00:00 2001 From: htdufhc-bob Date: Sun, 12 Jul 2026 10:58:40 +0900 Subject: [PATCH 37/83] =?UTF-8?q?refactor:=20DAO=EB=A5=BC=20JPA=20Reposito?= =?UTF-8?q?ry=EB=A1=9C=20=EC=A0=84=ED=99=98=ED=95=98=EA=B3=A0=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0/DTO=20=EC=9E=AC=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roomescape/auth/domain/LoginMember.java | 2 +- .../roomescape/auth/jwt/JwtTokenProvider.java | 2 +- .../roomescape/auth/service/AuthService.java | 14 +- .../member/controller/MemberController.java | 2 +- .../member/{domain => entity}/Member.java | 27 +++- .../member/{domain => entity}/Role.java | 2 +- .../repository/JpaMemberRepository.java | 52 +++++++ .../member/repository/MemberDao.java | 67 --------- .../member/repository/MemberRepository.java | 16 +++ .../member/service/MemberService.java | 18 +-- .../controller/ReservationController.java | 10 +- .../reservation/domain/Reservation.java | 51 ------- .../dto/MyReservationResponse.java | 37 +++++ .../reservation/dto/ReservationResponse.java | 4 +- .../reservation/entity/Reservation.java | 71 ++++++++++ .../repository/JpaReservationRepository.java | 60 ++++++++ .../repository/ReservationDao.java | 129 ------------------ .../repository/ReservationRepository.java | 19 +++ .../service/ReservationService.java | 87 +++++++++--- .../theme/controller/ThemeController.java | 23 ++-- .../roomescape/theme/dto/ThemeRequest.java | 7 + .../roomescape/theme/dto/ThemeResponse.java | 8 ++ .../theme/{domain => entity}/Theme.java | 21 ++- .../theme/repository/JpaThemeRepository.java | 43 ++++++ .../roomescape/theme/repository/ThemeDao.java | 42 ------ .../theme/repository/ThemeRepository.java | 17 +++ .../theme/service/ThemeService.java | 47 +++++++ .../time/controller/TimeController.java | 12 +- .../java/roomescape/time/domain/Time.java | 27 ---- .../java/roomescape/time/dto/TimeRequest.java | 14 ++ .../roomescape/time/dto/TimeResponse.java | 20 +++ .../java/roomescape/time/entity/Time.java | 42 ++++++ .../time/repository/JpaTimeRepository.java | 43 ++++++ .../roomescape/time/repository/TimeDao.java | 42 ------ .../time/repository/TimeRepository.java | 17 +++ .../roomescape/time/service/TimeService.java | 46 ++++--- src/test/java/roomescape/JpaTest.java | 33 +++++ src/test/java/roomescape/MissionStepTest.java | 16 +++ 38 files changed, 753 insertions(+), 437 deletions(-) rename src/main/java/roomescape/member/{domain => entity}/Member.java (53%) rename src/main/java/roomescape/member/{domain => entity}/Role.java (76%) create mode 100644 src/main/java/roomescape/member/repository/JpaMemberRepository.java delete mode 100644 src/main/java/roomescape/member/repository/MemberDao.java create mode 100644 src/main/java/roomescape/member/repository/MemberRepository.java delete mode 100644 src/main/java/roomescape/reservation/domain/Reservation.java create mode 100644 src/main/java/roomescape/reservation/dto/MyReservationResponse.java create mode 100644 src/main/java/roomescape/reservation/entity/Reservation.java create mode 100644 src/main/java/roomescape/reservation/repository/JpaReservationRepository.java delete mode 100644 src/main/java/roomescape/reservation/repository/ReservationDao.java create mode 100644 src/main/java/roomescape/reservation/repository/ReservationRepository.java create mode 100644 src/main/java/roomescape/theme/dto/ThemeRequest.java create mode 100644 src/main/java/roomescape/theme/dto/ThemeResponse.java rename src/main/java/roomescape/theme/{domain => entity}/Theme.java (53%) create mode 100644 src/main/java/roomescape/theme/repository/JpaThemeRepository.java delete mode 100644 src/main/java/roomescape/theme/repository/ThemeDao.java create mode 100644 src/main/java/roomescape/theme/repository/ThemeRepository.java create mode 100644 src/main/java/roomescape/theme/service/ThemeService.java delete mode 100644 src/main/java/roomescape/time/domain/Time.java create mode 100644 src/main/java/roomescape/time/dto/TimeRequest.java create mode 100644 src/main/java/roomescape/time/dto/TimeResponse.java create mode 100644 src/main/java/roomescape/time/entity/Time.java create mode 100644 src/main/java/roomescape/time/repository/JpaTimeRepository.java delete mode 100644 src/main/java/roomescape/time/repository/TimeDao.java create mode 100644 src/main/java/roomescape/time/repository/TimeRepository.java create mode 100644 src/test/java/roomescape/JpaTest.java diff --git a/src/main/java/roomescape/auth/domain/LoginMember.java b/src/main/java/roomescape/auth/domain/LoginMember.java index 704cd2d6e..79ece2272 100644 --- a/src/main/java/roomescape/auth/domain/LoginMember.java +++ b/src/main/java/roomescape/auth/domain/LoginMember.java @@ -1,6 +1,6 @@ package roomescape.auth.domain; -import roomescape.member.domain.Role; +import roomescape.member.entity.Role; public record LoginMember( Long id, diff --git a/src/main/java/roomescape/auth/jwt/JwtTokenProvider.java b/src/main/java/roomescape/auth/jwt/JwtTokenProvider.java index a2a0d6609..3e916cef1 100644 --- a/src/main/java/roomescape/auth/jwt/JwtTokenProvider.java +++ b/src/main/java/roomescape/auth/jwt/JwtTokenProvider.java @@ -10,7 +10,7 @@ import org.springframework.stereotype.Component; import roomescape.auth.exception.AuthErrorCode; import roomescape.exception.ApplicationException; -import roomescape.member.domain.Member; +import roomescape.member.entity.Member; import java.security.Key; import java.util.Base64; diff --git a/src/main/java/roomescape/auth/service/AuthService.java b/src/main/java/roomescape/auth/service/AuthService.java index 19538d502..9150bcab5 100644 --- a/src/main/java/roomescape/auth/service/AuthService.java +++ b/src/main/java/roomescape/auth/service/AuthService.java @@ -9,25 +9,25 @@ import roomescape.auth.jwt.JwtTokenProvider; import roomescape.auth.repository.RefreshTokenDao; import roomescape.exception.ApplicationException; -import roomescape.member.domain.Member; +import roomescape.member.entity.Member; import roomescape.member.exception.MemberErrorCode; -import roomescape.member.repository.MemberDao; +import roomescape.member.repository.MemberRepository; @Service public class AuthService { - private final MemberDao memberDao; + private final MemberRepository memberRepository; private final RefreshTokenDao refreshTokenDao; private final JwtTokenProvider tokenProvider; - public AuthService(MemberDao memberDao, RefreshTokenDao refreshTokenDao, JwtTokenProvider tokenProvider) { - this.memberDao = memberDao; + public AuthService(MemberRepository memberRepository, RefreshTokenDao refreshTokenDao, JwtTokenProvider tokenProvider) { + this.memberRepository = memberRepository; this.refreshTokenDao = refreshTokenDao; this.tokenProvider = tokenProvider; } public TokenResponse login(LoginRequest loginRequest) { - Member member = memberDao.findByEmailAndPassword(loginRequest.email(), loginRequest.password()) + Member member = memberRepository.findByEmailAndPassword(loginRequest.email(), loginRequest.password()) .orElseThrow(() -> new ApplicationException(MemberErrorCode.LOGIN_FAILED)); String accessToken = tokenProvider.createAccessToken(member); @@ -43,7 +43,7 @@ public String reissue(String refreshToken) { Long memberId = tokenProvider.getLoginMemberId(refreshToken); - Member member = memberDao.findById(memberId) + Member member = memberRepository.findById(memberId) .orElseThrow(() -> new ApplicationException(MemberErrorCode.MEMBER_NOT_FOUND)); return tokenProvider.createAccessToken(member); diff --git a/src/main/java/roomescape/member/controller/MemberController.java b/src/main/java/roomescape/member/controller/MemberController.java index 7ce0827d7..52bc3d5a7 100644 --- a/src/main/java/roomescape/member/controller/MemberController.java +++ b/src/main/java/roomescape/member/controller/MemberController.java @@ -21,7 +21,7 @@ public MemberController(MemberService memberService) { @PostMapping("/members") public ResponseEntity createMember(@Valid @RequestBody MemberRequest memberRequest) { - MemberResponse member = memberService.createMember(memberRequest); + MemberResponse member = memberService.create(memberRequest); return ResponseEntity.created(URI.create("/members/" + member.getId())).body(member); } } diff --git a/src/main/java/roomescape/member/domain/Member.java b/src/main/java/roomescape/member/entity/Member.java similarity index 53% rename from src/main/java/roomescape/member/domain/Member.java rename to src/main/java/roomescape/member/entity/Member.java index 29d3e90eb..71c53abab 100644 --- a/src/main/java/roomescape/member/domain/Member.java +++ b/src/main/java/roomescape/member/entity/Member.java @@ -1,13 +1,36 @@ -package roomescape.member.domain; +package roomescape.member.entity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +@Entity(name = "member") public class Member { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + + @Column(nullable = false) private String name; + + @Column(nullable = false, unique = true) private String email; + + @Column(nullable = false) private String password; + + @Enumerated(EnumType.STRING) private Role role; - public Member(Long id, String name, String email, Role role) { + public Member() { + } + + public Member(Long id, String name, String email, String password, Role role) { this.id = id; this.name = name; this.email = email; diff --git a/src/main/java/roomescape/member/domain/Role.java b/src/main/java/roomescape/member/entity/Role.java similarity index 76% rename from src/main/java/roomescape/member/domain/Role.java rename to src/main/java/roomescape/member/entity/Role.java index dd3f5e6c6..f3491acef 100644 --- a/src/main/java/roomescape/member/domain/Role.java +++ b/src/main/java/roomescape/member/entity/Role.java @@ -1,4 +1,4 @@ -package roomescape.member.domain; +package roomescape.member.entity; public enum Role { diff --git a/src/main/java/roomescape/member/repository/JpaMemberRepository.java b/src/main/java/roomescape/member/repository/JpaMemberRepository.java new file mode 100644 index 000000000..ac61e135c --- /dev/null +++ b/src/main/java/roomescape/member/repository/JpaMemberRepository.java @@ -0,0 +1,52 @@ +package roomescape.member.repository; + +import jakarta.persistence.EntityManager; +import org.springframework.stereotype.Repository; +import roomescape.member.entity.Member; + +import java.util.List; +import java.util.Optional; + +@Repository +public class JpaMemberRepository implements MemberRepository { + + private final EntityManager entityManager; + + public JpaMemberRepository(EntityManager entityManager) { + this.entityManager = entityManager; + } + + @Override + public Member save(Member member) { + entityManager.persist(member); + return member; + } + + @Override + public Optional findById(Long memberId) { + Member member = entityManager.find(Member.class, memberId); + + return Optional.ofNullable(member); + } + + @Override + public Optional findByEmailAndPassword(String email, String password) { + String jpql = "SELECT m FROM member AS m WHERE m.email = :email AND m.password = :password"; + List members = entityManager.createQuery(jpql, Member.class) + .setParameter("email", email) + .setParameter("password", password) + .getResultList(); + + return members.stream().findFirst(); + } + + @Override + public Optional findByName(String name) { + String jpql = "SELECT m FROM member AS m WHERE m.name = :name"; + List members = entityManager.createQuery(jpql, Member.class) + .setParameter("name", name) + .getResultList(); + + return members.stream().findFirst(); + } +} diff --git a/src/main/java/roomescape/member/repository/MemberDao.java b/src/main/java/roomescape/member/repository/MemberDao.java deleted file mode 100644 index e6fe4cd23..000000000 --- a/src/main/java/roomescape/member/repository/MemberDao.java +++ /dev/null @@ -1,67 +0,0 @@ -package roomescape.member.repository; - -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.jdbc.support.KeyHolder; -import org.springframework.stereotype.Repository; -import roomescape.member.domain.Member; -import roomescape.member.domain.Role; - -import java.util.Optional; - -@Repository -public class MemberDao { - private JdbcTemplate jdbcTemplate; - - public MemberDao(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } - - public Member save(Member member) { - KeyHolder keyHolder = new GeneratedKeyHolder(); - jdbcTemplate.update(connection -> { - var ps = connection.prepareStatement("INSERT INTO member(name, email, password, role) VALUES (?, ?, ?, ?)", new String[]{"id"}); - ps.setString(1, member.getName()); - ps.setString(2, member.getEmail()); - ps.setString(3, member.getPassword()); - ps.setString(4, member.getRole().name()); - return ps; - }, keyHolder); - - return new Member(keyHolder.getKey().longValue(), member.getName(), member.getEmail(), member.getRole()); - } - - public Optional findById(Long id) { - return jdbcTemplate.query( - "SELECT * FROM member WHERE id = ?", - rowMapper(), - id - ).stream().findFirst(); - } - - public Optional findByEmailAndPassword(String email, String password) { - return jdbcTemplate.query( - "SELECT id, name, email, role FROM member WHERE email = ? AND password = ?", - rowMapper(), - email, password - ).stream().findFirst(); - } - - public Optional findByName(String name) { - return jdbcTemplate.query( - "SELECT id, name, email, role FROM member WHERE name = ?", - rowMapper(), - name - ).stream().findFirst(); - } - - private RowMapper rowMapper() { - return (resultSet, rowNum) -> new Member( - resultSet.getLong("id"), - resultSet.getString("name"), - resultSet.getString("email"), - Role.valueOf(resultSet.getString("role")) - ); - } -} diff --git a/src/main/java/roomescape/member/repository/MemberRepository.java b/src/main/java/roomescape/member/repository/MemberRepository.java new file mode 100644 index 000000000..d475135e0 --- /dev/null +++ b/src/main/java/roomescape/member/repository/MemberRepository.java @@ -0,0 +1,16 @@ +package roomescape.member.repository; + +import roomescape.member.entity.Member; + +import java.util.Optional; + +public interface MemberRepository { + + Member save(Member member); + + Optional findById(Long memberId); + + Optional findByEmailAndPassword(String email, String password); + + Optional findByName(String name); +} diff --git a/src/main/java/roomescape/member/service/MemberService.java b/src/main/java/roomescape/member/service/MemberService.java index f7d938b01..f549f349e 100644 --- a/src/main/java/roomescape/member/service/MemberService.java +++ b/src/main/java/roomescape/member/service/MemberService.java @@ -1,22 +1,24 @@ package roomescape.member.service; import org.springframework.stereotype.Service; -import roomescape.member.domain.Member; -import roomescape.member.domain.Role; +import org.springframework.transaction.annotation.Transactional; import roomescape.member.dto.MemberRequest; import roomescape.member.dto.MemberResponse; -import roomescape.member.repository.MemberDao; +import roomescape.member.entity.Member; +import roomescape.member.entity.Role; +import roomescape.member.repository.MemberRepository; @Service +@Transactional(readOnly = true) public class MemberService { - private MemberDao memberDao; + private MemberRepository memberRepository; - public MemberService(MemberDao memberDao) { - this.memberDao = memberDao; + public MemberService(MemberRepository memberRepository) { + this.memberRepository = memberRepository; } - public MemberResponse createMember(MemberRequest memberRequest) { - Member member = memberDao.save(new Member(memberRequest.getName(), memberRequest.getEmail(), memberRequest.getPassword(), Role.USER)); + public MemberResponse create(MemberRequest memberRequest) { + Member member = memberRepository.save(new Member(memberRequest.getName(), memberRequest.getEmail(), memberRequest.getPassword(), Role.USER)); return new MemberResponse(member.getId(), member.getName(), member.getEmail()); } } diff --git a/src/main/java/roomescape/reservation/controller/ReservationController.java b/src/main/java/roomescape/reservation/controller/ReservationController.java index 98259fd07..6c544963e 100644 --- a/src/main/java/roomescape/reservation/controller/ReservationController.java +++ b/src/main/java/roomescape/reservation/controller/ReservationController.java @@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.RestController; import roomescape.auth.domain.LoginMember; import roomescape.auth.web.Login; +import roomescape.reservation.dto.MyReservationResponse; import roomescape.reservation.dto.ReservationRequest; import roomescape.reservation.dto.ReservationResponse; import roomescape.reservation.service.ReservationService; @@ -31,9 +32,16 @@ public List list() { return reservationService.findAll(); } + @GetMapping("/reservations-mine") + public ResponseEntity> getUserReservations(@Login LoginMember loginMember) { + List responseList = reservationService.findReservationsByMember(loginMember); + + return ResponseEntity.ok(responseList); + } + @PostMapping("/reservations") public ResponseEntity create(@Valid @RequestBody ReservationRequest reservationRequest, @Login LoginMember loginMember) { - ReservationResponse reservation = reservationService.save(reservationRequest, loginMember); + ReservationResponse reservation = reservationService.create(reservationRequest, loginMember); return ResponseEntity.created(URI.create("/reservations/" + reservation.getId())).body(reservation); } diff --git a/src/main/java/roomescape/reservation/domain/Reservation.java b/src/main/java/roomescape/reservation/domain/Reservation.java deleted file mode 100644 index 75b1b538f..000000000 --- a/src/main/java/roomescape/reservation/domain/Reservation.java +++ /dev/null @@ -1,51 +0,0 @@ -package roomescape.reservation.domain; - -import roomescape.theme.domain.Theme; -import roomescape.time.domain.Time; - -public class Reservation { - private Long id; - private String name; - private String date; - private Time time; - private Theme theme; - - public Reservation(Long id, String name, String date, Time time, Theme theme) { - this.id = id; - this.name = name; - this.date = date; - this.time = time; - this.theme = theme; - } - - public Reservation(String name, String date, Time time, Theme theme) { - this.name = name; - this.date = date; - this.time = time; - this.theme = theme; - } - - public Reservation() { - - } - - public Long getId() { - return id; - } - - public String getName() { - return name; - } - - public String getDate() { - return date; - } - - public Time getTime() { - return time; - } - - public Theme getTheme() { - return theme; - } -} diff --git a/src/main/java/roomescape/reservation/dto/MyReservationResponse.java b/src/main/java/roomescape/reservation/dto/MyReservationResponse.java new file mode 100644 index 000000000..3a425481e --- /dev/null +++ b/src/main/java/roomescape/reservation/dto/MyReservationResponse.java @@ -0,0 +1,37 @@ +package roomescape.reservation.dto; + +public class MyReservationResponse { + private Long id; + private String theme; + private String date; + private String time; + private String status; + + public MyReservationResponse(Long id, String theme, String date, String time, String status) { + this.id = id; + this.theme = theme; + this.date = date; + this.time = time; + this.status = status; + } + + public Long getId() { + return id; + } + + public String getTheme() { + return theme; + } + + public String getDate() { + return date; + } + + public String getTime() { + return time; + } + + public String getStatus() { + return status; + } +} diff --git a/src/main/java/roomescape/reservation/dto/ReservationResponse.java b/src/main/java/roomescape/reservation/dto/ReservationResponse.java index 2c219d018..66507caf8 100644 --- a/src/main/java/roomescape/reservation/dto/ReservationResponse.java +++ b/src/main/java/roomescape/reservation/dto/ReservationResponse.java @@ -7,11 +7,11 @@ public class ReservationResponse { private String date; private String time; - public ReservationResponse(Long id, String name, String theme, String date, String time) { + public ReservationResponse(Long id, String name, String date, String time, String theme) { this.id = id; this.name = name; - this.theme = theme; this.date = date; + this.theme = theme; this.time = time; } diff --git a/src/main/java/roomescape/reservation/entity/Reservation.java b/src/main/java/roomescape/reservation/entity/Reservation.java new file mode 100644 index 000000000..e20843168 --- /dev/null +++ b/src/main/java/roomescape/reservation/entity/Reservation.java @@ -0,0 +1,71 @@ +package roomescape.reservation.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import roomescape.member.entity.Member; +import roomescape.theme.entity.Theme; +import roomescape.time.entity.Time; + +import java.time.LocalDate; + +@Entity(name = "reservation") +public class Reservation { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private LocalDate date; + + @ManyToOne + private Member member; + + @ManyToOne + private Time time; + + @ManyToOne + private Theme theme; + + public Reservation() { + } + + public Reservation(Long id, Member member, LocalDate date, Time time, Theme theme) { + this.id = id; + this.member = member; + this.date = date; + this.time = time; + this.theme = theme; + } + + public Reservation(Member member, LocalDate date, Time time, Theme theme) { + this.member = member; + this.date = date; + this.time = time; + this.theme = theme; + } + + public Long getId() { + return id; + } + + public Member getMember() { + return member; + } + + public LocalDate getDate() { + return date; + } + + public Time getTime() { + return time; + } + + public Theme getTheme() { + return theme; + } +} diff --git a/src/main/java/roomescape/reservation/repository/JpaReservationRepository.java b/src/main/java/roomescape/reservation/repository/JpaReservationRepository.java new file mode 100644 index 000000000..8f1144a95 --- /dev/null +++ b/src/main/java/roomescape/reservation/repository/JpaReservationRepository.java @@ -0,0 +1,60 @@ +package roomescape.reservation.repository; + +import jakarta.persistence.EntityManager; +import org.springframework.stereotype.Repository; +import roomescape.reservation.entity.Reservation; + +import java.time.LocalDate; +import java.util.List; + +@Repository +public class JpaReservationRepository implements ReservationRepository { + + private final EntityManager entityManager; + + public JpaReservationRepository(EntityManager entityManager) { + this.entityManager = entityManager; + } + + @Override + public List findAll() { + String jpql = "SELECT r FROM reservation AS r"; + + return entityManager.createQuery(jpql, Reservation.class) + .getResultList(); + } + + @Override + public List findAllByMemberId(Long memerId) { + String jpql = "SELECT r FROM reservation AS r WHERE r.member.id = :memberId"; + + return entityManager.createQuery(jpql, Reservation.class) + .setParameter("memberId", memerId) + .getResultList(); + } + + @Override + public List findByDateAndThemeId(LocalDate date, Long themeId) { + String jpql = "SELECT r FROM reservation AS r WHERE r.date = :date AND r.theme.id = :themeId"; + + return entityManager.createQuery(jpql, Reservation.class) + .setParameter("date", date) + .setParameter("themeId", themeId) + .getResultList(); + } + + @Override + public Reservation save(Reservation reservation) { + entityManager.persist(reservation); + + return reservation; + } + + @Override + public void deleteById(Long id) { + Reservation reservation = entityManager.find(Reservation.class, id); + if (reservation != null) { + entityManager.remove(reservation); + } + } +} diff --git a/src/main/java/roomescape/reservation/repository/ReservationDao.java b/src/main/java/roomescape/reservation/repository/ReservationDao.java deleted file mode 100644 index c92dbba35..000000000 --- a/src/main/java/roomescape/reservation/repository/ReservationDao.java +++ /dev/null @@ -1,129 +0,0 @@ -package roomescape.reservation.repository; - -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.jdbc.support.KeyHolder; -import org.springframework.stereotype.Repository; -import roomescape.reservation.domain.Reservation; -import roomescape.reservation.dto.ReservationRequest; -import roomescape.theme.domain.Theme; -import roomescape.time.domain.Time; - -import java.sql.PreparedStatement; -import java.util.List; - -@Repository -public class ReservationDao { - - private final JdbcTemplate jdbcTemplate; - - public ReservationDao(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } - - public List findAll() { - return jdbcTemplate.query( - "SELECT r.id AS reservation_id, r.name as reservation_name, r.date as reservation_date, " + - "t.id AS theme_id, t.name AS theme_name, t.description AS theme_description, " + - "ti.id AS time_id, ti.time_value AS time_value " + - "FROM reservation r " + - "JOIN theme t ON r.theme_id = t.id " + - "JOIN time ti ON r.time_id = ti.id", - - (rs, rowNum) -> new Reservation( - rs.getLong("reservation_id"), - rs.getString("reservation_name"), - rs.getString("reservation_date"), - new Time( - rs.getLong("time_id"), - rs.getString("time_value") - ), - new Theme( - rs.getLong("theme_id"), - rs.getString("theme_name"), - rs.getString("theme_description") - ))); - } - - public Reservation save(ReservationRequest reservationRequest, String name) { - KeyHolder keyHolder = new GeneratedKeyHolder(); - jdbcTemplate.update(connection -> { - PreparedStatement ps = connection.prepareStatement("INSERT INTO reservation(date, name, theme_id, time_id) VALUES (?, ?, ?, ?)", new String[]{"id"}); - ps.setString(1, reservationRequest.getDate()); - ps.setString(2, name); - ps.setLong(3, reservationRequest.getTheme()); - ps.setLong(4, reservationRequest.getTime()); - return ps; - }, keyHolder); - - Time time = jdbcTemplate.queryForObject("SELECT * FROM time WHERE id = ?", - (rs, rowNum) -> new Time(rs.getLong("id"), rs.getString("time_value")), - reservationRequest.getTime()); - - Theme theme = jdbcTemplate.queryForObject("SELECT * FROM theme WHERE id = ?", - (rs, rowNum) -> new Theme(rs.getLong("id"), rs.getString("name"), rs.getString("description")), - reservationRequest.getTheme()); - - return new Reservation( - keyHolder.getKey().longValue(), - name, - reservationRequest.getDate(), - time, - theme - ); - } - - public void deleteById(Long id) { - jdbcTemplate.update("DELETE FROM reservation WHERE id = ?", id); - } - - public List findReservationsByDateAndTheme(String date, Long themeId) { - return jdbcTemplate.query( - "SELECT r.id AS reservation_id, r.name as reservation_name, r.date as reservation_date, " + - "t.id AS theme_id, t.name AS theme_name, t.description AS theme_description, " + - "ti.id AS time_id, ti.time_value AS time_value " + - "FROM reservation r " + - "JOIN theme t ON r.theme_id = t.id " + - "JOIN time ti ON r.time_id = ti.id " + - "WHERE r.date = ? AND r.theme_id = ?", - new Object[]{date, themeId}, - (rs, rowNum) -> new Reservation( - rs.getLong("reservation_id"), - rs.getString("reservation_name"), - rs.getString("reservation_date"), - new Time( - rs.getLong("time_id"), - rs.getString("time_value") - ), - new Theme( - rs.getLong("theme_id"), - rs.getString("theme_name"), - rs.getString("theme_description") - ))); - } - - public List findByDateAndThemeId(String date, Long themeId) { - return jdbcTemplate.query( - "SELECT r.id AS reservation_id, r.name as reservation_name, r.date as reservation_date, " + - "t.id AS theme_id, t.name AS theme_name, t.description AS theme_description, " + - "ti.id AS time_id, ti.time_value AS time_value " + - "FROM reservation r " + - "JOIN theme t ON r.theme_id = t.id " + - "JOIN time ti ON r.time_id = ti.id " + - "WHERE r.date = ? AND r.theme_id = ?", - new Object[]{date, themeId}, - (rs, rowNum) -> new Reservation( - rs.getLong("reservation_id"), - rs.getString("reservation_name"), - rs.getString("reservation_date"), - new Time( - rs.getLong("time_id"), - rs.getString("time_value") - ), - new Theme( - rs.getLong("theme_id"), - rs.getString("theme_name"), - rs.getString("theme_description") - ))); - } -} diff --git a/src/main/java/roomescape/reservation/repository/ReservationRepository.java b/src/main/java/roomescape/reservation/repository/ReservationRepository.java new file mode 100644 index 000000000..002b5f2f1 --- /dev/null +++ b/src/main/java/roomescape/reservation/repository/ReservationRepository.java @@ -0,0 +1,19 @@ +package roomescape.reservation.repository; + +import roomescape.reservation.entity.Reservation; + +import java.time.LocalDate; +import java.util.List; + +public interface ReservationRepository { + + List findAll(); + + List findAllByMemberId(Long memerId); + + List findByDateAndThemeId(LocalDate date, Long themeId); + + Reservation save(Reservation reservation); + + void deleteById(Long id); +} diff --git a/src/main/java/roomescape/reservation/service/ReservationService.java b/src/main/java/roomescape/reservation/service/ReservationService.java index 34c8d9532..38dc05634 100644 --- a/src/main/java/roomescape/reservation/service/ReservationService.java +++ b/src/main/java/roomescape/reservation/service/ReservationService.java @@ -1,52 +1,97 @@ package roomescape.reservation.service; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import roomescape.auth.domain.LoginMember; import roomescape.exception.ApplicationException; +import roomescape.member.entity.Member; import roomescape.member.exception.MemberErrorCode; -import roomescape.member.repository.MemberDao; -import roomescape.reservation.domain.Reservation; +import roomescape.member.repository.MemberRepository; +import roomescape.reservation.dto.MyReservationResponse; import roomescape.reservation.dto.ReservationRequest; import roomescape.reservation.dto.ReservationResponse; -import roomescape.reservation.repository.ReservationDao; +import roomescape.reservation.entity.Reservation; +import roomescape.reservation.repository.ReservationRepository; +import roomescape.theme.entity.Theme; +import roomescape.theme.repository.ThemeRepository; +import roomescape.time.entity.Time; +import roomescape.time.repository.TimeRepository; +import java.time.LocalDate; import java.util.List; @Service +@Transactional(readOnly = true) public class ReservationService { - private ReservationDao reservationDao; - private MemberDao memberDao; + private final ReservationRepository reservationRepository; + private final MemberRepository memberRepository; + private final TimeRepository timeRepository; + private final ThemeRepository themeRepository; - public ReservationService(ReservationDao reservationDao, MemberDao memberDao) { - this.reservationDao = reservationDao; - this.memberDao = memberDao; + public ReservationService(ReservationRepository reservationRepository, MemberRepository memberRepository, TimeRepository timeRepository, ThemeRepository themeRepository) { + this.reservationRepository = reservationRepository; + this.memberRepository = memberRepository; + this.timeRepository = timeRepository; + this.themeRepository = themeRepository; } - public ReservationResponse save(ReservationRequest reservationRequest, LoginMember loginMember) { - String name = resolveName(reservationRequest, loginMember); + @Transactional + public ReservationResponse create(ReservationRequest request, LoginMember loginMember) { + Member member = resolveMember(request, loginMember); + LocalDate date = LocalDate.parse(request.getDate()); + Time time = timeRepository.findById(request.getTime()) + .orElseThrow(); + Theme theme = themeRepository.findById(request.getTheme()) + .orElseThrow(); - Reservation reservation = reservationDao.save(reservationRequest, name); + Reservation reservation = new Reservation(member, date, time, theme); + Reservation savedReservation = reservationRepository.save(reservation); - return new ReservationResponse(reservation.getId(), name, reservation.getTheme().getName(), reservation.getDate(), reservation.getTime().getValue()); + return new ReservationResponse( + savedReservation.getId(), + member.getName(), + savedReservation.getDate().toString(), + savedReservation.getTime().getTimeValue(), + savedReservation.getTheme().getName() + ); } - public void deleteById(Long id) { - reservationDao.deleteById(id); + public List findAll() { + return reservationRepository.findAll().stream() + .map(it -> new ReservationResponse( + it.getId(), + it.getMember().getName(), + it.getDate().toString(), + it.getTime().getTimeValue(), + it.getTheme().getName() + )) + .toList(); } - public List findAll() { - return reservationDao.findAll().stream() - .map(it -> new ReservationResponse(it.getId(), it.getName(), it.getTheme().getName(), it.getDate(), it.getTime().getValue())) + public List findReservationsByMember(LoginMember loginMember) { + return reservationRepository.findAllByMemberId(loginMember.id()) + .stream() + .map(reservation -> new MyReservationResponse( + reservation.getId(), + reservation.getTheme().getName(), + reservation.getDate().toString(), + reservation.getTime().getTimeValue(), + "예약" + )) .toList(); } - private String resolveName(ReservationRequest reservationRequest, LoginMember loginMember) { + public void deleteById(Long id) { + reservationRepository.deleteById(id); + } + + private Member resolveMember(ReservationRequest reservationRequest, LoginMember loginMember) { if (reservationRequest.getName() != null && !reservationRequest.getName().isBlank()) { - memberDao.findByName(reservationRequest.getName()) + return memberRepository.findByName(reservationRequest.getName()) .orElseThrow(() -> new ApplicationException(MemberErrorCode.MEMBER_NOT_FOUND)); - return reservationRequest.getName(); } - return loginMember.name(); + return memberRepository.findById(loginMember.id()) + .orElseThrow(() -> new ApplicationException(MemberErrorCode.MEMBER_NOT_FOUND)); } } diff --git a/src/main/java/roomescape/theme/controller/ThemeController.java b/src/main/java/roomescape/theme/controller/ThemeController.java index dd83f58f7..69a258ba7 100644 --- a/src/main/java/roomescape/theme/controller/ThemeController.java +++ b/src/main/java/roomescape/theme/controller/ThemeController.java @@ -7,34 +7,35 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; -import roomescape.theme.domain.Theme; -import roomescape.theme.repository.ThemeDao; +import roomescape.theme.dto.ThemeRequest; +import roomescape.theme.dto.ThemeResponse; +import roomescape.theme.service.ThemeService; import java.net.URI; import java.util.List; @RestController public class ThemeController { - private ThemeDao themeDao; + private ThemeService themeService; - public ThemeController(ThemeDao themeDao) { - this.themeDao = themeDao; + public ThemeController(ThemeService themeService) { + this.themeService = themeService; } @PostMapping("/themes") - public ResponseEntity createTheme(@RequestBody Theme theme) { - Theme newTheme = themeDao.save(theme); - return ResponseEntity.created(URI.create("/themes/" + newTheme.getId())).body(newTheme); + public ResponseEntity createTheme(@RequestBody ThemeRequest request) { + ThemeResponse themeResponse = themeService.create(request); + return ResponseEntity.created(URI.create("/themes/" + themeResponse.id())).body(themeResponse); } @GetMapping("/themes") - public ResponseEntity> list() { - return ResponseEntity.ok(themeDao.findAll()); + public ResponseEntity> list() { + return ResponseEntity.ok(themeService.findAll()); } @DeleteMapping("/themes/{id}") public ResponseEntity deleteTheme(@PathVariable Long id) { - themeDao.deleteById(id); + themeService.deleteById(id); return ResponseEntity.noContent().build(); } } diff --git a/src/main/java/roomescape/theme/dto/ThemeRequest.java b/src/main/java/roomescape/theme/dto/ThemeRequest.java new file mode 100644 index 000000000..997802235 --- /dev/null +++ b/src/main/java/roomescape/theme/dto/ThemeRequest.java @@ -0,0 +1,7 @@ +package roomescape.theme.dto; + +public record ThemeRequest( + String name, + String description +) { +} diff --git a/src/main/java/roomescape/theme/dto/ThemeResponse.java b/src/main/java/roomescape/theme/dto/ThemeResponse.java new file mode 100644 index 000000000..e1cbb7d1d --- /dev/null +++ b/src/main/java/roomescape/theme/dto/ThemeResponse.java @@ -0,0 +1,8 @@ +package roomescape.theme.dto; + +public record ThemeResponse( + Long id, + String name, + String description +) { +} diff --git a/src/main/java/roomescape/theme/domain/Theme.java b/src/main/java/roomescape/theme/entity/Theme.java similarity index 53% rename from src/main/java/roomescape/theme/domain/Theme.java rename to src/main/java/roomescape/theme/entity/Theme.java index 9063a4e2c..3df004202 100644 --- a/src/main/java/roomescape/theme/domain/Theme.java +++ b/src/main/java/roomescape/theme/entity/Theme.java @@ -1,10 +1,29 @@ -package roomescape.theme.domain; +package roomescape.theme.entity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import org.hibernate.annotations.ColumnDefault; + +@Entity(name = "theme") public class Theme { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + + @Column(nullable = false) private String name; + + @Column(nullable = false) private String description; + @Column(nullable = false) + @ColumnDefault("false") + private Boolean deleted = false; + public Theme() { } diff --git a/src/main/java/roomescape/theme/repository/JpaThemeRepository.java b/src/main/java/roomescape/theme/repository/JpaThemeRepository.java new file mode 100644 index 000000000..662b02afb --- /dev/null +++ b/src/main/java/roomescape/theme/repository/JpaThemeRepository.java @@ -0,0 +1,43 @@ +package roomescape.theme.repository; + +import jakarta.persistence.EntityManager; +import org.springframework.stereotype.Repository; +import roomescape.theme.entity.Theme; + +import java.util.List; +import java.util.Optional; + +@Repository +public class JpaThemeRepository implements ThemeRepository { + + private final EntityManager entityManager; + + public JpaThemeRepository(EntityManager entityManager) { + this.entityManager = entityManager; + } + + @Override + public Theme save(Theme theme) { + entityManager.persist(theme); + return theme; + } + + @Override + public List findAll() { + String jpql = "SELECT t FROM theme AS t"; + return entityManager.createQuery(jpql, Theme.class) + .getResultList(); + } + + @Override + public Optional findById(Long id) { + Theme theme = entityManager.find(Theme.class, id); + return Optional.ofNullable(theme); + } + + @Override + public void deleteById(Long id) { + Theme theme = entityManager.find(Theme.class, id); + entityManager.remove(theme); + } +} diff --git a/src/main/java/roomescape/theme/repository/ThemeDao.java b/src/main/java/roomescape/theme/repository/ThemeDao.java deleted file mode 100644 index 5d681c408..000000000 --- a/src/main/java/roomescape/theme/repository/ThemeDao.java +++ /dev/null @@ -1,42 +0,0 @@ -package roomescape.theme.repository; - -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.jdbc.support.KeyHolder; -import org.springframework.stereotype.Repository; -import roomescape.theme.domain.Theme; - -import java.util.List; - -@Repository -public class ThemeDao { - private JdbcTemplate jdbcTemplate; - - public ThemeDao(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } - - public List findAll() { - return jdbcTemplate.query("SELECT * FROM theme where deleted = false", (rs, rowNum) -> new Theme( - rs.getLong("id"), - rs.getString("name"), - rs.getString("description") - )); - } - - public Theme save(Theme theme) { - KeyHolder keyHolder = new GeneratedKeyHolder(); - jdbcTemplate.update(connection -> { - var ps = connection.prepareStatement("INSERT INTO theme(name, description) VALUES (?, ?)", new String[]{"id"}); - ps.setString(1, theme.getName()); - ps.setString(2, theme.getDescription()); - return ps; - }, keyHolder); - - return new Theme(keyHolder.getKey().longValue(), theme.getName(), theme.getDescription()); - } - - public void deleteById(Long id) { - jdbcTemplate.update("UPDATE theme SET deleted = true WHERE id = ?", id); - } -} diff --git a/src/main/java/roomescape/theme/repository/ThemeRepository.java b/src/main/java/roomescape/theme/repository/ThemeRepository.java new file mode 100644 index 000000000..a85cda3d5 --- /dev/null +++ b/src/main/java/roomescape/theme/repository/ThemeRepository.java @@ -0,0 +1,17 @@ +package roomescape.theme.repository; + +import roomescape.theme.entity.Theme; + +import java.util.List; +import java.util.Optional; + +public interface ThemeRepository { + + Theme save(Theme theme); + + List findAll(); + + Optional findById(Long id); + + void deleteById(Long id); +} diff --git a/src/main/java/roomescape/theme/service/ThemeService.java b/src/main/java/roomescape/theme/service/ThemeService.java new file mode 100644 index 000000000..7320ca4a0 --- /dev/null +++ b/src/main/java/roomescape/theme/service/ThemeService.java @@ -0,0 +1,47 @@ +package roomescape.theme.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import roomescape.theme.dto.ThemeRequest; +import roomescape.theme.dto.ThemeResponse; +import roomescape.theme.entity.Theme; +import roomescape.theme.repository.ThemeRepository; + +import java.util.List; + +@Service +@Transactional(readOnly = true) +public class ThemeService { + + private final ThemeRepository themeRepository; + + public ThemeService(ThemeRepository themeRepository) { + this.themeRepository = themeRepository; + } + + public ThemeResponse create(ThemeRequest request) { + Theme theme = new Theme(request.name(), request.description()); + Theme savedTheme = themeRepository.save(theme); + + return new ThemeResponse( + savedTheme.getId(), + savedTheme.getName(), + savedTheme.getDescription() + ); + } + + public List findAll() { + return themeRepository.findAll() + .stream() + .map(theme -> new ThemeResponse( + theme.getId(), + theme.getName(), + theme.getDescription() + )) + .toList(); + } + + public void deleteById(Long id) { + themeRepository.deleteById(id); + } +} diff --git a/src/main/java/roomescape/time/controller/TimeController.java b/src/main/java/roomescape/time/controller/TimeController.java index 7a589acb6..f640c9d94 100644 --- a/src/main/java/roomescape/time/controller/TimeController.java +++ b/src/main/java/roomescape/time/controller/TimeController.java @@ -8,8 +8,10 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import roomescape.time.domain.Time; import roomescape.time.dto.AvailableTime; +import roomescape.time.dto.TimeRequest; +import roomescape.time.dto.TimeResponse; +import roomescape.time.entity.Time; import roomescape.time.service.TimeService; import java.net.URI; @@ -24,17 +26,17 @@ public TimeController(TimeService timeService) { } @GetMapping("/times") - public List