From 3f4a41795484803341835d190ee4b2ab8f074f2d Mon Sep 17 00:00:00 2001 From: Minjoo Date: Tue, 14 May 2024 15:25:46 +0900 Subject: [PATCH 01/42] migrate first mission code Co-authored-by: tkdgur0906 --- README.md | 40 + build.gradle | 9 +- .../auth/AdminAuthHandlerInterceptor.java | 37 + .../roomescape/auth/AuthenticatedMember.java | 11 + .../AuthenticatedMemberArgumentResolver.java | 47 ++ .../java/roomescape/config/AuthWebConfig.java | 33 + .../controller/api/AuthApiController.java | 49 ++ .../controller/api/MemberApiController.java | 30 + .../api/ReservationApiController.java | 93 ++ .../api/ReservationTimeApiController.java | 81 ++ .../controller/api/ThemeApiController.java | 72 ++ .../controller/web/AdminController.java | 28 + .../controller/web/AuthController.java | 13 + .../controller/web/UserController.java | 18 + src/main/java/roomescape/domain/Member.java | 42 + .../java/roomescape/domain/Reservation.java | 58 ++ .../roomescape/domain/ReservationStatus.java | 49 ++ .../roomescape/domain/ReservationTime.java | 40 + src/main/java/roomescape/domain/Role.java | 8 + src/main/java/roomescape/domain/Theme.java | 54 ++ .../exception/AuthenticationException.java | 8 + .../exception/GlobalExceptionHandler.java | 46 + .../repository/MemberRepository.java | 52 ++ .../repository/ReservationRepository.java | 187 ++++ .../repository/ReservationTimeRepository.java | 89 ++ .../repository/ThemeRepository.java | 85 ++ .../roomescape/service/auth/AuthService.java | 32 + .../service/auth/TokenProvider.java | 52 ++ .../service/dto/request/LoginRequest.java | 9 + .../request/ReservationAdminSaveRequest.java | 20 + .../dto/request/ReservationSaveRequest.java | 19 + .../request/ReservationTimeSaveRequest.java | 13 + .../service/dto/request/ThemeSaveRequest.java | 14 + .../dto/response/MemberIdAndNameResponse.java | 4 + .../dto/response/ReservationResponse.java | 19 + .../response/ReservationStatusResponse.java | 12 + .../dto/response/ReservationTimeResponse.java | 13 + .../service/dto/response/ThemeResponse.java | 10 + .../service/member/MemberService.java | 21 + .../AdminReservationCreateService.java | 32 + .../reservation/ReservationCreateService.java | 32 + .../ReservationCreateValidator.java | 65 ++ .../reservation/ReservationDeleteService.java | 20 + .../reservation/ReservationFindService.java | 27 + .../ReservationTimeCreateService.java | 25 + .../ReservationTimeDeleteService.java | 28 + .../ReservationTimeFindService.java | 29 + .../service/theme/ThemeCreateService.java | 21 + .../service/theme/ThemeDeleteService.java | 29 + .../service/theme/ThemeFindService.java | 32 + src/main/resources/application.yml | 10 + src/main/resources/data.sql | 18 + src/main/resources/schema.sql | 38 + src/main/resources/static/css/flatpickr.css | 795 ++++++++++++++++++ src/main/resources/static/css/reservation.css | 15 + src/main/resources/static/css/style.css | 62 ++ .../resources/static/image/admin-logo.png | Bin 0 -> 4640 bytes .../static/image/default-profile.png | Bin 0 -> 20300 bytes src/main/resources/static/js/flatpickr.js | 2 + src/main/resources/static/js/ranking.js | 37 + .../resources/static/js/reservation-legacy.js | 146 ++++ .../resources/static/js/reservation-new.js | 194 +++++ .../static/js/reservation-with-member.js | 238 ++++++ src/main/resources/static/js/reservation.js | 179 ++++ src/main/resources/static/js/scripts.js | 0 src/main/resources/static/js/theme.js | 136 +++ src/main/resources/static/js/time.js | 135 +++ .../resources/static/js/user-reservation.js | 182 ++++ src/main/resources/static/js/user-scripts.js | 152 ++++ src/main/resources/templates/admin/index.html | 58 ++ .../templates/admin/reservation-legacy.html | 57 ++ .../templates/admin/reservation-new.html | 105 +++ .../templates/admin/reservation.html | 60 ++ src/main/resources/templates/admin/theme.html | 76 ++ src/main/resources/templates/admin/time.html | 74 ++ src/main/resources/templates/index.html | 56 ++ src/main/resources/templates/login.html | 64 ++ src/main/resources/templates/reservation.html | 89 ++ src/main/resources/templates/signup.html | 67 ++ src/test/java/roomescape/JDBCTest.java | 31 + .../roomescape/RoomescapeApplicationTest.java | 13 - .../controller/api/AuthApiControllerTest.java | 58 ++ .../api/MemberApiControllerTest.java | 24 + .../api/ReservationApiControllerTest.java | 110 +++ .../api/ReservationTimeApiControllerTest.java | 43 + .../api/ThemeApiControllerTest.java | 65 ++ .../controller/web/AdminControllerTest.java | 37 + .../domain/ReservationStatusTest.java | 31 + .../dto/ReservationSaveRequestTest.java | 19 + .../service/dto/ThemeSaveRequestTest.java | 17 + .../ReservationCreateServiceTest.java | 73 ++ .../ReservationTimeCreateServiceTest.java | 46 + .../ReservationTimeDeleteServiceTest.java | 41 + .../ReservationTimeFindServiceTest.java | 41 + .../service/theme/ThemeDeleteServiceTest.java | 41 + .../java/roomescape/util/TokenGenerator.java | 24 + src/test/resources/data.sql | 27 + 97 files changed, 5625 insertions(+), 18 deletions(-) create mode 100644 README.md create mode 100644 src/main/java/roomescape/auth/AdminAuthHandlerInterceptor.java create mode 100644 src/main/java/roomescape/auth/AuthenticatedMember.java create mode 100644 src/main/java/roomescape/auth/AuthenticatedMemberArgumentResolver.java create mode 100644 src/main/java/roomescape/config/AuthWebConfig.java create mode 100644 src/main/java/roomescape/controller/api/AuthApiController.java create mode 100644 src/main/java/roomescape/controller/api/MemberApiController.java create mode 100644 src/main/java/roomescape/controller/api/ReservationApiController.java create mode 100644 src/main/java/roomescape/controller/api/ReservationTimeApiController.java create mode 100644 src/main/java/roomescape/controller/api/ThemeApiController.java create mode 100644 src/main/java/roomescape/controller/web/AdminController.java create mode 100644 src/main/java/roomescape/controller/web/AuthController.java create mode 100644 src/main/java/roomescape/controller/web/UserController.java create mode 100644 src/main/java/roomescape/domain/Member.java create mode 100644 src/main/java/roomescape/domain/Reservation.java create mode 100644 src/main/java/roomescape/domain/ReservationStatus.java create mode 100644 src/main/java/roomescape/domain/ReservationTime.java create mode 100644 src/main/java/roomescape/domain/Role.java create mode 100644 src/main/java/roomescape/domain/Theme.java create mode 100644 src/main/java/roomescape/exception/AuthenticationException.java create mode 100644 src/main/java/roomescape/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/roomescape/repository/MemberRepository.java create mode 100644 src/main/java/roomescape/repository/ReservationRepository.java create mode 100644 src/main/java/roomescape/repository/ReservationTimeRepository.java create mode 100644 src/main/java/roomescape/repository/ThemeRepository.java create mode 100644 src/main/java/roomescape/service/auth/AuthService.java create mode 100644 src/main/java/roomescape/service/auth/TokenProvider.java create mode 100644 src/main/java/roomescape/service/dto/request/LoginRequest.java create mode 100644 src/main/java/roomescape/service/dto/request/ReservationAdminSaveRequest.java create mode 100644 src/main/java/roomescape/service/dto/request/ReservationSaveRequest.java create mode 100644 src/main/java/roomescape/service/dto/request/ReservationTimeSaveRequest.java create mode 100644 src/main/java/roomescape/service/dto/request/ThemeSaveRequest.java create mode 100644 src/main/java/roomescape/service/dto/response/MemberIdAndNameResponse.java create mode 100644 src/main/java/roomescape/service/dto/response/ReservationResponse.java create mode 100644 src/main/java/roomescape/service/dto/response/ReservationStatusResponse.java create mode 100644 src/main/java/roomescape/service/dto/response/ReservationTimeResponse.java create mode 100644 src/main/java/roomescape/service/dto/response/ThemeResponse.java create mode 100644 src/main/java/roomescape/service/member/MemberService.java create mode 100644 src/main/java/roomescape/service/reservation/AdminReservationCreateService.java create mode 100644 src/main/java/roomescape/service/reservation/ReservationCreateService.java create mode 100644 src/main/java/roomescape/service/reservation/ReservationCreateValidator.java create mode 100644 src/main/java/roomescape/service/reservation/ReservationDeleteService.java create mode 100644 src/main/java/roomescape/service/reservation/ReservationFindService.java create mode 100644 src/main/java/roomescape/service/reservationtime/ReservationTimeCreateService.java create mode 100644 src/main/java/roomescape/service/reservationtime/ReservationTimeDeleteService.java create mode 100644 src/main/java/roomescape/service/reservationtime/ReservationTimeFindService.java create mode 100644 src/main/java/roomescape/service/theme/ThemeCreateService.java create mode 100644 src/main/java/roomescape/service/theme/ThemeDeleteService.java create mode 100644 src/main/java/roomescape/service/theme/ThemeFindService.java create mode 100644 src/main/resources/application.yml create mode 100644 src/main/resources/data.sql create mode 100644 src/main/resources/schema.sql create mode 100644 src/main/resources/static/css/flatpickr.css create mode 100644 src/main/resources/static/css/reservation.css create mode 100644 src/main/resources/static/css/style.css create mode 100644 src/main/resources/static/image/admin-logo.png create mode 100644 src/main/resources/static/image/default-profile.png create mode 100644 src/main/resources/static/js/flatpickr.js create mode 100644 src/main/resources/static/js/ranking.js create mode 100644 src/main/resources/static/js/reservation-legacy.js create mode 100644 src/main/resources/static/js/reservation-new.js create mode 100644 src/main/resources/static/js/reservation-with-member.js create mode 100644 src/main/resources/static/js/reservation.js create mode 100644 src/main/resources/static/js/scripts.js create mode 100644 src/main/resources/static/js/theme.js create mode 100644 src/main/resources/static/js/time.js create mode 100644 src/main/resources/static/js/user-reservation.js create mode 100644 src/main/resources/static/js/user-scripts.js create mode 100644 src/main/resources/templates/admin/index.html create mode 100644 src/main/resources/templates/admin/reservation-legacy.html create mode 100644 src/main/resources/templates/admin/reservation-new.html create mode 100644 src/main/resources/templates/admin/reservation.html create mode 100644 src/main/resources/templates/admin/theme.html create mode 100644 src/main/resources/templates/admin/time.html create mode 100644 src/main/resources/templates/index.html create mode 100644 src/main/resources/templates/login.html create mode 100644 src/main/resources/templates/reservation.html create mode 100644 src/main/resources/templates/signup.html create mode 100644 src/test/java/roomescape/JDBCTest.java delete mode 100644 src/test/java/roomescape/RoomescapeApplicationTest.java create mode 100644 src/test/java/roomescape/controller/api/AuthApiControllerTest.java create mode 100644 src/test/java/roomescape/controller/api/MemberApiControllerTest.java create mode 100644 src/test/java/roomescape/controller/api/ReservationApiControllerTest.java create mode 100644 src/test/java/roomescape/controller/api/ReservationTimeApiControllerTest.java create mode 100644 src/test/java/roomescape/controller/api/ThemeApiControllerTest.java create mode 100644 src/test/java/roomescape/controller/web/AdminControllerTest.java create mode 100644 src/test/java/roomescape/domain/ReservationStatusTest.java create mode 100644 src/test/java/roomescape/service/dto/ReservationSaveRequestTest.java create mode 100644 src/test/java/roomescape/service/dto/ThemeSaveRequestTest.java create mode 100644 src/test/java/roomescape/service/reservation/ReservationCreateServiceTest.java create mode 100644 src/test/java/roomescape/service/reservationtime/ReservationTimeCreateServiceTest.java create mode 100644 src/test/java/roomescape/service/reservationtime/ReservationTimeDeleteServiceTest.java create mode 100644 src/test/java/roomescape/service/reservationtime/ReservationTimeFindServiceTest.java create mode 100644 src/test/java/roomescape/service/theme/ThemeDeleteServiceTest.java create mode 100644 src/test/java/roomescape/util/TokenGenerator.java create mode 100644 src/test/resources/data.sql diff --git a/README.md b/README.md new file mode 100644 index 000000000..07b59e312 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +## 요구사항 명세 + +- [x] localhost:8080/admin 요청 시 index.html 페이지가 응답한다. +- [x] /admin/reservation 요청 시 reservation-legacy.html 응답한다. +- [x] /reservations 요청 시 예약 목록을 응답한다. +- [x] 예약 페이지 요청 시 예약 목록을 조회하여 보여준다. +- [x] 예약을 추가한다. +- [x] 예약을 삭제한다. + + +- [x] 시간 생성 시 시작 시간에 유효하지 않은 값이 입력되었을 때 + - [x] 시작 시간은 빈칸일 수 없다. + - [x] HH:mm 형식이어야 한다. + +- [x] 예약 생성 시 예약자명, 날짜, 시간에 유효하지 않은 값이 입력 되었을 때 + - [x] 예약자명은 빈칸일 수 없다. + - [x] 날짜는 빈칸일 수 없다. + - [x] 날짜는 YYYY-MM-dd 형식이어야 한다. + - [x] 시간에 대한 아이디는 정수여야 한다. + +- [x] 특정 시간에 대한 예약이 존재할 때, 그 시간을 삭제할 수 없다. +- [x] 존재하는 시간에 대해서만 예약할 수 있다. + +- [x] 지나간 날짜와 시간에 대한 예약 생성은 불가능하다. +- [x] 중복 예약은 불가능하다. (ex. 이미 4월 1일 10시에 예약이 되어있다면, 4월 1일 10시에 대한 예약을 생성할 수 없다) +- [x] 시간은 중복될 수 없다. + +- [x] 모든 테마는 시작 시간이 동일하다. +- [x] 테마 이름은 빈칸일 수 없다. +- [x] 관리자가 테마를 관리할 수 있다. +- [x] 이미 예약 중인 테마는 삭제할 수 없다. +- [x] 존재하는 테마에 대해서만 예약할 수 있다. +- [x] 관리자가 방탈출 예약 시, 테마 정보를 포함할 수 있다. + +- [x] 사용자가 날짜를 선택하면 테마를 조회할 수 있다. +- [x] 사용자가 테마를 선택하면 예약 가능한 시간을 조회할 수 있다. +- [x] 사용자는 예약자명을 입력할 수 있다. +- [x] 사용자가 예약할 수 있다. +- [x] 인기 테마 조회 기능을 추가합니다. + - [x] 최근 일주일을 기준으로 해당 기간 내에 방문하는 예약이 많은 테마 10개를 조회한다. diff --git a/build.gradle b/build.gradle index c1b2280eb..603e9418a 100644 --- a/build.gradle +++ b/build.gradle @@ -17,11 +17,10 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-jdbc' - - implementation 'io.jsonwebtoken:jjwt-api:0.11.2' - implementation 'io.jsonwebtoken:jjwt-impl:0.11.2' - implementation 'io.jsonwebtoken:jjwt-gson:0.11.2' - + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'io.jsonwebtoken:jjwt-api:0.12.5' + implementation 'io.jsonwebtoken:jjwt-jackson:0.12.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5' runtimeOnly 'com.h2database:h2' testImplementation 'org.springframework.boot:spring-boot-starter-test' diff --git a/src/main/java/roomescape/auth/AdminAuthHandlerInterceptor.java b/src/main/java/roomescape/auth/AdminAuthHandlerInterceptor.java new file mode 100644 index 000000000..1d0237a5d --- /dev/null +++ b/src/main/java/roomescape/auth/AdminAuthHandlerInterceptor.java @@ -0,0 +1,37 @@ +package roomescape.auth; + +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.domain.Member; +import roomescape.domain.Role; +import roomescape.exception.AuthenticationException; +import roomescape.service.auth.AuthService; +import roomescape.service.auth.TokenProvider; + +@Component +public class AdminAuthHandlerInterceptor implements HandlerInterceptor { + + private final AuthService authService; + private final TokenProvider tokenProvider; + + public AdminAuthHandlerInterceptor(AuthService authService, TokenProvider tokenProvider) { + this.authService = authService; + this.tokenProvider = tokenProvider; + } + + @Override + public boolean preHandle(HttpServletRequest request, + HttpServletResponse response, + Object handler) throws Exception { + Cookie[] cookies = request.getCookies(); + String token = tokenProvider.extractTokenFromCookie(cookies); + Member member = authService.findMemberByToken(token); + if (member == null || !member.getRole().equals(Role.ADMIN)) { + throw new AuthenticationException("권한이 없습니다."); + } + return true; + } +} diff --git a/src/main/java/roomescape/auth/AuthenticatedMember.java b/src/main/java/roomescape/auth/AuthenticatedMember.java new file mode 100644 index 000000000..d3afdd07d --- /dev/null +++ b/src/main/java/roomescape/auth/AuthenticatedMember.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 AuthenticatedMember { +} diff --git a/src/main/java/roomescape/auth/AuthenticatedMemberArgumentResolver.java b/src/main/java/roomescape/auth/AuthenticatedMemberArgumentResolver.java new file mode 100644 index 000000000..46cf57cda --- /dev/null +++ b/src/main/java/roomescape/auth/AuthenticatedMemberArgumentResolver.java @@ -0,0 +1,47 @@ +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.domain.Member; +import roomescape.exception.AuthenticationException; +import roomescape.service.auth.AuthService; +import roomescape.service.auth.TokenProvider; + +@Component +public class AuthenticatedMemberArgumentResolver implements HandlerMethodArgumentResolver { + + private final AuthService authService; + private final TokenProvider tokenProvider; + + public AuthenticatedMemberArgumentResolver(AuthService authService, TokenProvider tokenProvider) { + this.authService = authService; + this.tokenProvider = tokenProvider; + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(AuthenticatedMember.class) + && Member.class.isAssignableFrom(parameter.getParameterType()); + } + + @Override + public Member resolveArgument(MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) throws Exception { + HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); + Cookie[] cookies = request.getCookies(); + String token = tokenProvider.extractTokenFromCookie(cookies); + + if ("".equals(token)) { + throw new AuthenticationException("로그인해 주세요"); + } + return authService.findMemberByToken(token); + } +} diff --git a/src/main/java/roomescape/config/AuthWebConfig.java b/src/main/java/roomescape/config/AuthWebConfig.java new file mode 100644 index 000000000..40fbc88b4 --- /dev/null +++ b/src/main/java/roomescape/config/AuthWebConfig.java @@ -0,0 +1,33 @@ +package roomescape.config; + +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.AdminAuthHandlerInterceptor; +import roomescape.auth.AuthenticatedMemberArgumentResolver; + +import java.util.List; + +@Configuration +public class AuthWebConfig implements WebMvcConfigurer { + + private final AuthenticatedMemberArgumentResolver authenticatedMemberArgumentResolver; + private final AdminAuthHandlerInterceptor adminAuthHandlerInterceptor; + + public AuthWebConfig(AuthenticatedMemberArgumentResolver authenticatedMemberArgumentResolver, + AdminAuthHandlerInterceptor adminAuthHandlerInterceptor) { + this.authenticatedMemberArgumentResolver = authenticatedMemberArgumentResolver; + this.adminAuthHandlerInterceptor = adminAuthHandlerInterceptor; + } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(authenticatedMemberArgumentResolver); + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(adminAuthHandlerInterceptor).addPathPatterns("/admin/**","/members"); + } +} diff --git a/src/main/java/roomescape/controller/api/AuthApiController.java b/src/main/java/roomescape/controller/api/AuthApiController.java new file mode 100644 index 000000000..1372a9b69 --- /dev/null +++ b/src/main/java/roomescape/controller/api/AuthApiController.java @@ -0,0 +1,49 @@ +package roomescape.controller.api; + +import jakarta.servlet.http.Cookie; +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.auth.AuthenticatedMember; +import roomescape.domain.Member; +import roomescape.service.auth.AuthService; +import roomescape.service.dto.request.LoginRequest; +import roomescape.service.dto.response.MemberIdAndNameResponse; + +@RestController +public class AuthApiController { + + private final AuthService authService; + + public AuthApiController(AuthService authService) { + this.authService = authService; + } + + @GetMapping("/login/check") + public ResponseEntity getMemberLoginInfo(@AuthenticatedMember Member member) { + return ResponseEntity.ok(new MemberIdAndNameResponse(member.getId(), member.getName())); + } + + @PostMapping("/login") + public ResponseEntity login(@RequestBody @Valid LoginRequest request, HttpServletResponse response) { + String accessToken = authService.login(request); + Cookie cookie = new Cookie("token", accessToken); + cookie.setHttpOnly(true); + cookie.setPath("/"); + response.addCookie(cookie); + return ResponseEntity.ok().build(); + } + + @PostMapping("/logout") + public ResponseEntity logout(HttpServletResponse response) { + Cookie cookie = new Cookie("token", null); + cookie.setPath("/"); + cookie.setMaxAge(0); + response.addCookie(cookie); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/roomescape/controller/api/MemberApiController.java b/src/main/java/roomescape/controller/api/MemberApiController.java new file mode 100644 index 000000000..6c50651a8 --- /dev/null +++ b/src/main/java/roomescape/controller/api/MemberApiController.java @@ -0,0 +1,30 @@ +package roomescape.controller.api; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import roomescape.domain.Member; +import roomescape.service.dto.response.MemberIdAndNameResponse; +import roomescape.service.member.MemberService; + +import java.util.List; + +@RestController +public class MemberApiController { + + private final MemberService memberService; + + public MemberApiController(MemberService memberService) { + this.memberService = memberService; + } + + @GetMapping("/members") + public ResponseEntity> getMembers() { + List members = memberService.findMembers(); + return ResponseEntity.ok( + members.stream() + .map(member -> new MemberIdAndNameResponse(member.getId(), member.getName())) + .toList() + ); + } +} diff --git a/src/main/java/roomescape/controller/api/ReservationApiController.java b/src/main/java/roomescape/controller/api/ReservationApiController.java new file mode 100644 index 000000000..56abe14da --- /dev/null +++ b/src/main/java/roomescape/controller/api/ReservationApiController.java @@ -0,0 +1,93 @@ +package roomescape.controller.api; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import roomescape.auth.AuthenticatedMember; +import roomescape.domain.Member; +import roomescape.domain.Reservation; +import roomescape.service.dto.request.ReservationAdminSaveRequest; +import roomescape.service.dto.request.ReservationSaveRequest; +import roomescape.service.dto.response.ReservationResponse; +import roomescape.service.reservation.AdminReservationCreateService; +import roomescape.service.reservation.ReservationCreateService; +import roomescape.service.reservation.ReservationDeleteService; +import roomescape.service.reservation.ReservationFindService; + +import java.net.URI; +import java.time.LocalDate; +import java.util.List; + +@Validated +@RestController +public class ReservationApiController { + + private final ReservationCreateService reservationCreateService; + private final AdminReservationCreateService adminReservationCreateService; + private final ReservationFindService reservationFindService; + private final ReservationDeleteService reservationDeleteService; + + public ReservationApiController(ReservationCreateService reservationCreateService, + AdminReservationCreateService adminReservationCreateService, + ReservationFindService reservationFindService, + ReservationDeleteService reservationDeleteService) { + this.reservationCreateService = reservationCreateService; + this.adminReservationCreateService = adminReservationCreateService; + this.reservationFindService = reservationFindService; + this.reservationDeleteService = reservationDeleteService; + } + + @GetMapping("/reservations") + public ResponseEntity> getReservations() { + List reservations = reservationFindService.findReservations(); + return ResponseEntity.ok( + reservations.stream() + .map(ReservationResponse::new) + .toList() + ); + } + + @GetMapping("/admin/reservations/search") + public ResponseEntity> getSearchingReservations(@RequestParam long memberId, + @RequestParam long themeId, + @RequestParam LocalDate dateFrom, + @RequestParam LocalDate dateTo + ) { + List reservations = reservationFindService.searchReservations(memberId, themeId, dateFrom, dateTo); + return ResponseEntity.ok( + reservations.stream() + .map(ReservationResponse::new) + .toList() + ); + } + + @PostMapping("/reservations") + public ResponseEntity addReservationByUser(@RequestBody @Valid ReservationSaveRequest request, + @AuthenticatedMember Member member) { + Reservation newReservation = reservationCreateService.createReservation(request, member); + return ResponseEntity.created(URI.create("/reservations/" + newReservation.getId())) + .body(new ReservationResponse(newReservation)); + } + + @PostMapping("/admin/reservations") + public ResponseEntity addReservationByAdmin(@RequestBody @Valid ReservationAdminSaveRequest request) { + Reservation newReservation = adminReservationCreateService.createReservation(request); + return ResponseEntity.created(URI.create("/admin/reservations/" + newReservation.getId())) + .body(new ReservationResponse(newReservation)); + } + + @DeleteMapping("/reservations/{id}") + public ResponseEntity deleteReservation(@PathVariable + @Positive(message = "1 이상의 값만 입력해주세요.") long id) { + reservationDeleteService.deleteReservation(id); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/roomescape/controller/api/ReservationTimeApiController.java b/src/main/java/roomescape/controller/api/ReservationTimeApiController.java new file mode 100644 index 000000000..28d67b13c --- /dev/null +++ b/src/main/java/roomescape/controller/api/ReservationTimeApiController.java @@ -0,0 +1,81 @@ +package roomescape.controller.api; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import roomescape.domain.ReservationStatus; +import roomescape.domain.ReservationTime; +import roomescape.service.dto.request.ReservationTimeSaveRequest; +import roomescape.service.dto.response.ReservationStatusResponse; +import roomescape.service.dto.response.ReservationTimeResponse; +import roomescape.service.reservationtime.ReservationTimeCreateService; +import roomescape.service.reservationtime.ReservationTimeDeleteService; +import roomescape.service.reservationtime.ReservationTimeFindService; + +import java.net.URI; +import java.time.LocalDate; +import java.util.List; + +@Validated +@RestController +public class ReservationTimeApiController { + + private final ReservationTimeCreateService reservationTimeCreateService; + private final ReservationTimeFindService reservationTimeFindService; + private final ReservationTimeDeleteService reservationTimeDeleteService; + + public ReservationTimeApiController(ReservationTimeCreateService reservationTimeCreateService, + ReservationTimeFindService reservationTimeFindService, + ReservationTimeDeleteService reservationTimeDeleteService) { + this.reservationTimeCreateService = reservationTimeCreateService; + this.reservationTimeFindService = reservationTimeFindService; + this.reservationTimeDeleteService = reservationTimeDeleteService; + } + + @GetMapping("/times") + public ResponseEntity> getReservationTimes() { + List reservationTimes = reservationTimeFindService.findReservationTimes(); + return ResponseEntity.ok( + reservationTimes.stream() + .map(ReservationTimeResponse::new) + .toList() + ); + } + + @GetMapping("/times/available") + public ResponseEntity> getReservationTimesIsBooked( + @RequestParam LocalDate date, + @RequestParam @Positive(message = "1 이상의 값만 입력해주세요.") long themeId) { + ReservationStatus reservationStatus = reservationTimeFindService.findIsBooked(date, themeId); + return ResponseEntity.ok( + reservationStatus.getReservationStatus() + .keySet() + .stream() + .map(reservationTime -> new ReservationStatusResponse( + reservationTime, + reservationStatus.findReservationStatusBy(reservationTime)) + ).toList()); + } + + @PostMapping("/times") + public ResponseEntity addReservationTime(@RequestBody @Valid ReservationTimeSaveRequest request) { + ReservationTime reservationTime = reservationTimeCreateService.createReservationTime(request); + return ResponseEntity.created(URI.create("times/" + reservationTime.getId())) + .body(new ReservationTimeResponse(reservationTime)); + } + + @DeleteMapping("/times/{id}") + public ResponseEntity deleteReservationTime(@PathVariable + @Positive(message = "1 이상의 값만 입력해주세요.") long id) { + reservationTimeDeleteService.deleteReservationTime(id); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/roomescape/controller/api/ThemeApiController.java b/src/main/java/roomescape/controller/api/ThemeApiController.java new file mode 100644 index 000000000..af5ee3879 --- /dev/null +++ b/src/main/java/roomescape/controller/api/ThemeApiController.java @@ -0,0 +1,72 @@ +package roomescape.controller.api; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import roomescape.domain.Theme; +import roomescape.service.dto.request.ThemeSaveRequest; +import roomescape.service.dto.response.ThemeResponse; +import roomescape.service.theme.ThemeCreateService; +import roomescape.service.theme.ThemeDeleteService; +import roomescape.service.theme.ThemeFindService; + +import java.net.URI; +import java.util.List; + +@Validated +@RestController +public class ThemeApiController { + + private final ThemeCreateService themeCreateService; + private final ThemeFindService themeFindService; + private final ThemeDeleteService themeDeleteService; + + public ThemeApiController(ThemeCreateService themeCreateService, + ThemeFindService themeFindService, + ThemeDeleteService themeDeleteService) { + this.themeCreateService = themeCreateService; + this.themeFindService = themeFindService; + this.themeDeleteService = themeDeleteService; + } + + @GetMapping("/themes") + public ResponseEntity> getThemes() { + List themes = themeFindService.findThemes(); + return ResponseEntity.ok( + themes.stream() + .map(ThemeResponse::new) + .toList() + ); + } + + @GetMapping("/themes/ranks") + public ResponseEntity> getThemeRanks() { + List themes = themeFindService.findThemeRanks(); + return ResponseEntity.ok( + themes.stream() + .map(ThemeResponse::new) + .toList() + ); + } + + @PostMapping("/themes") + public ResponseEntity addTheme(@RequestBody @Valid ThemeSaveRequest request) { + Theme theme = themeCreateService.createTheme(request); + return ResponseEntity.created(URI.create("/themes/" + theme.getId())) + .body(new ThemeResponse(theme)); + } + + @DeleteMapping("/themes/{id}") + public ResponseEntity deleteTheme(@PathVariable + @Positive(message = "1 이상의 값만 입력해주세요.") long id) { + themeDeleteService.deleteTheme(id); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/roomescape/controller/web/AdminController.java b/src/main/java/roomescape/controller/web/AdminController.java new file mode 100644 index 000000000..226c546d2 --- /dev/null +++ b/src/main/java/roomescape/controller/web/AdminController.java @@ -0,0 +1,28 @@ +package roomescape.controller.web; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class AdminController { + + @GetMapping("/admin") + public String adminPage() { + return "admin/index"; + } + + @GetMapping("/admin/time") + public String reservationTimeAdminPage() { + return "admin/time"; + } + + @GetMapping("/admin/reservation") + public String reservationAdminPage() { + return "admin/reservation-new"; + } + + @GetMapping("/admin/theme") + public String themeAdminPage() { + return "admin/theme"; + } +} diff --git a/src/main/java/roomescape/controller/web/AuthController.java b/src/main/java/roomescape/controller/web/AuthController.java new file mode 100644 index 000000000..b0bee0cf8 --- /dev/null +++ b/src/main/java/roomescape/controller/web/AuthController.java @@ -0,0 +1,13 @@ +package roomescape.controller.web; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class AuthController { + + @GetMapping("/login") + public String loginPage() { + return "login"; + } +} diff --git a/src/main/java/roomescape/controller/web/UserController.java b/src/main/java/roomescape/controller/web/UserController.java new file mode 100644 index 000000000..0be99253c --- /dev/null +++ b/src/main/java/roomescape/controller/web/UserController.java @@ -0,0 +1,18 @@ +package roomescape.controller.web; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class UserController { + + @GetMapping("/reservation") + public String reservationUserPage() { + return "reservation"; + } + + @GetMapping + public String mainUserPage() { + return "index"; + } +} diff --git a/src/main/java/roomescape/domain/Member.java b/src/main/java/roomescape/domain/Member.java new file mode 100644 index 000000000..ecd10061b --- /dev/null +++ b/src/main/java/roomescape/domain/Member.java @@ -0,0 +1,42 @@ +package roomescape.domain; + +public class Member { + + private Long id; + private final String name; + private final String email; + private final String password; + private final Role role; + + public Member(Long id, String name, String email, String password, Role role) { + this.id = id; + this.name = name; + this.email = email; + this.password = password; + this.role = role; + } + + public Member(String name, String email, String password, Role role) { + this(null, name, email, password, role); + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getEmail() { + return email; + } + + public String getPassword() { + return password; + } + + public Role getRole() { + return role; + } +} diff --git a/src/main/java/roomescape/domain/Reservation.java b/src/main/java/roomescape/domain/Reservation.java new file mode 100644 index 000000000..76f2db1d4 --- /dev/null +++ b/src/main/java/roomescape/domain/Reservation.java @@ -0,0 +1,58 @@ +package roomescape.domain; + +import java.time.LocalDate; +import java.util.Objects; + +public class Reservation { + + private Long id; + private final Member member; + private final LocalDate date; + private final ReservationTime reservationTime; + private final Theme theme; + + public Reservation(Long id, Member member, LocalDate date, ReservationTime reservationTime, Theme theme) { + this.id = id; + this.member = member; + this.date = date; + this.reservationTime = reservationTime; + this.theme = theme; + } + + public Reservation(Member member, LocalDate date, ReservationTime reservationTime, Theme theme) { + this(null, member, date, reservationTime, theme); + } + + public Long getId() { + return id; + } + + public Member getMember() { + return member; + } + + public LocalDate getDate() { + return date; + } + + public ReservationTime getReservationTime() { + return reservationTime; + } + + public Theme getTheme() { + return theme; + } + + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (object == null || getClass() != object.getClass()) return false; + Reservation that = (Reservation) object; + return Objects.equals(id, that.id) && Objects.equals(member, that.member) && Objects.equals(date, that.date) && Objects.equals(reservationTime, that.reservationTime) && Objects.equals(theme, that.theme); + } + + @Override + public int hashCode() { + return Objects.hash(id, member, date, reservationTime, theme); + } +} diff --git a/src/main/java/roomescape/domain/ReservationStatus.java b/src/main/java/roomescape/domain/ReservationStatus.java new file mode 100644 index 000000000..f9f1ecec7 --- /dev/null +++ b/src/main/java/roomescape/domain/ReservationStatus.java @@ -0,0 +1,49 @@ +package roomescape.domain; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class ReservationStatus { + + private final Map reservationStatus; + + private ReservationStatus(Map reservationStatus) { + this.reservationStatus = reservationStatus; + } + + public static ReservationStatus of(List reservedTimes, List reservationTimes) { + Map reservationStatus = new HashMap<>(); + for (ReservationTime reservationTime : reservationTimes) { + reservationStatus.put(reservationTime, isReserved(reservedTimes, reservationTime)); + } + return new ReservationStatus(reservationStatus); + } + + private static boolean isReserved(List reservedTimes, ReservationTime reservationTime) { + return reservedTimes.stream() + .anyMatch(reservedTime -> reservedTime.equals(reservationTime)); + } + + public Boolean findReservationStatusBy(ReservationTime reservationTime) { + return reservationStatus.get(reservationTime); + } + + public Map getReservationStatus() { + return reservationStatus; + } + + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (object == null || getClass() != object.getClass()) return false; + ReservationStatus that = (ReservationStatus) object; + return Objects.equals(reservationStatus, that.reservationStatus); + } + + @Override + public int hashCode() { + return Objects.hash(reservationStatus); + } +} diff --git a/src/main/java/roomescape/domain/ReservationTime.java b/src/main/java/roomescape/domain/ReservationTime.java new file mode 100644 index 000000000..99b3e8756 --- /dev/null +++ b/src/main/java/roomescape/domain/ReservationTime.java @@ -0,0 +1,40 @@ +package roomescape.domain; + +import java.time.LocalTime; +import java.util.Objects; + +public class ReservationTime { + + private Long id; + private final LocalTime startAt; + + public ReservationTime(LocalTime startAt) { + this.startAt = startAt; + } + + public ReservationTime(Long id, LocalTime startAt) { + this.id = id; + this.startAt = startAt; + } + + public Long getId() { + return id; + } + + public LocalTime getStartAt() { + return startAt; + } + + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (object == null || getClass() != object.getClass()) return false; + ReservationTime that = (ReservationTime) object; + return Objects.equals(id, that.id) && Objects.equals(startAt, that.startAt); + } + + @Override + public int hashCode() { + return Objects.hash(id, startAt); + } +} diff --git a/src/main/java/roomescape/domain/Role.java b/src/main/java/roomescape/domain/Role.java new file mode 100644 index 000000000..e54a3097c --- /dev/null +++ b/src/main/java/roomescape/domain/Role.java @@ -0,0 +1,8 @@ +package roomescape.domain; + +public enum Role { + + ADMIN, + USER + ; +} diff --git a/src/main/java/roomescape/domain/Theme.java b/src/main/java/roomescape/domain/Theme.java new file mode 100644 index 000000000..6a410e405 --- /dev/null +++ b/src/main/java/roomescape/domain/Theme.java @@ -0,0 +1,54 @@ +package roomescape.domain; + +import java.util.Objects; + +public class Theme { + + private Long id; + private final String name; + private final String description; + private final String thumbnail; + + public Theme(Long id, String name, String description, String thumbnail) { + this.id = id; + this.name = name; + this.description = description; + this.thumbnail = thumbnail; + } + + public Theme(String name, String description, String thumbnail) { + this(null, name, description, thumbnail); + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public String getThumbnail() { + return thumbnail; + } + + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (object == null || getClass() != object.getClass()) return false; + Theme theme = (Theme) object; + return Objects.equals(id, theme.id) && + Objects.equals(name, theme.name) && + Objects.equals(description, theme.description) && + Objects.equals(thumbnail, theme.thumbnail); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, description, thumbnail); + } +} diff --git a/src/main/java/roomescape/exception/AuthenticationException.java b/src/main/java/roomescape/exception/AuthenticationException.java new file mode 100644 index 000000000..d7cc72c17 --- /dev/null +++ b/src/main/java/roomescape/exception/AuthenticationException.java @@ -0,0 +1,8 @@ +package roomescape.exception; + +public class AuthenticationException extends RuntimeException { + + public AuthenticationException(String message) { + super(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..fd9e8f509 --- /dev/null +++ b/src/main/java/roomescape/exception/GlobalExceptionHandler.java @@ -0,0 +1,46 @@ +package roomescape.exception; + +import jakarta.validation.ConstraintViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(value = IllegalArgumentException.class) + ResponseEntity handleIllegalArgumentException(IllegalArgumentException ex) { + return ResponseEntity.badRequest().body(ex.getMessage()); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) { + return new ResponseEntity<>( + ex.getBindingResult().getFieldErrors().get(0).getDefaultMessage(), + HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(ConstraintViolationException.class) + public ResponseEntity handleMethodConstraintViolationException(ConstraintViolationException ex) { + return ResponseEntity.badRequest().body(ex.getMessage()); + } + + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity handleHttpMessageNotReadableException(HttpMessageNotReadableException ex) { + return ResponseEntity.badRequest().body(ex.getMessage()); + } + + @ExceptionHandler(AuthenticationException.class) + public ResponseEntity handleAuthenticationException(AuthenticationException ex) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ex.getMessage()); + } + + @ExceptionHandler(value = RuntimeException.class) + ResponseEntity handleRuntimeException() { + return ResponseEntity.internalServerError() + .body("서버에서 예기치 못한 오류가 발생했습니다. 문제가 지속되는 경우 관리자에게 문의해주세요."); + } +} diff --git a/src/main/java/roomescape/repository/MemberRepository.java b/src/main/java/roomescape/repository/MemberRepository.java new file mode 100644 index 000000000..896b47c1b --- /dev/null +++ b/src/main/java/roomescape/repository/MemberRepository.java @@ -0,0 +1,52 @@ +package roomescape.repository; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; +import roomescape.domain.Member; +import roomescape.domain.Role; + +import java.util.List; +import java.util.Optional; + +@Repository +public class MemberRepository { + + private final JdbcTemplate jdbcTemplate; + + private final RowMapper memberRowMapper = (resultSet, rowNum) -> new Member( + resultSet.getLong("id"), + resultSet.getString("name"), + resultSet.getString("email"), + resultSet.getString("password"), + Role.valueOf(resultSet.getString("role")) + ); + + + public MemberRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public Optional findById(long id) { + String sql = "SELECT id, name, email, password, role " + + "FROM member " + + "WHERE id = ?"; + List members = jdbcTemplate.query(sql, memberRowMapper, id); + return members.isEmpty() ? Optional.empty() : Optional.of(members.get(0)); + } + + public Optional findByEmailAndPassword(String email, String password) { + String sql = "SELECT id, name, email, password, role " + + "FROM member " + + "WHERE email = ? " + + "AND password = ?"; + List members = jdbcTemplate.query(sql, memberRowMapper, email, password); + return members.isEmpty() ? Optional.empty() : Optional.of(members.get(0)); + } + + public List findAll() { + String sql = "SELECT id, name, email, password, role " + + "FROM member "; + return jdbcTemplate.query(sql, memberRowMapper); + } +} diff --git a/src/main/java/roomescape/repository/ReservationRepository.java b/src/main/java/roomescape/repository/ReservationRepository.java new file mode 100644 index 000000000..7a1b27570 --- /dev/null +++ b/src/main/java/roomescape/repository/ReservationRepository.java @@ -0,0 +1,187 @@ +package roomescape.repository; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.stereotype.Repository; +import roomescape.domain.Member; +import roomescape.domain.Reservation; +import roomescape.domain.ReservationTime; +import roomescape.domain.Role; +import roomescape.domain.Theme; + +import java.sql.Date; +import java.sql.PreparedStatement; +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +@Repository +public class ReservationRepository { + + private final JdbcTemplate jdbcTemplate; + + private final RowMapper reservationRowMapper = (resultSet, rowNum) -> new Reservation( + resultSet.getLong("id"), + new Member(resultSet.getLong("member_id"), + resultSet.getString("member_name"), + resultSet.getString("member_email"), + resultSet.getString("member_password"), + Role.valueOf(resultSet.getString("member_role"))), + resultSet.getDate("date").toLocalDate(), + new ReservationTime(resultSet.getLong("reservation_time_id"), + resultSet.getTime("time_value").toLocalTime()), + new Theme(resultSet.getLong("theme_id"), + resultSet.getString("theme_name"), + resultSet.getString("theme_description"), + resultSet.getString("theme_thumbnail") + ) + ); + + public ReservationRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public Reservation save(Reservation reservation) { + String sql = "INSERT INTO reservation " + + "(member_id, date, reservation_time_id, theme_id) " + + "VALUES (?, ?, ?, ?)"; + GeneratedKeyHolder keyHolder = new GeneratedKeyHolder(); + + jdbcTemplate.update(con -> { + PreparedStatement ps = con.prepareStatement( + sql, + new String[]{"id"}); + ps.setLong(1, reservation.getMember().getId()); + ps.setDate(2, Date.valueOf(reservation.getDate())); + ps.setLong(3, reservation.getReservationTime().getId()); + ps.setLong(4, reservation.getTheme().getId()); + return ps; + }, keyHolder); + + return new Reservation(keyHolder.getKey().longValue(), reservation.getMember(), + reservation.getDate(), reservation.getReservationTime(), reservation.getTheme()); + } + + public Optional findById(long id) { + String sql = "SELECT " + + " m.id AS member_id, " + + " m.name AS member_name, " + + " m.email AS member_email, " + + " m.password AS member_password, " + + " m.role AS member_role, " + + " r.id, " + + " r.member_id, " + + " r.date, " + + " r.reservation_time_id, " + + " r.theme_id, " + + " t.start_at AS time_value, " + + " th.id AS theme_id, " + + " th.name AS theme_name, " + + " th.description AS theme_description, " + + " th.thumbnail AS theme_thumbnail " + + "FROM reservation AS r " + + "INNER JOIN reservation_time AS t " + + "ON r.reservation_time_id = t.id " + + "INNER JOIN theme AS th " + + "ON r.theme_id = th.id " + + "INNER JOIN member AS m " + + "ON r.member_id = m.id " + + "WHERE r.id = ?"; + List reservations = jdbcTemplate.query(sql, reservationRowMapper, id); + return reservations.isEmpty() ? Optional.empty() : Optional.of(reservations.get(0)); + } + + public List searchReservations(long memberId, long themeId, LocalDate dateFrom, LocalDate dateTo) { + String sql = "SELECT " + + " m.id AS member_id, " + + " m.name AS member_name, " + + " m.email AS member_email, " + + " m.password AS member_password, " + + " m.role AS member_role, " + + " r.id, " + + " r.member_id, " + + " r.date, " + + " r.reservation_time_id, " + + " r.theme_id, " + + " t.start_at AS time_value, " + + " th.id AS theme_id, " + + " th.name AS theme_name, " + + " th.description AS theme_description, " + + " th.thumbnail AS theme_thumbnail " + + "FROM reservation AS r " + + "INNER JOIN reservation_time AS t " + + "ON r.reservation_time_id = t.id " + + "INNER JOIN theme AS th " + + "ON r.theme_id = th.id " + + "INNER JOIN member AS m " + + "ON r.member_id = m.id " + + "WHERE r.member_id = ? " + + "AND r.theme_id = ? " + + "AND r.date BETWEEN ? AND ?"; + return jdbcTemplate.query(sql, reservationRowMapper, + memberId, themeId, Date.valueOf(dateFrom), Date.valueOf(dateTo)); + } + + public List findAll() { + String sql = "SELECT " + + " m.id AS member_id, " + + " m.name AS member_name, " + + " m.email AS member_email, " + + " m.password AS member_password, " + + " m.role AS member_role, " + + " r.id, " + + " r.member_id, " + + " r.date, " + + " r.reservation_time_id, " + + " r.theme_id, " + + " t.start_at AS time_value, " + + " th.id AS theme_id, " + + " th.name AS theme_name, " + + " th.description AS theme_description, " + + " th.thumbnail AS theme_thumbnail " + + "FROM reservation AS r " + + "INNER JOIN reservation_time AS t " + + "ON r.reservation_time_id = t.id " + + "INNER JOIN theme AS th " + + "ON r.theme_id = th.id " + + "INNER JOIN member AS m " + + "ON r.member_id = m.id"; + return jdbcTemplate.query(sql, reservationRowMapper); + } + + public void deleteById(long id) { + String sql = "DELETE FROM reservation " + + "WHERE id = ?"; + jdbcTemplate.update(sql, id); + } + + public boolean existsByReservationTimeId(long reservationTimeId) { + String sql = "SELECT exists(" + + "SELECT 1 " + + "FROM reservation " + + "WHERE reservation_time_id = ?)"; + + return jdbcTemplate.queryForObject(sql, Boolean.class, reservationTimeId); + } + + public boolean existsByReservationThemeId(long themeId) { + String sql = "SELECT exists(" + + "SELECT 1 " + + "FROM reservation " + + "WHERE theme_id = ?)"; + + return jdbcTemplate.queryForObject(sql, Boolean.class, themeId); + } + + public boolean existsByDateAndTimeIdAndThemeId(LocalDate date, long reservationTimeId, long themeId) { + String sql = "SELECT exists(" + + "SELECT 1 " + + "FROM reservation " + + "WHERE date = ? " + + "AND reservation_time_id = ? " + + "AND theme_id = ?)"; + + return jdbcTemplate.queryForObject(sql, Boolean.class, Date.valueOf(date), reservationTimeId, themeId); + } +} diff --git a/src/main/java/roomescape/repository/ReservationTimeRepository.java b/src/main/java/roomescape/repository/ReservationTimeRepository.java new file mode 100644 index 000000000..339345a55 --- /dev/null +++ b/src/main/java/roomescape/repository/ReservationTimeRepository.java @@ -0,0 +1,89 @@ +package roomescape.repository; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.stereotype.Repository; +import roomescape.domain.ReservationTime; + +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.Time; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; +import java.util.Optional; + +@Repository +public class ReservationTimeRepository { + + private final JdbcTemplate jdbcTemplate; + + private final RowMapper reservationTimeRowMapper = (resultSet, rowNum) -> new ReservationTime( + resultSet.getLong("id"), + resultSet.getTime("start_at").toLocalTime() + ); + + + public ReservationTimeRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public ReservationTime save(ReservationTime time) { + String sql = "INSERT INTO reservation_time " + + "(start_at) " + + "VALUES (?)"; + GeneratedKeyHolder keyHolder = new GeneratedKeyHolder(); + + jdbcTemplate.update(con -> { + PreparedStatement ps = con.prepareStatement( + sql, + new String[]{"id"}); + ps.setTime(1, Time.valueOf(time.getStartAt())); + return ps; + }, keyHolder); + return new ReservationTime(keyHolder.getKey().longValue(), time.getStartAt()); + } + + public Optional findById(long id) { + String sql = "SELECT id, start_at " + + "FROM reservation_time " + + "WHERE id = ?"; + List reservationTimes = jdbcTemplate.query(sql, reservationTimeRowMapper, id); + return reservationTimes.isEmpty() ? Optional.empty() : Optional.of(reservationTimes.get(0)); + } + + public Optional findByStartAt(LocalTime startAt) { + String sql = "SELECT id, start_at " + + "FROM reservation_time " + + "WHERE start_at = ?"; + List reservationTimes = jdbcTemplate.query(sql, reservationTimeRowMapper, Time.valueOf(startAt)); + return reservationTimes.isEmpty() ? Optional.empty() : Optional.of(reservationTimes.get(0)); + } + + public List findReservedBy(LocalDate date, long themeId) { + String sql = "SELECT rt.id, rt.start_at " + + "FROM reservation_time AS rt " + + "INNER JOIN reservation AS r " + + "ON rt.id = r.reservation_time_id " + + "WHERE r.date = ? " + + "AND r.theme_id = ?"; + return jdbcTemplate.query(sql, reservationTimeRowMapper, Date.valueOf(date), themeId); + } + + public List findAll() { + String sql = "SELECT id, start_at " + + "FROM reservation_time"; + return jdbcTemplate.query(sql, reservationTimeRowMapper); + } + + public void deleteById(long id) { + String sql = "DELETE FROM reservation_time " + + "WHERE id = ?"; + jdbcTemplate.update(con -> { + PreparedStatement ps = con.prepareStatement(sql); + ps.setLong(1, id); + return ps; + }); + } +} diff --git a/src/main/java/roomescape/repository/ThemeRepository.java b/src/main/java/roomescape/repository/ThemeRepository.java new file mode 100644 index 000000000..45403a4a7 --- /dev/null +++ b/src/main/java/roomescape/repository/ThemeRepository.java @@ -0,0 +1,85 @@ +package roomescape.repository; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.stereotype.Repository; +import roomescape.domain.Theme; + +import java.sql.Date; +import java.sql.PreparedStatement; +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +@Repository +public class ThemeRepository { + + private final JdbcTemplate jdbcTemplate; + + private final RowMapper themeRowMapper = (resultSet, rowNum) -> new Theme( + resultSet.getLong("id"), + resultSet.getString("name"), + resultSet.getString("description"), + resultSet.getString("thumbnail") + ); + + public ThemeRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public Theme save(Theme theme) { + String sql = "INSERT INTO theme " + + "(name, description, thumbnail) " + + "VALUES (?, ? ,?)"; + GeneratedKeyHolder keyHolder = new GeneratedKeyHolder(); + + jdbcTemplate.update(con -> { + PreparedStatement ps = con.prepareStatement( + sql, + new String[]{"id"}); + ps.setString(1, theme.getName()); + ps.setString(2, theme.getDescription()); + ps.setString(3, theme.getThumbnail()); + return ps; + }, keyHolder); + return new Theme(keyHolder.getKey().longValue(), theme.getName(), + theme.getDescription(), theme.getThumbnail()); + } + + public Optional findById(long id) { + String sql = "SELECT id, name, description, thumbnail " + + "FROM theme " + + "WHERE id = ?"; + List themes = jdbcTemplate.query(sql, themeRowMapper, id); + return themes.isEmpty() ? Optional.empty() : Optional.of(themes.get(0)); + } + + public List findAll() { + String sql = "SELECT id, name, description, thumbnail " + + "FROM theme"; + return jdbcTemplate.query(sql, themeRowMapper); + } + + public List findRanksByPeriodAndCount(LocalDate start, LocalDate end, int count) { + String sql = "SELECT t.id, t.name, t.description, t.thumbnail " + + "FROM theme AS t " + + "INNER JOIN reservation AS r " + + "ON t.id = r.theme_id " + + "WHERE r.date BETWEEN ? AND ? " + + "GROUP BY t.id " + + "ORDER BY count(t.id) DESC " + + "LIMIT ?"; + return jdbcTemplate.query(sql, themeRowMapper, Date.valueOf(start), Date.valueOf(end), count); + } + + public void deleteById(long id) { + String sql = "DELETE FROM theme " + + "WHERE id = ?"; + jdbcTemplate.update(con -> { + PreparedStatement ps = con.prepareStatement(sql); + ps.setLong(1, id); + return ps; + }); + } +} diff --git a/src/main/java/roomescape/service/auth/AuthService.java b/src/main/java/roomescape/service/auth/AuthService.java new file mode 100644 index 000000000..e26ac56e3 --- /dev/null +++ b/src/main/java/roomescape/service/auth/AuthService.java @@ -0,0 +1,32 @@ +package roomescape.service.auth; + +import org.springframework.stereotype.Service; +import roomescape.domain.Member; +import roomescape.exception.AuthenticationException; +import roomescape.repository.MemberRepository; +import roomescape.service.dto.request.LoginRequest; + +@Service +public class AuthService { + + private final MemberRepository memberRepository; + private final TokenProvider tokenProvider; + + public AuthService(MemberRepository memberRepository, TokenProvider tokenProvider) { + this.memberRepository = memberRepository; + this.tokenProvider = tokenProvider; + } + + public Member findMemberByToken(String token) { + long memberId = tokenProvider.parseToken(token); + return memberRepository.findById(memberId) + .orElseThrow(() -> new AuthenticationException("잘못된 토큰 정보입니다.")); + } + + public String login(LoginRequest request) { + Member member = memberRepository.findByEmailAndPassword(request.email(), request.password()) + .orElseThrow(() -> new AuthenticationException("잘못된 로그인 정보입니다.")); + + return tokenProvider.generateAccessToken(member.getId()); + } +} diff --git a/src/main/java/roomescape/service/auth/TokenProvider.java b/src/main/java/roomescape/service/auth/TokenProvider.java new file mode 100644 index 000000000..d7854756e --- /dev/null +++ b/src/main/java/roomescape/service/auth/TokenProvider.java @@ -0,0 +1,52 @@ +package roomescape.service.auth; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import jakarta.servlet.http.Cookie; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import roomescape.exception.AuthenticationException; + +import java.security.Key; +import java.util.Arrays; + +@Component +public class TokenProvider { + + private static final String TOKEN = "token"; + + private final Key key; + + public TokenProvider(@Value("${jwt.secret}") String secretKey) { + this.key = Keys.hmacShaKeyFor(secretKey.getBytes()); + } + + public String generateAccessToken(long memberId) { + return Jwts.builder() + .setSubject(String.valueOf(memberId)) + .signWith(key) + .compact(); + } + + public Long parseToken(String token) { + if (token == "") { + throw new AuthenticationException("잘못된 토큰 정보입니다."); + } + return Long.valueOf(Jwts.parser() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + .getBody().getSubject()); + } + + public String extractTokenFromCookie(Cookie[] cookies) { + if (cookies == null) { + return ""; + } + return Arrays.stream(cookies) + .filter(cookie -> TOKEN.equals(cookie.getName())) + .map(Cookie::getValue) + .findFirst() + .orElse(""); + } +} diff --git a/src/main/java/roomescape/service/dto/request/LoginRequest.java b/src/main/java/roomescape/service/dto/request/LoginRequest.java new file mode 100644 index 000000000..1704b988a --- /dev/null +++ b/src/main/java/roomescape/service/dto/request/LoginRequest.java @@ -0,0 +1,9 @@ +package roomescape.service.dto.request; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotNull; + +public record LoginRequest(@Email(message = "잘못된 이메일 형식입니다.") + @NotNull(message = "이메일을 입력해주세요") String email, + @NotNull(message = "비밀번호를 입력해주세요") String password) { +} diff --git a/src/main/java/roomescape/service/dto/request/ReservationAdminSaveRequest.java b/src/main/java/roomescape/service/dto/request/ReservationAdminSaveRequest.java new file mode 100644 index 000000000..b1f009fca --- /dev/null +++ b/src/main/java/roomescape/service/dto/request/ReservationAdminSaveRequest.java @@ -0,0 +1,20 @@ +package roomescape.service.dto.request; + +import jakarta.validation.constraints.NotNull; +import roomescape.domain.Member; +import roomescape.domain.Reservation; +import roomescape.domain.ReservationTime; +import roomescape.domain.Theme; + +import java.time.LocalDate; + +public record ReservationAdminSaveRequest(@NotNull(message = "멤버를 입력해주세요") Long memberId, + @NotNull(message = "예약 날짜를 입력해주세요.") LocalDate date, + @NotNull(message = "예약 시간을 입력해주세요.") Long timeId, + @NotNull(message = "예약 테마를 입력해주세요.") Long themeId) { + + public Reservation toEntity(ReservationAdminSaveRequest request, ReservationTime reservationTime, + Theme theme, Member member) { + return new Reservation(member, request.date(), reservationTime, theme); + } +} diff --git a/src/main/java/roomescape/service/dto/request/ReservationSaveRequest.java b/src/main/java/roomescape/service/dto/request/ReservationSaveRequest.java new file mode 100644 index 000000000..7f29e9351 --- /dev/null +++ b/src/main/java/roomescape/service/dto/request/ReservationSaveRequest.java @@ -0,0 +1,19 @@ +package roomescape.service.dto.request; + +import jakarta.validation.constraints.NotNull; +import roomescape.domain.Member; +import roomescape.domain.Reservation; +import roomescape.domain.ReservationTime; +import roomescape.domain.Theme; + +import java.time.LocalDate; + +public record ReservationSaveRequest(@NotNull(message = "예약 날짜를 입력해주세요.") LocalDate date, + @NotNull(message = "예약 시간을 입력해주세요.") Long timeId, + @NotNull(message = "예약 테마를 입력해주세요.") Long themeId) { + + public Reservation toEntity(ReservationSaveRequest request, ReservationTime reservationTime, + Theme theme, Member member) { + return new Reservation(member, request.date(), reservationTime, theme); + } +} diff --git a/src/main/java/roomescape/service/dto/request/ReservationTimeSaveRequest.java b/src/main/java/roomescape/service/dto/request/ReservationTimeSaveRequest.java new file mode 100644 index 000000000..3cd620603 --- /dev/null +++ b/src/main/java/roomescape/service/dto/request/ReservationTimeSaveRequest.java @@ -0,0 +1,13 @@ +package roomescape.service.dto.request; + +import jakarta.validation.constraints.NotNull; +import roomescape.domain.ReservationTime; + +import java.time.LocalTime; + +public record ReservationTimeSaveRequest(@NotNull(message = "예약 시간을 입력해주세요.") LocalTime startAt) { + + public ReservationTime toEntity(ReservationTimeSaveRequest request) { + return new ReservationTime(request.startAt()); + } +} diff --git a/src/main/java/roomescape/service/dto/request/ThemeSaveRequest.java b/src/main/java/roomescape/service/dto/request/ThemeSaveRequest.java new file mode 100644 index 000000000..f714c3a23 --- /dev/null +++ b/src/main/java/roomescape/service/dto/request/ThemeSaveRequest.java @@ -0,0 +1,14 @@ +package roomescape.service.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import roomescape.domain.Theme; + +public record ThemeSaveRequest(@NotBlank(message = "테마 이름을 입력해주세요.") String name, + @NotNull(message = "테마 이름을 입력해주세요.") String description, + @NotNull(message = "테마 썸네일을 입력해주세요.") String thumbnail) { + + public Theme toEntity(ThemeSaveRequest request) { + return new Theme(request.name(), request.description(), request.thumbnail()); + } +} diff --git a/src/main/java/roomescape/service/dto/response/MemberIdAndNameResponse.java b/src/main/java/roomescape/service/dto/response/MemberIdAndNameResponse.java new file mode 100644 index 000000000..a9f3db2e2 --- /dev/null +++ b/src/main/java/roomescape/service/dto/response/MemberIdAndNameResponse.java @@ -0,0 +1,4 @@ +package roomescape.service.dto.response; + +public record MemberIdAndNameResponse(Long id, String name) { +} diff --git a/src/main/java/roomescape/service/dto/response/ReservationResponse.java b/src/main/java/roomescape/service/dto/response/ReservationResponse.java new file mode 100644 index 000000000..a28a5fd66 --- /dev/null +++ b/src/main/java/roomescape/service/dto/response/ReservationResponse.java @@ -0,0 +1,19 @@ +package roomescape.service.dto.response; + +import roomescape.domain.Reservation; + +import java.time.LocalDate; + +public record ReservationResponse(Long id, MemberIdAndNameResponse member, LocalDate date, + ReservationTimeResponse time, + ThemeResponse theme) { + + public ReservationResponse(Reservation reservation) { + this(reservation.getId(), + new MemberIdAndNameResponse(reservation.getMember().getId(), reservation.getMember().getName()), + reservation.getDate(), + new ReservationTimeResponse(reservation.getReservationTime()), + new ThemeResponse(reservation.getTheme())); + + } +} diff --git a/src/main/java/roomescape/service/dto/response/ReservationStatusResponse.java b/src/main/java/roomescape/service/dto/response/ReservationStatusResponse.java new file mode 100644 index 000000000..d8a9f9b87 --- /dev/null +++ b/src/main/java/roomescape/service/dto/response/ReservationStatusResponse.java @@ -0,0 +1,12 @@ +package roomescape.service.dto.response; + +import roomescape.domain.ReservationTime; + +import java.time.LocalTime; + +public record ReservationStatusResponse(LocalTime startAt, Long timeId, boolean alreadyBooked) { + + public ReservationStatusResponse(ReservationTime reservationTime, boolean alreadyBooked) { + this(reservationTime.getStartAt(), reservationTime.getId(), alreadyBooked); + } +} diff --git a/src/main/java/roomescape/service/dto/response/ReservationTimeResponse.java b/src/main/java/roomescape/service/dto/response/ReservationTimeResponse.java new file mode 100644 index 000000000..4d6d29469 --- /dev/null +++ b/src/main/java/roomescape/service/dto/response/ReservationTimeResponse.java @@ -0,0 +1,13 @@ +package roomescape.service.dto.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import roomescape.domain.ReservationTime; + +import java.time.LocalTime; + +public record ReservationTimeResponse(Long id, @JsonFormat(pattern = "HH:mm") LocalTime startAt) { + + public ReservationTimeResponse(ReservationTime reservationTime) { + this(reservationTime.getId(), reservationTime.getStartAt()); + } +} diff --git a/src/main/java/roomescape/service/dto/response/ThemeResponse.java b/src/main/java/roomescape/service/dto/response/ThemeResponse.java new file mode 100644 index 000000000..35a7c2d9c --- /dev/null +++ b/src/main/java/roomescape/service/dto/response/ThemeResponse.java @@ -0,0 +1,10 @@ +package roomescape.service.dto.response; + +import roomescape.domain.Theme; + +public record ThemeResponse(Long id, String name, String description, String thumbnail) { + + public ThemeResponse(Theme theme) { + this(theme.getId(), theme.getName(), theme.getDescription(), theme.getThumbnail()); + } +} diff --git a/src/main/java/roomescape/service/member/MemberService.java b/src/main/java/roomescape/service/member/MemberService.java new file mode 100644 index 000000000..aa23ee6fd --- /dev/null +++ b/src/main/java/roomescape/service/member/MemberService.java @@ -0,0 +1,21 @@ +package roomescape.service.member; + +import org.springframework.stereotype.Service; +import roomescape.domain.Member; +import roomescape.repository.MemberRepository; + +import java.util.List; + +@Service +public class MemberService { + + private final MemberRepository memberRepository; + + public MemberService(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + public List findMembers() { + return memberRepository.findAll(); + } +} diff --git a/src/main/java/roomescape/service/reservation/AdminReservationCreateService.java b/src/main/java/roomescape/service/reservation/AdminReservationCreateService.java new file mode 100644 index 000000000..835b3e72a --- /dev/null +++ b/src/main/java/roomescape/service/reservation/AdminReservationCreateService.java @@ -0,0 +1,32 @@ +package roomescape.service.reservation; + +import org.springframework.stereotype.Service; +import roomescape.domain.Member; +import roomescape.domain.Reservation; +import roomescape.domain.ReservationTime; +import roomescape.domain.Theme; +import roomescape.repository.ReservationRepository; +import roomescape.service.dto.request.ReservationAdminSaveRequest; + +@Service +public class AdminReservationCreateService { + + private final ReservationRepository reservationRepository; + private final ReservationCreateValidator reservationCreateValidator; + + public AdminReservationCreateService(ReservationRepository reservationRepository, ReservationCreateValidator reservationCreateValidator) { + this.reservationRepository = reservationRepository; + this.reservationCreateValidator = reservationCreateValidator; + } + + public Reservation createReservation(ReservationAdminSaveRequest request) { + ReservationTime reservationTime = reservationCreateValidator.getValidReservationTime(request.timeId()); + reservationCreateValidator.validateDateIsFuture(request.date(), reservationTime); + Theme theme = reservationCreateValidator.getValidTheme(request.themeId()); + reservationCreateValidator.validateAlreadyBooked(request.date(), request.timeId(), request.themeId()); + Member member = reservationCreateValidator.getValidMember(request.memberId()); + + Reservation reservation = request.toEntity(request, reservationTime, theme, member); + return reservationRepository.save(reservation); + } +} diff --git a/src/main/java/roomescape/service/reservation/ReservationCreateService.java b/src/main/java/roomescape/service/reservation/ReservationCreateService.java new file mode 100644 index 000000000..7aaec14ef --- /dev/null +++ b/src/main/java/roomescape/service/reservation/ReservationCreateService.java @@ -0,0 +1,32 @@ +package roomescape.service.reservation; + +import org.springframework.stereotype.Service; +import roomescape.domain.Member; +import roomescape.domain.Reservation; +import roomescape.domain.ReservationTime; +import roomescape.domain.Theme; +import roomescape.repository.ReservationRepository; +import roomescape.service.dto.request.ReservationSaveRequest; + +@Service +public class ReservationCreateService { + + private final ReservationCreateValidator reservationCreateValidator; + private final ReservationRepository reservationRepository; + + public ReservationCreateService(ReservationCreateValidator reservationCreateValidator, + ReservationRepository reservationRepository) { + this.reservationCreateValidator = reservationCreateValidator; + this.reservationRepository = reservationRepository; + } + + public Reservation createReservation(ReservationSaveRequest request, Member member) { + ReservationTime reservationTime = reservationCreateValidator.getValidReservationTime(request.timeId()); + reservationCreateValidator.validateDateIsFuture(request.date(), reservationTime); + Theme theme = reservationCreateValidator.getValidTheme(request.themeId()); + reservationCreateValidator.validateAlreadyBooked(request.date(), request.timeId(), request.themeId()); + + Reservation reservation = request.toEntity(request, reservationTime, theme, member); + return reservationRepository.save(reservation); + } +} diff --git a/src/main/java/roomescape/service/reservation/ReservationCreateValidator.java b/src/main/java/roomescape/service/reservation/ReservationCreateValidator.java new file mode 100644 index 000000000..624f0ec21 --- /dev/null +++ b/src/main/java/roomescape/service/reservation/ReservationCreateValidator.java @@ -0,0 +1,65 @@ +package roomescape.service.reservation; + +import org.springframework.stereotype.Component; +import roomescape.domain.Member; +import roomescape.domain.ReservationTime; +import roomescape.domain.Theme; +import roomescape.repository.MemberRepository; +import roomescape.repository.ReservationRepository; +import roomescape.repository.ReservationTimeRepository; +import roomescape.repository.ThemeRepository; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Component +public class ReservationCreateValidator { + + private final ReservationRepository reservationRepository; + private final ReservationTimeRepository reservationTimeRepository; + private final ThemeRepository themeRepository; + private final MemberRepository memberRepository; + + public ReservationCreateValidator(ReservationRepository reservationRepository, + ReservationTimeRepository reservationTimeRepository, + ThemeRepository themeRepository, + MemberRepository memberRepository) { + this.reservationRepository = reservationRepository; + this.reservationTimeRepository = reservationTimeRepository; + this.themeRepository = themeRepository; + this.memberRepository = memberRepository; + } + + + public ReservationTime getValidReservationTime(long reservationId) { + return reservationTimeRepository.findById(reservationId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 예약 시간 입니다.")); + } + + public Theme getValidTheme(long themeId) { + return themeRepository.findById(themeId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 테마 입니다.")); + } + + public void validateAlreadyBooked(LocalDate date, long timeId, long themeId) { + if (reservationRepository.existsByDateAndTimeIdAndThemeId(date, timeId, themeId)) { + throw new IllegalArgumentException("해당 시간에 이미 예약된 테마입니다."); + } + } + + public void validateDateIsFuture(LocalDate date, ReservationTime reservationTime) { + LocalDateTime localDateTime = toLocalDateTime(date, reservationTime); + if (localDateTime.isBefore(LocalDateTime.now())) { + throw new IllegalArgumentException("지나간 날짜와 시간에 대한 예약 생성은 불가능합니다."); + } + } + + private LocalDateTime toLocalDateTime(LocalDate date, ReservationTime reservationTime) { + return LocalDateTime.of(date, reservationTime.getStartAt()); + } + + public Member getValidMember(long memberId) { + return memberRepository.findById(memberId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다.")); + } +} diff --git a/src/main/java/roomescape/service/reservation/ReservationDeleteService.java b/src/main/java/roomescape/service/reservation/ReservationDeleteService.java new file mode 100644 index 000000000..ffbf79b5c --- /dev/null +++ b/src/main/java/roomescape/service/reservation/ReservationDeleteService.java @@ -0,0 +1,20 @@ +package roomescape.service.reservation; + +import org.springframework.stereotype.Service; +import roomescape.repository.ReservationRepository; + +@Service +public class ReservationDeleteService { + + private final ReservationRepository reservationRepository; + + public ReservationDeleteService(ReservationRepository reservationRepository) { + this.reservationRepository = reservationRepository; + } + + public void deleteReservation(long id) { + reservationRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 예약 아이디 입니다.")); + reservationRepository.deleteById(id); + } +} diff --git a/src/main/java/roomescape/service/reservation/ReservationFindService.java b/src/main/java/roomescape/service/reservation/ReservationFindService.java new file mode 100644 index 000000000..ac2319339 --- /dev/null +++ b/src/main/java/roomescape/service/reservation/ReservationFindService.java @@ -0,0 +1,27 @@ +package roomescape.service.reservation; + +import org.springframework.stereotype.Service; +import roomescape.domain.Reservation; +import roomescape.repository.ReservationRepository; + +import java.time.LocalDate; +import java.util.List; + +@Service +public class ReservationFindService { + + private final ReservationRepository reservationRepository; + + public ReservationFindService(ReservationRepository reservationRepository) { + this.reservationRepository = reservationRepository; + } + + public List findReservations() { + return reservationRepository.findAll(); + } + + public List searchReservations(long memberId, long themeId, + LocalDate dateFrom, LocalDate dateTo) { + return reservationRepository.searchReservations(memberId, themeId, dateFrom, dateTo); + } +} diff --git a/src/main/java/roomescape/service/reservationtime/ReservationTimeCreateService.java b/src/main/java/roomescape/service/reservationtime/ReservationTimeCreateService.java new file mode 100644 index 000000000..7335f3868 --- /dev/null +++ b/src/main/java/roomescape/service/reservationtime/ReservationTimeCreateService.java @@ -0,0 +1,25 @@ +package roomescape.service.reservationtime; + +import org.springframework.stereotype.Service; +import roomescape.domain.ReservationTime; +import roomescape.repository.ReservationTimeRepository; +import roomescape.service.dto.request.ReservationTimeSaveRequest; + +@Service +public class ReservationTimeCreateService { + + private final ReservationTimeRepository reservationTimeRepository; + + public ReservationTimeCreateService(ReservationTimeRepository reservationTimeRepository) { + this.reservationTimeRepository = reservationTimeRepository; + } + + public ReservationTime createReservationTime(ReservationTimeSaveRequest request) { + if (reservationTimeRepository.findByStartAt(request.startAt()).isPresent()) { + throw new IllegalArgumentException("이미 존재하는 예약 시간입니다."); + } + + ReservationTime newReservationTime = request.toEntity(request); + return reservationTimeRepository.save(newReservationTime); + } +} diff --git a/src/main/java/roomescape/service/reservationtime/ReservationTimeDeleteService.java b/src/main/java/roomescape/service/reservationtime/ReservationTimeDeleteService.java new file mode 100644 index 000000000..229162098 --- /dev/null +++ b/src/main/java/roomescape/service/reservationtime/ReservationTimeDeleteService.java @@ -0,0 +1,28 @@ +package roomescape.service.reservationtime; + +import org.springframework.stereotype.Service; +import roomescape.repository.ReservationRepository; +import roomescape.repository.ReservationTimeRepository; + +@Service +public class ReservationTimeDeleteService { + + private final ReservationTimeRepository reservationTimeRepository; + private final ReservationRepository reservationRepository; + + public ReservationTimeDeleteService(ReservationTimeRepository reservationTimeRepository, + ReservationRepository reservationRepository) { + this.reservationTimeRepository = reservationTimeRepository; + this.reservationRepository = reservationRepository; + } + + public void deleteReservationTime(long id) { + reservationTimeRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 예약 시간 아이디 입니다.")); + + if (reservationRepository.existsByReservationTimeId(id)) { + throw new IllegalArgumentException("이미 예약중인 시간은 삭제할 수 없습니다."); + } + reservationTimeRepository.deleteById(id); + } +} diff --git a/src/main/java/roomescape/service/reservationtime/ReservationTimeFindService.java b/src/main/java/roomescape/service/reservationtime/ReservationTimeFindService.java new file mode 100644 index 000000000..4419d8a0d --- /dev/null +++ b/src/main/java/roomescape/service/reservationtime/ReservationTimeFindService.java @@ -0,0 +1,29 @@ +package roomescape.service.reservationtime; + +import org.springframework.stereotype.Service; +import roomescape.domain.ReservationStatus; +import roomescape.domain.ReservationTime; +import roomescape.repository.ReservationTimeRepository; + +import java.time.LocalDate; +import java.util.List; + +@Service +public class ReservationTimeFindService { + + private final ReservationTimeRepository reservationTimeRepository; + + public ReservationTimeFindService(ReservationTimeRepository reservationTimeRepository) { + this.reservationTimeRepository = reservationTimeRepository; + } + + public List findReservationTimes() { + return reservationTimeRepository.findAll(); + } + + public ReservationStatus findIsBooked(LocalDate date, long themeId) { + List reservedTimes = reservationTimeRepository.findReservedBy(date, themeId); + List reservationTimes = reservationTimeRepository.findAll(); + return ReservationStatus.of(reservedTimes, reservationTimes); + } +} diff --git a/src/main/java/roomescape/service/theme/ThemeCreateService.java b/src/main/java/roomescape/service/theme/ThemeCreateService.java new file mode 100644 index 000000000..abe30291f --- /dev/null +++ b/src/main/java/roomescape/service/theme/ThemeCreateService.java @@ -0,0 +1,21 @@ +package roomescape.service.theme; + +import org.springframework.stereotype.Service; +import roomescape.domain.Theme; +import roomescape.repository.ThemeRepository; +import roomescape.service.dto.request.ThemeSaveRequest; + +@Service +public class ThemeCreateService { + + private final ThemeRepository themeRepository; + + public ThemeCreateService(ThemeRepository themeRepository) { + this.themeRepository = themeRepository; + } + + public Theme createTheme(ThemeSaveRequest request) { + Theme theme = request.toEntity(request); + return themeRepository.save(theme); + } +} diff --git a/src/main/java/roomescape/service/theme/ThemeDeleteService.java b/src/main/java/roomescape/service/theme/ThemeDeleteService.java new file mode 100644 index 000000000..17f0bf771 --- /dev/null +++ b/src/main/java/roomescape/service/theme/ThemeDeleteService.java @@ -0,0 +1,29 @@ +package roomescape.service.theme; + +import org.springframework.stereotype.Service; +import roomescape.repository.ReservationRepository; +import roomescape.repository.ThemeRepository; + +@Service +public class ThemeDeleteService { + + private final ThemeRepository themeRepository; + private final ReservationRepository reservationRepository; + + public ThemeDeleteService(ThemeRepository themeRepository, + ReservationRepository reservationRepository) { + this.themeRepository = themeRepository; + this.reservationRepository = reservationRepository; + } + + public void deleteTheme(long id) { + themeRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 테마 아이디 입니다.")); + + if (reservationRepository.existsByReservationThemeId(id)) { + throw new IllegalArgumentException("이미 예약중인 테마는 삭제할 수 없습니다."); + } + + themeRepository.deleteById(id); + } +} diff --git a/src/main/java/roomescape/service/theme/ThemeFindService.java b/src/main/java/roomescape/service/theme/ThemeFindService.java new file mode 100644 index 000000000..f77b10fc2 --- /dev/null +++ b/src/main/java/roomescape/service/theme/ThemeFindService.java @@ -0,0 +1,32 @@ +package roomescape.service.theme; + +import org.springframework.stereotype.Service; +import roomescape.domain.Theme; +import roomescape.repository.ThemeRepository; + +import java.time.LocalDate; +import java.util.List; + +@Service +public class ThemeFindService { + private static final int START_DAYS_SUBTRACT = 7; + private static final int END_DAYS_SUBTRACT = 1; + private static final int RANK_COUNT = 7; + + private final ThemeRepository themeRepository; + + public ThemeFindService(ThemeRepository themeRepository) { + this.themeRepository = themeRepository; + } + + public List findThemes() { + return themeRepository.findAll(); + } + + public List findThemeRanks() { + return themeRepository.findRanksByPeriodAndCount( + LocalDate.now().minusDays(START_DAYS_SUBTRACT), + LocalDate.now().minusDays(END_DAYS_SUBTRACT), + RANK_COUNT); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 000000000..b70507cf1 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,10 @@ +spring: + h2: + console: + enabled: true + path: /h2-consoles + datasource: + url: jdbc:h2:mem:database + +jwt: + secret: Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E= diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql new file mode 100644 index 000000000..5b32770c0 --- /dev/null +++ b/src/main/resources/data.sql @@ -0,0 +1,18 @@ +INSERT INTO theme (id, name, description, thumbnail) +VALUES (1, 'theme1', 'description1', 'thumbnail1'); +INSERT INTO theme (id, name, description, thumbnail) +VALUES (2, 'theme2', 'description2', 'thumbnail2'); + +INSERT INTO reservation_time (id, start_at) +VALUES (1, '10:00'); +INSERT INTO reservation_time (id, start_at) +VALUES (2, '11:00'); + +INSERT INTO member +VALUES (1, 'testUser', 'test@naver.com', '1234', 'USER'); +INSERT INTO member +VALUES (2, 'testAdmin', 'admin@naver.com', '1234', 'ADMIN'); + +INSERT INTO reservation (member_id, date, reservation_time_id, theme_Id) +VALUES (1, CURRENT_DATE + INTERVAL '1' DAY, 1, 1); + diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 000000000..45d3dd909 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,38 @@ +CREATE TABLE reservation_time +( + id BIGINT NOT NULL AUTO_INCREMENT, + start_at TIME NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE theme +( + id BIGINT NOT NULL AUTO_INCREMENT, + name VARCHAR(255) NOT NULL, + description VARCHAR(255) NOT NULL, + thumbnail VARCHAR(255) NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE reservation +( + id BIGINT NOT NULL AUTO_INCREMENT, + member_id BIGINT, + date DATE NOT NULL, + reservation_time_id BIGINT, + theme_id BIGINT, + UNIQUE (date, reservation_time_id, theme_id), + PRIMARY KEY (id), + FOREIGN KEY (reservation_time_id) REFERENCES reservation_time (id), + FOREIGN KEY (theme_id) REFERENCES theme (id) +); + +CREATE TABLE member +( + id BIGINT NOT NULL AUTO_INCREMENT, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + role VARCHAR(255), + PRIMARY KEY (id) +); diff --git a/src/main/resources/static/css/flatpickr.css b/src/main/resources/static/css/flatpickr.css new file mode 100644 index 000000000..6f99fcf00 --- /dev/null +++ b/src/main/resources/static/css/flatpickr.css @@ -0,0 +1,795 @@ +.flatpickr-calendar { + background: transparent; + opacity: 0; + display: none; + text-align: center; + visibility: hidden; + padding: 0; + -webkit-animation: none; + animation: none; + direction: ltr; + border: 0; + font-size: 14px; + line-height: 24px; + border-radius: 5px; + position: absolute; + width: 307.875px; + -webkit-box-sizing: border-box; + box-sizing: border-box; + -ms-touch-action: manipulation; + touch-action: manipulation; + background: #fff; + -webkit-box-shadow: 1px 0 0 #e6e6e6, -1px 0 0 #e6e6e6, 0 1px 0 #e6e6e6, 0 -1px 0 #e6e6e6, 0 3px 13px rgba(0,0,0,0.08); + box-shadow: 1px 0 0 #e6e6e6, -1px 0 0 #e6e6e6, 0 1px 0 #e6e6e6, 0 -1px 0 #e6e6e6, 0 3px 13px rgba(0,0,0,0.08); +} +.flatpickr-calendar.open, +.flatpickr-calendar.inline { + opacity: 1; + max-height: 640px; + visibility: visible; +} +.flatpickr-calendar.open { + display: inline-block; + z-index: 99999; +} +.flatpickr-calendar.animate.open { + -webkit-animation: fpFadeInDown 300ms cubic-bezier(0.23, 1, 0.32, 1); + animation: fpFadeInDown 300ms cubic-bezier(0.23, 1, 0.32, 1); +} +.flatpickr-calendar.inline { + display: block; + position: relative; + top: 2px; +} +.flatpickr-calendar.static { + position: absolute; + top: calc(100% + 2px); +} +.flatpickr-calendar.static.open { + z-index: 999; + display: block; +} +.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+1) .flatpickr-day.inRange:nth-child(7n+7) { + -webkit-box-shadow: none !important; + box-shadow: none !important; +} +.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+2) .flatpickr-day.inRange:nth-child(7n+1) { + -webkit-box-shadow: -2px 0 0 #e6e6e6, 5px 0 0 #e6e6e6; + box-shadow: -2px 0 0 #e6e6e6, 5px 0 0 #e6e6e6; +} +.flatpickr-calendar .hasWeeks .dayContainer, +.flatpickr-calendar .hasTime .dayContainer { + border-bottom: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.flatpickr-calendar .hasWeeks .dayContainer { + border-left: 0; +} +.flatpickr-calendar.hasTime .flatpickr-time { + height: 40px; + border-top: 1px solid #e6e6e6; +} +.flatpickr-calendar.noCalendar.hasTime .flatpickr-time { + height: auto; +} +.flatpickr-calendar:before, +.flatpickr-calendar:after { + position: absolute; + display: block; + pointer-events: none; + border: solid transparent; + content: ''; + height: 0; + width: 0; + left: 22px; +} +.flatpickr-calendar.rightMost:before, +.flatpickr-calendar.arrowRight:before, +.flatpickr-calendar.rightMost:after, +.flatpickr-calendar.arrowRight:after { + left: auto; + right: 22px; +} +.flatpickr-calendar.arrowCenter:before, +.flatpickr-calendar.arrowCenter:after { + left: 50%; + right: 50%; +} +.flatpickr-calendar:before { + border-width: 5px; + margin: 0 -5px; +} +.flatpickr-calendar:after { + border-width: 4px; + margin: 0 -4px; +} +.flatpickr-calendar.arrowTop:before, +.flatpickr-calendar.arrowTop:after { + bottom: 100%; +} +.flatpickr-calendar.arrowTop:before { + border-bottom-color: #e6e6e6; +} +.flatpickr-calendar.arrowTop:after { + border-bottom-color: #fff; +} +.flatpickr-calendar.arrowBottom:before, +.flatpickr-calendar.arrowBottom:after { + top: 100%; +} +.flatpickr-calendar.arrowBottom:before { + border-top-color: #e6e6e6; +} +.flatpickr-calendar.arrowBottom:after { + border-top-color: #fff; +} +.flatpickr-calendar:focus { + outline: 0; +} +.flatpickr-wrapper { + position: relative; + display: inline-block; +} +.flatpickr-months { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} +.flatpickr-months .flatpickr-month { + background: transparent; + color: rgba(0,0,0,0.9); + fill: rgba(0,0,0,0.9); + height: 34px; + line-height: 1; + text-align: center; + position: relative; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + overflow: hidden; + -webkit-box-flex: 1; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; +} +.flatpickr-months .flatpickr-prev-month, +.flatpickr-months .flatpickr-next-month { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + text-decoration: none; + cursor: pointer; + position: absolute; + top: 0; + height: 34px; + padding: 10px; + z-index: 3; + color: rgba(0,0,0,0.9); + fill: rgba(0,0,0,0.9); +} +.flatpickr-months .flatpickr-prev-month.flatpickr-disabled, +.flatpickr-months .flatpickr-next-month.flatpickr-disabled { + display: none; +} +.flatpickr-months .flatpickr-prev-month i, +.flatpickr-months .flatpickr-next-month i { + position: relative; +} +.flatpickr-months .flatpickr-prev-month.flatpickr-prev-month, +.flatpickr-months .flatpickr-next-month.flatpickr-prev-month { +/* + /*rtl:begin:ignore*/ +/* + */ + left: 0; +/* + /*rtl:end:ignore*/ +/* + */ +} +/* + /*rtl:begin:ignore*/ +/* + /*rtl:end:ignore*/ +.flatpickr-months .flatpickr-prev-month.flatpickr-next-month, +.flatpickr-months .flatpickr-next-month.flatpickr-next-month { +/* + /*rtl:begin:ignore*/ +/* + */ + right: 0; +/* + /*rtl:end:ignore*/ +/* + */ +} +/* + /*rtl:begin:ignore*/ +/* + /*rtl:end:ignore*/ +.flatpickr-months .flatpickr-prev-month:hover, +.flatpickr-months .flatpickr-next-month:hover { + color: #959ea9; +} +.flatpickr-months .flatpickr-prev-month:hover svg, +.flatpickr-months .flatpickr-next-month:hover svg { + fill: #f64747; +} +.flatpickr-months .flatpickr-prev-month svg, +.flatpickr-months .flatpickr-next-month svg { + width: 14px; + height: 14px; +} +.flatpickr-months .flatpickr-prev-month svg path, +.flatpickr-months .flatpickr-next-month svg path { + -webkit-transition: fill 0.1s; + transition: fill 0.1s; + fill: inherit; +} +.numInputWrapper { + position: relative; + height: auto; +} +.numInputWrapper input, +.numInputWrapper span { + display: inline-block; +} +.numInputWrapper input { + width: 100%; +} +.numInputWrapper input::-ms-clear { + display: none; +} +.numInputWrapper input::-webkit-outer-spin-button, +.numInputWrapper input::-webkit-inner-spin-button { + margin: 0; + -webkit-appearance: none; +} +.numInputWrapper span { + position: absolute; + right: 0; + width: 14px; + padding: 0 4px 0 2px; + height: 50%; + line-height: 50%; + opacity: 0; + cursor: pointer; + border: 1px solid rgba(57,57,57,0.15); + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +.numInputWrapper span:hover { + background: rgba(0,0,0,0.1); +} +.numInputWrapper span:active { + background: rgba(0,0,0,0.2); +} +.numInputWrapper span:after { + display: block; + content: ""; + position: absolute; +} +.numInputWrapper span.arrowUp { + top: 0; + border-bottom: 0; +} +.numInputWrapper span.arrowUp:after { + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-bottom: 4px solid rgba(57,57,57,0.6); + top: 26%; +} +.numInputWrapper span.arrowDown { + top: 50%; +} +.numInputWrapper span.arrowDown:after { + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid rgba(57,57,57,0.6); + top: 40%; +} +.numInputWrapper span svg { + width: inherit; + height: auto; +} +.numInputWrapper span svg path { + fill: rgba(0,0,0,0.5); +} +.numInputWrapper:hover { + background: rgba(0,0,0,0.05); +} +.numInputWrapper:hover span { + opacity: 1; +} +.flatpickr-current-month { + font-size: 135%; + line-height: inherit; + font-weight: 300; + color: inherit; + position: absolute; + width: 75%; + left: 12.5%; + padding: 7.48px 0 0 0; + line-height: 1; + height: 34px; + display: inline-block; + text-align: center; + -webkit-transform: translate3d(0px, 0px, 0px); + transform: translate3d(0px, 0px, 0px); +} +.flatpickr-current-month span.cur-month { + font-family: inherit; + font-weight: 700; + color: inherit; + display: inline-block; + margin-left: 0.5ch; + padding: 0; +} +.flatpickr-current-month span.cur-month:hover { + background: rgba(0,0,0,0.05); +} +.flatpickr-current-month .numInputWrapper { + width: 6ch; + width: 7ch\0; + display: inline-block; +} +.flatpickr-current-month .numInputWrapper span.arrowUp:after { + border-bottom-color: rgba(0,0,0,0.9); +} +.flatpickr-current-month .numInputWrapper span.arrowDown:after { + border-top-color: rgba(0,0,0,0.9); +} +.flatpickr-current-month input.cur-year { + background: transparent; + -webkit-box-sizing: border-box; + box-sizing: border-box; + color: inherit; + cursor: text; + padding: 0 0 0 0.5ch; + margin: 0; + display: inline-block; + font-size: inherit; + font-family: inherit; + font-weight: 300; + line-height: inherit; + height: auto; + border: 0; + border-radius: 0; + vertical-align: initial; + -webkit-appearance: textfield; + -moz-appearance: textfield; + appearance: textfield; +} +.flatpickr-current-month input.cur-year:focus { + outline: 0; +} +.flatpickr-current-month input.cur-year[disabled], +.flatpickr-current-month input.cur-year[disabled]:hover { + font-size: 100%; + color: rgba(0,0,0,0.5); + background: transparent; + pointer-events: none; +} +.flatpickr-current-month .flatpickr-monthDropdown-months { + appearance: menulist; + background: transparent; + border: none; + border-radius: 0; + box-sizing: border-box; + color: inherit; + cursor: pointer; + font-size: inherit; + font-family: inherit; + font-weight: 300; + height: auto; + line-height: inherit; + margin: -1px 0 0 0; + outline: none; + padding: 0 0 0 0.5ch; + position: relative; + vertical-align: initial; + -webkit-box-sizing: border-box; + -webkit-appearance: menulist; + -moz-appearance: menulist; + width: auto; +} +.flatpickr-current-month .flatpickr-monthDropdown-months:focus, +.flatpickr-current-month .flatpickr-monthDropdown-months:active { + outline: none; +} +.flatpickr-current-month .flatpickr-monthDropdown-months:hover { + background: rgba(0,0,0,0.05); +} +.flatpickr-current-month .flatpickr-monthDropdown-months .flatpickr-monthDropdown-month { + background-color: transparent; + outline: none; + padding: 0; +} +.flatpickr-weekdays { + background: transparent; + text-align: center; + overflow: hidden; + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; + height: 28px; +} +.flatpickr-weekdays .flatpickr-weekdaycontainer { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 1; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; +} +span.flatpickr-weekday { + cursor: default; + font-size: 90%; + background: transparent; + color: rgba(0,0,0,0.54); + line-height: 1; + margin: 0; + text-align: center; + display: block; + -webkit-box-flex: 1; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + font-weight: bolder; +} +.dayContainer, +.flatpickr-weeks { + padding: 1px 0 0 0; +} +.flatpickr-days { + position: relative; + overflow: hidden; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-align: start; + -webkit-align-items: flex-start; + -ms-flex-align: start; + align-items: flex-start; + width: 307.875px; +} +.flatpickr-days:focus { + outline: 0; +} +.dayContainer { + padding: 0; + outline: 0; + text-align: left; + width: 307.875px; + min-width: 307.875px; + max-width: 307.875px; + -webkit-box-sizing: border-box; + box-sizing: border-box; + display: inline-block; + display: -ms-flexbox; + display: -webkit-box; + display: -webkit-flex; + display: flex; + -webkit-flex-wrap: wrap; + flex-wrap: wrap; + -ms-flex-wrap: wrap; + -ms-flex-pack: justify; + -webkit-justify-content: space-around; + justify-content: space-around; + -webkit-transform: translate3d(0px, 0px, 0px); + transform: translate3d(0px, 0px, 0px); + opacity: 1; +} +.dayContainer + .dayContainer { + -webkit-box-shadow: -1px 0 0 #e6e6e6; + box-shadow: -1px 0 0 #e6e6e6; +} +.flatpickr-day { + background: none; + border: 1px solid transparent; + border-radius: 150px; + -webkit-box-sizing: border-box; + box-sizing: border-box; + color: #393939; + cursor: pointer; + font-weight: 400; + width: 14.2857143%; + -webkit-flex-basis: 14.2857143%; + -ms-flex-preferred-size: 14.2857143%; + flex-basis: 14.2857143%; + max-width: 39px; + height: 39px; + line-height: 39px; + margin: 0; + display: inline-block; + position: relative; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + text-align: center; +} +.flatpickr-day.inRange, +.flatpickr-day.prevMonthDay.inRange, +.flatpickr-day.nextMonthDay.inRange, +.flatpickr-day.today.inRange, +.flatpickr-day.prevMonthDay.today.inRange, +.flatpickr-day.nextMonthDay.today.inRange, +.flatpickr-day:hover, +.flatpickr-day.prevMonthDay:hover, +.flatpickr-day.nextMonthDay:hover, +.flatpickr-day:focus, +.flatpickr-day.prevMonthDay:focus, +.flatpickr-day.nextMonthDay:focus { + cursor: pointer; + outline: 0; + background: #e6e6e6; + border-color: #e6e6e6; +} +.flatpickr-day.today { + border-color: #959ea9; +} +.flatpickr-day.today:hover, +.flatpickr-day.today:focus { + border-color: #959ea9; + background: #959ea9; + color: #fff; +} +.flatpickr-day.selected, +.flatpickr-day.startRange, +.flatpickr-day.endRange, +.flatpickr-day.selected.inRange, +.flatpickr-day.startRange.inRange, +.flatpickr-day.endRange.inRange, +.flatpickr-day.selected:focus, +.flatpickr-day.startRange:focus, +.flatpickr-day.endRange:focus, +.flatpickr-day.selected:hover, +.flatpickr-day.startRange:hover, +.flatpickr-day.endRange:hover, +.flatpickr-day.selected.prevMonthDay, +.flatpickr-day.startRange.prevMonthDay, +.flatpickr-day.endRange.prevMonthDay, +.flatpickr-day.selected.nextMonthDay, +.flatpickr-day.startRange.nextMonthDay, +.flatpickr-day.endRange.nextMonthDay { + background: #569ff7; + -webkit-box-shadow: none; + box-shadow: none; + color: #fff; + border-color: #569ff7; +} +.flatpickr-day.selected.startRange, +.flatpickr-day.startRange.startRange, +.flatpickr-day.endRange.startRange { + border-radius: 50px 0 0 50px; +} +.flatpickr-day.selected.endRange, +.flatpickr-day.startRange.endRange, +.flatpickr-day.endRange.endRange { + border-radius: 0 50px 50px 0; +} +.flatpickr-day.selected.startRange + .endRange:not(:nth-child(7n+1)), +.flatpickr-day.startRange.startRange + .endRange:not(:nth-child(7n+1)), +.flatpickr-day.endRange.startRange + .endRange:not(:nth-child(7n+1)) { + -webkit-box-shadow: -10px 0 0 #569ff7; + box-shadow: -10px 0 0 #569ff7; +} +.flatpickr-day.selected.startRange.endRange, +.flatpickr-day.startRange.startRange.endRange, +.flatpickr-day.endRange.startRange.endRange { + border-radius: 50px; +} +.flatpickr-day.inRange { + border-radius: 0; + -webkit-box-shadow: -5px 0 0 #e6e6e6, 5px 0 0 #e6e6e6; + box-shadow: -5px 0 0 #e6e6e6, 5px 0 0 #e6e6e6; +} +.flatpickr-day.flatpickr-disabled, +.flatpickr-day.flatpickr-disabled:hover, +.flatpickr-day.prevMonthDay, +.flatpickr-day.nextMonthDay, +.flatpickr-day.notAllowed, +.flatpickr-day.notAllowed.prevMonthDay, +.flatpickr-day.notAllowed.nextMonthDay { + color: rgba(57,57,57,0.3); + background: transparent; + border-color: transparent; + cursor: default; +} +.flatpickr-day.flatpickr-disabled, +.flatpickr-day.flatpickr-disabled:hover { + cursor: not-allowed; + color: rgba(57,57,57,0.1); +} +.flatpickr-day.week.selected { + border-radius: 0; + -webkit-box-shadow: -5px 0 0 #569ff7, 5px 0 0 #569ff7; + box-shadow: -5px 0 0 #569ff7, 5px 0 0 #569ff7; +} +.flatpickr-day.hidden { + visibility: hidden; +} +.rangeMode .flatpickr-day { + margin-top: 1px; +} +.flatpickr-weekwrapper { + float: left; +} +.flatpickr-weekwrapper .flatpickr-weeks { + padding: 0 12px; + -webkit-box-shadow: 1px 0 0 #e6e6e6; + box-shadow: 1px 0 0 #e6e6e6; +} +.flatpickr-weekwrapper .flatpickr-weekday { + float: none; + width: 100%; + line-height: 28px; +} +.flatpickr-weekwrapper span.flatpickr-day, +.flatpickr-weekwrapper span.flatpickr-day:hover { + display: block; + width: 100%; + max-width: none; + color: rgba(57,57,57,0.3); + background: transparent; + cursor: default; + border: none; +} +.flatpickr-innerContainer { + display: block; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-sizing: border-box; + box-sizing: border-box; + overflow: hidden; +} +.flatpickr-rContainer { + display: inline-block; + padding: 0; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +.flatpickr-time { + text-align: center; + outline: 0; + display: block; + height: 0; + line-height: 40px; + max-height: 40px; + -webkit-box-sizing: border-box; + box-sizing: border-box; + overflow: hidden; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} +.flatpickr-time:after { + content: ""; + display: table; + clear: both; +} +.flatpickr-time .numInputWrapper { + -webkit-box-flex: 1; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + width: 40%; + height: 40px; + float: left; +} +.flatpickr-time .numInputWrapper span.arrowUp:after { + border-bottom-color: #393939; +} +.flatpickr-time .numInputWrapper span.arrowDown:after { + border-top-color: #393939; +} +.flatpickr-time.hasSeconds .numInputWrapper { + width: 26%; +} +.flatpickr-time.time24hr .numInputWrapper { + width: 49%; +} +.flatpickr-time input { + background: transparent; + -webkit-box-shadow: none; + box-shadow: none; + border: 0; + border-radius: 0; + text-align: center; + margin: 0; + padding: 0; + height: inherit; + line-height: inherit; + color: #393939; + font-size: 14px; + position: relative; + -webkit-box-sizing: border-box; + box-sizing: border-box; + -webkit-appearance: textfield; + -moz-appearance: textfield; + appearance: textfield; +} +.flatpickr-time input.flatpickr-hour { + font-weight: bold; +} +.flatpickr-time input.flatpickr-minute, +.flatpickr-time input.flatpickr-second { + font-weight: 400; +} +.flatpickr-time input:focus { + outline: 0; + border: 0; +} +.flatpickr-time .flatpickr-time-separator, +.flatpickr-time .flatpickr-am-pm { + height: inherit; + float: left; + line-height: inherit; + color: #393939; + font-weight: bold; + width: 2%; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-align-self: center; + -ms-flex-item-align: center; + align-self: center; +} +.flatpickr-time .flatpickr-am-pm { + outline: 0; + width: 18%; + cursor: pointer; + text-align: center; + font-weight: 400; +} +.flatpickr-time input:hover, +.flatpickr-time .flatpickr-am-pm:hover, +.flatpickr-time input:focus, +.flatpickr-time .flatpickr-am-pm:focus { + background: #eee; +} +.flatpickr-input[readonly] { + cursor: pointer; +} +@-webkit-keyframes fpFadeInDown { + from { + opacity: 0; + -webkit-transform: translate3d(0, -20px, 0); + transform: translate3d(0, -20px, 0); + } + to { + opacity: 1; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} +@keyframes fpFadeInDown { + from { + opacity: 0; + -webkit-transform: translate3d(0, -20px, 0); + transform: translate3d(0, -20px, 0); + } + to { + opacity: 1; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} diff --git a/src/main/resources/static/css/reservation.css b/src/main/resources/static/css/reservation.css new file mode 100644 index 000000000..de9666b71 --- /dev/null +++ b/src/main/resources/static/css/reservation.css @@ -0,0 +1,15 @@ +.disabled { + pointer-events: none; + opacity: 0.6; +} + +#theme-slots .theme-slot.active, #time-slots .time-slot.active { + background-color: #0a3711 !important; + color: white; +} + +#time-slots .time-slot.disabled { + background-color: #cccccc; + color: #666666; + cursor: not-allowed; +} diff --git a/src/main/resources/static/css/style.css b/src/main/resources/static/css/style.css new file mode 100644 index 000000000..815065743 --- /dev/null +++ b/src/main/resources/static/css/style.css @@ -0,0 +1,62 @@ +.profile-image { + height: 30px; + width: 30px; + border-radius: 50%; + margin-right: 5px; /* 이름과의 간격 조정 */ +} + +.nav-item .dropdown-toggle::after { + display: none; /* 드롭다운 화살표 제거 */ +} + +.nav-item { + margin-right: 10px; /* 네비게이션 간격 조정 */ +} + +.content-container { + width: 70%; + margin: 50px auto; +} + +.content-container-title { + text-align: center; + margin-bottom: 30px; +} + +.form-group input { + width: 100%; + padding: 10px; + margin: 10px 0; + border-radius: 5px; + border: 1px solid #ddd; +} + +/* Solid 버튼 */ +.btn-custom { + background-color: #0a3711; /* 버튼 기본 배경색 */ + color: white; /* 버튼 텍스트 색상 */ + border: 1px solid #0a3711; /* 테두리 색상 일치 */ +} + +.btn-custom:hover { + background-color: #083d0f; /* 호버 상태에서의 배경색 */ + color: white; /* 호버 상태에서의 텍스트 색상 */ + border: 1px solid #083d0f; /* 호버 상태에서의 테두리 색상 */ +} + +/* Outline 버튼 */ +.btn-outline-custom { + background-color: transparent; /* 버튼 기본 배경색 투명 */ + color: #0a3711; /* 버튼 텍스트 색상 */ + border: 1px solid #0a3711; /* 테두리 색상 */ +} + +.btn-outline-custom:hover { + background-color: #0a3711; /* 호버 상태에서의 배경색 */ + color: white; /* 호버 상태에서의 텍스트 색상 */ + border: 1px solid #0a3711; /* 호버 상태에서의 테두리 색상 유지 */ +} + +.cursor-pointer { + cursor: pointer; +} diff --git a/src/main/resources/static/image/admin-logo.png b/src/main/resources/static/image/admin-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b46677eceab6aeb595dcf404a64b46be958eb786 GIT binary patch literal 4640 zcmV+*65s8KP)q=XOevX>8Eq`-_69)W4q!^wl5k7evji?wcnK4g zJr}ls-s}rEmHUSu4l%-cLwyOLbGe&J6yGAqdw`A*Ei3Q?HRc~neY*!kwA=91ivT3u zlX<-=SAiU9)sylAAOBtD!I7<2{WJ)%3!u5H?KsDnuPK}sq1%!u>qiF*p9cI0{nj0^ z3c&5oyd06Q0NFVFD{`mto!5+R+WB}K{_6n~m*aF#_A*4iOH7pyAm|I#RQg&^ZGT2w zF~m(AEr6u+xoZ@CcMQG*kUwJlvx|Mh_r@Jb)I{C_xINh`>#Ons^eOmq`$B99B5eWO zeD>|Y^-=nqC0k9?cz)mA`*y}1NK{1D0=Qkvx`59{$v(q1=^$VR|^8GnvPBt$9I%B#vMx(M56T4cx64XDhgj=pu+m*?zV=Bw-&4mpykx; zvsC1rw*7=m%80o(?jWMTx&VAfuK*IZ{aE1KC;Kh)8mj_m?Ok>v$SZC7u!`|N5O)Z+ zu_^#h{lByA!!j?M?|lJ#mD5QQi+F)RvTE{N!h zY}t+!VGC;lP?a-m*^U?y$D1$D@@Lo)z{xZL&$41W27J*|;*P-AQA~J8^5?T>}kN-3qcH=^or^g+GEvyJYe2s0Dj|~KwSI;vUD*{lFxvt}{ zQ?xbi2y9_T0BF*N-58l~&vOtvKLKJ!ZP<;Gy>SO%3rj5kdu-T^4OtYxEYTfy?8b!h zhqyzqg%trL#oJ=TZhYu(-Bv7vQ_)`d1Q3O&`;wI!X{P)P&&5ouYx>i(}!iQp3luc*lqzf;Hhx6 zRUh_vw3ur9O58!%CW_s`#?5E?K)NIF6&k9%x-?k0Iqo=Y6UCZ+U&VPR4U~&VkvkVc zGB2(U(-VmxtpHBnl$iwNqR%v|iz^sQk?lMf42|DXC<1 z+qO2~CA9J?pl)9jiE>%7fB5@x#}OrwCy7dRW>-jpp99B4?Rz>z^_tPa;Y;I=BWfbw zxLMj<*ha$X0e&5|?;uq9cF~%5TC6ZCV zRE#|~R;;Wke=|Aa3KMB!qZZ>s7(Kh#U-(hPe(Q?Za|HvqJ#D9;<^~`ifxB@*zK}|o z5ANEyYu>K)tCqSD008T7+-^ea&0(l`d|4iuuhkfFeP70m!(PwRQRC=7 zzoH}M_K_X@nEi3b3O3VSY^kmlhDt9o4jA?>AnB4Do5C3Y| z>e95j5P;jYtP96KOOZ1~bq$cRCcEw0L?v_l?Glb%IhY+>5=0h{20|6i>cushCN-J&SzIE{7)@51pVFUz_8UE zg;5JY+zp>?{9vO;S&<1{bHEH5+sNbRPk#<~Df`P_N3O|L?z z)9j)0wPxjALjyyhCbm-56`{0m;~Wb#o&c7tX*=HW%;!K~Y|BTjsPf6u;Lzno8^(oW z!!YBRaO?yTz5q_9$!*Pj08cLinnSlA9@m!!3zu88A4m+=3Ze`-=P!xEG9FeMqXU%8@lIk zdvh1S+9O)rQsp+_tU23=9J#VMF!Z6I(o{b8DnU0)YeKn4h3iU{!fnmT%+W@C70mXy?^G{4_`DY^llS*d42AmUiRJKv%5+HDD-k_fX($Pf} zE6+>ibLSoSPt^i&d$R8r<<)j#vWebN8W`#iDoyp~)+qdSlo7>^px4m3+=VIy(A74{+Z!IRFfzfpY>20M{hsl4%8y?#aHI2JG}rBJk9b zRL`<5LV9D@s3`i1KmlYsvu&z;I?8}TN2DB7YfILB8l4xw+K0d($B?tv)iHN{yc_=v#^I-O9KTJ!?tbQQD6S~Hw3^BX7#$e8U47{WY#mj1zwxEJ zG&r;YrkDpv<+I-qc%3EZaZn=RJgvBK*Q3*2)Y`l3M9=qc1y+aX``?QLh4Yw0s{H1j z%u3_SU={lbt486bQmN^0x$lXCVn3t>Xzp%XB@q0LX-rb$dDqT#(cZq@4-_k@UXZVb zKPIJjN6l2U)8m>pMLJE=Y(a30tRIhOJ z*{`$u$eAXAI%@RwtK#NekJV@c01>G@m$4&(Q~3u4U>q*2*->L~p_>}p)ZEn;xNTe2 zZ>i0N{=T;R|HQuL)~!slmZ2Jq&>C2F@L!!jL%%9G$q~~NRH!1eS!Oi_5?*GHyB;o6 z;}BKF58R<;0NkFo=K#4{oHQ6)JoN)RgrZtwdKsNfHk0Y;?MMMNwMe2S@STX6xWIW~ zEe;xtZxl;SUFA(9fz7mt30yo{9THk!s>LtXY|A$UK92!VbRh^p5&1}Qpz!*`$HG%k z)vVn#JU5lk)!LB+K%w^C)ngj?JchL-6rNwRlR8Jye<=13t*`D{zt&g9VpPLeKWP+w zZLLNeBf*`(C8edSgy3G{d_dsV(!lVHvw^H#+3e=C{|xexT79Z;S=;m5YT7rD?#W&N zJiFFdJfnT-?(CF;G>$kY*6N_n85L*zoikmuq`UnDw=~iRyltjyt0Xr*Zob93xxG?v z?#ZlFpU;L7h(N0f-%91P-#D-u_y9&!tY)E#8-*M8-LvnH(_NG5&R$@|`vvfvklml- zZp(gdwjxM%<&H5bKMeCl5P-nz+(fCb_0;x*b#7Gsc`X3r>$j&XcJtZmMfkSGpTW1g z+p<4Qb>*r$C^OxgJ6nvu33#TZcvgFk_Y1c>^Kt-5<+FDRoMFjX+&o?!C@d#nCM}iE zUM*lX5#uDvf_%}3TS@~%gG_!)xb}#aRLMJA@ZSdVqKM)dQ}|19^Vxd|zv-(B1b#L; zP?++q-jh3=x6g8OWmxRtPiq2?0+tiUnn2|z32K~Ws%wDM3X~{tnkVy!dVIK_dX8$mWl8BpDze4@B<*Q2vf5z}bMwCU@7ohT0> zuX97Owdl5819Qm{)Nsg|seCSQ@kgn@uu+8XL>W=sDDvJX7n&>t0f4~gndVu2Pc8)( z#Hc{wuF}BJZ3q6pAOrx~na*bdS3H+C4gCS+nkXZR319FJNrtsR1OSR$oy+Hz29*~} z$*X`JvoN@m+Lw>l$|EqV(?o784vZYq>0K}a0FElFzAv!!z9&Xi(F^wV=;q%2@;TdYoDv&Ed1CwDqdB;xEH2%TEKNdHiT{t~pWxwZm&)VO&CoogE zo6mk6xST_W#}yI&&GWggJXpBmZey6fm1mZXO= z=PSHpiqy3uD!ioBU)XrKv4tf7pqgyqsovZgdnblXm|wWOJ=LAP%9?d0lmjTm564ar2Ca`%w&OsA%iMhC z%hSydOvTdPrMV@&OQ$~vF{E#<=h$umglT)X^B=jWjQ+3NoxOObZR2iF+bM27`=yHS zYh)C5$T$Y!3vO!c6Xdf$6J?`{>{4ML>OWPGrvvAp zhn@tk%IUzA=MeZtwu#orDja`w|JFo@d%K{=f-IxDvy>l3x!IxenmwEMSUuYo;Qs-N W@S<$gOrt{p00007|Op)OC{L7}0c zQeJ-Wr*19*?o$3wJaYCmxIv)nAj3O47Kq&4h0t7wN1a!PBlp8@(`bs^A}NaFzZJ%O zr;>z3sZ!|bj@bhypQ$iOM5kkWjF4-)P&~fa5qeGM&7vT3k$Nk5A+m<@v(>BL|PY;K`ZrG=@QF@Yk4$a92rZ5}B^ek?m-H+Sg>3+h+&&L9g(a z7v5{J!&=c(^n+V}L!ZX1Yeq|vZjdI$e%6durbN`3YA|+;vkqhrPY)LFJm5fA_oz`h zVRu09!mjLjGUxw}RFT3#RUc`li>+cgs9(g2$9jaVR^)3JP~@kj4Ojox0Ldi5sp|Rb zW$U$MYQ!fG65gVUMO|cNSP|9YVZ<|+<-kHCD}^T4zaTV}M>p0uW(!@W*+9LL6pgI6 zf7U{VU_wx}2_Qgib(=&dDQJd`AOhq(*%pYNL|bY$DAuc975iuOB4g2cXopA9!;jd1 zK>Y!xJkt*_b*EftDReE;={Qn+ENk&8njW2u-b4E$zdX;HSO&+;?9`Z?7BBHlb4^ol zN=d#|gi{Al2C(9kG3aG?uY^ICRd7t&!eeKDXJrfrv21qL`VJc=_c>2YH)(WV6;pMO z2B^N#hI*QLx?H@a_e3Xai0o>dHChbnpa3Bcjj#;LLm#8T=qsf3rQlB%n@SYbQ_5P3 zTA@KfY3aGo;bWZoyd#n@KI=v6vPg>5Z zbRgU0({VRb9On@6xWT=L6f}Sl!DL)fpAx!k?*FA4pd!*DOzhd^Aq~N?2O!!pZHZu& zAJ>eicxa8kRvSiWr8Lexg6VCRo&vc_>gd+TXs{}83R}Z# zoVT;X;P!8EkY(ybtSJ*r_=2O`29?gz!gWQGFYNpR2SQUc(%{UN*U;91HiOkswA4H_ zk))ZDQc0peNI_NZA}do@S!l9A8=sfGNYclSTL~GJSD()hGFYRikNI`f3vltAmJwVQ z#0yzuClAPS|LiFz#4-qfby0+_M0;ySANOrXUJaWj{FLQ6<#v&&x3*_DdQAzkta%qe zcaU+|-Vn3|M9j*b6cvU|U zpa--^Qi3Mo0?SZmo?smsAK3uy07e`qqzfw`-ch%go>twdM*ZSN1ybir7;@bwhxAu0 zbK`$(OM7VXSfV*c4<1ku`j|QrgO9WiWk#4=Zg&-oZY>(9EW0N(K2Vi;kHdSZt+Zi> z!mgf~&pWwq(JWuX+#0|4L)qe4%^idYLgK1-BGGRdybN15S@y$7UrxP$DN#s`PDir? z$AAg)OYBAch9t#R4vzG?bX~?^H8~&B>B6+osXe#fnr2>q8hl8FY4`iZX@m$qB418L z82o^(abZ>*`0r=2O>dx=#zIw|C2qzfQJdZi5gANrRiGD+&=;VLV$?|OBK*F2bq}%N z3L}?$RnL#Od2S+hWKCsHo=fL5e&5~hp^_NXtCL$>@S}J-OFDguUs>Z2RFaYvmQ|&m z{(en%*#{#WSpc)PFQKZV>oP$km-vO=?Ei&U&#r>Wo9EEkKkpJYlx1gv=|poDlv%=f zKmy=ekx$gY?e}G+C?>sIo(}=D8^cPa9#f&7`hHb$c@rIrC|3(Fd5hLTOUI~U=iv8b zc@P5mBA)4=D$v1L_16}zf&r>7ul{CkR88E2k*!S3kfZJKdn#$Cka`!|BX1aU5Q~`F zkinASM>lYQ;0j^=qou^eYZMi3Tmc zjNY=-fPfDuqQ2m1!^Q<;=vZOVJY6yW1`mg#QuSS(RiX42-yZ&GA7 zPh=yV1h*M(F&j~(o}PT+Oa0(N;lkx* z=2{hVh+qVWK=`?jpjIh8l2B-})hFF7>>iurN}@J82^lK98l3e~#z;Kx#U7M|^f4a( zwrjd?ES@V@-t~Eot&W*tE-P1WJ&U?VC$s!vS$W6_sh#_SP}qk_oy^DP=?*{mg`pn! z`%#c=W~5=e`!mh@_yFnf{1h8j%Q6jCf*%>fS9ns*=_^nisd7p8!7MWe>6(0nYSVwy z?1*cGAs%@YRXcv~KeN27^&vWpM(jHFpg&WVc3Ln;LLSoc?#6LXkvsYs@`d#v)Tb_7 ztCyb&N}~0~EHaC2_#WDSbm?P5G3}}7qS1V{HVJ-08^cC1aXq@5O{_uT1R9@lk&n^No! zYa?L|yA1QMuCzwfqI9RX#;b8+W*s@FHkem>j{W}ZR+KKfMVq5bCpLX~XcX7vJDVph z6!~|eo7~xqsO*kY1{(hx^m*V$Wk)A{dx`Ne+sLX_oaKW!^J>XR>3;cVL*tWz)tnDb zC~bO$mKU-n-()?Y#MvauzdJE;9)Uu>4cfLS_o~WN^PF0=Ot?)(eT^w7>Mm-G&!9Ru zi+gOV3rkNEM8{DX?T!=l*biBqJj>3jq$Xx6HPVz@=1^B6aU&St7-Iq zdjb>c&DEfmc7yE&f^cN#QKb`Yw3)eq3z>QiU>wzXPFWh3Vwj>2X0HsJbS#Oj zdq?zr=|Ph77$|4Wf)QMVp`Z8WDR}nx5o*#B`Xu)$^TMI{CkpnT$hnI z6kTBMp$hd(k=j#tBP8w_Td(9Lb{#6QG{yM3Ss=4)Qun?s_311^zi~gIJDXiO2y;&U z*XB5^ie#$z;AA`5ufw-~VoT;v^W{1mkx-D;JjJvO#S@%58|VbCKYoFbUn#vA>{ipN z6D?z+n3*-Pj=-3$ZeP{sTa)balygH5LO>hE{-sAf_Rd6=31`^ko6gv&(UYX3o(Shn z;EQL?)Hu2GJ{!|Pk}`t|a`~nWAFxJ3S9G4e=_o<;$>>SWQO~0`f5|+CWe+URlJgjOE=vW&;GpX{+1a$|`n)xpv)4KA zxxTVBlrkmheRr0PdPhe6W1EEn26m6WN|4cjI#qY|1ifFOH3=_&70KY>EcWitB}gF; z!lRm|Uctx6nzGW*hqTwV!!0d~Z2(*v{buwwCQ8Qg*1MSUQRT*1SFL)}Y8n}N*4Y<$ zWxH^8>1L^1S#hshh?#EJfRY21S-^Nwb66dyN(bp`jsU8$|~N7fDY2MhlHWEa!bc zouCwp+|t4FP4i*$^;D7C3r@LA(!m25%hL9_@rTQWh>D!7^+h{s?f2t^;2LYB(}EbB z1jl)<{}7VC7FjklxE;v3#eW@tLq^MSd-65I@G?=&_j=eavZHSlI97O7qgqHZz zHLu=tzS}WAG)Uu5g`>n1p4GH5aO5FFG)Nb7Qgm9Vw{7U$&=(U-9-OCRF~VRBh^9b` zp)D|IL}z|30#i_B+!sW6a{G zIwXv_-k~SACRdDvlGPEia0?|~GQm1PmGdE&o|p9g_xk)nP>WzE_>jbz05mbA4B$Iw zZ1(@{hlvyQx={~Vp(QH))C7rEAzsT|SILyI`cnPKpu~SFa&6@I4qJGm$t|MGak7Bk z&~&07+cE*k3N;UJoEXlew7;!#RulT9*hYR>Zz=zx@6b$MD%KD=^KF~4QO&Qf+rBz* zjXL#Bi*(YnpKuBP|fFW-)7>>*!xYdr+H_ z4pB5IuXz23TCnVo1&9k}OQ)ViNx?9dRP7|);++g1sepTph(2ld82Upwl-))Ra3P zwd)rV$@Pq}Hq9$&L*!T|Dmh(|Uo`iNY{Z+9A3r>}UEq$vJbTHaXc*8Wh5=LOdldJG z?;tt5VSVTU=I6K#cCJjgXALQ86(!_6k)pSxRJB%z@Q(rWy}Z(`OmXTpo_g|_%H``@ zgV9zagFQsNjPuD4@XnDPey>q&kxzNy(v})QL7*JxFv;u8g9}>q?tbLz7>N4qq(pTSuC+)Xawn*QBXBrR4Zs`P{PQ z`EW`q(Q1Z{W>G3NYt=J=H@vBb`iNMX{A=>?EZ<| zOHy`8-4C_3BXWP*ESjnF@a4xD+sZ`>jifGu>MZLBh|JNprzeZcd{U3;pZ z4ng&jmSXB}hpW2!Z*Cp_@hGgjTs-yrZ7<2khu%V-@o$H0-W6si6)69m>SnYNhn;iq zj}UrN``v~-Gr4OOf-GlpTPuSna2jMrR{1?mQCJh2=Xl?%f+^G3Rs+|pBnf+A9gYS!@a?lLH-mW1yDz+6 z{r1Z;)pQS4AP^p}Pukdn`*5^}4?pJ5@NN#+ez7AF$D zpaE>>AsaXJFB9s`OWA%g{j_0;2>EUAUIrc*wF)3Fz*{|j7ZX0(Dr?ZN>W8@$g_N}x z`n$1VEPKs`HqZU{>`%^FmkGWdj?q$L_i36|v+kWXD;e9TSag6o?jrinXmD~k+mUWN ziot`k%{1%7%y4(-iG1s18B^GhQkrsbD#-;?I)mMz{XOm~OcY(jM?MbkDUh=RW~d}8 zqlI(&b^XPEYyHV0ZK68q+L#j;MEbnyV=18OO zhYJxGCR{w~dirO2s*|4qfvV|D`%NA_>)!?$N40G$n1+!pt24X^hRBOVz6B)1w*9gS zrm(!}Xd7-T?h^fSjmq?Nznm8$l`s5B6_VdH+FD10({5n(%`ikd32oCvrsF>TN_1Cq zF)T+#_9kY;IDrk`rn*5-u!g8SwEA7+ zDUfD+T=r(z>pz>G2#nl}<_(w~AIUEH`Z)bmgnU>V` z(5z&;tQ z)3-KLIY*z>=p0uohiu)>{VrZ!cj(wbWm|Jv*c5W@t?^g97A5K5K1V2%d#yaEECh2|n z)Xm^I@?)(PuvLi;`)V#b&{PreGCN_utF>7;yZJ+V< zT@BE#K#APF4QGsq`qAdh-5@Y!-$%N4ZDf%-D2cFiu9<6*9t(Pia?i6?K^J1!5xuoP z6l9{?y%+qJj#5jHnz4N%co4KX0 zS*7f4-{5Z8n%|F5%iCrI_Q>#aKRnbc7pGUcF+XEZwyBaxxKGYEpt>M#p-X%G~+PVrZ$B30S*x6~0rI$EJ zqG(53Kcwo9G#c`ubv+>-s!<1)WWNlFk^agnL5AHsQ(Pv~(hi!Np7qx8n<8vq`mmcM z8n=o~xtCsF>9^kqOJ|>>`ne<&!VusISjeyV?qmj)`*D!=YNEdZ=B@8vO1!Vv3D!nQ z@a0Lfy57i7*9(9Ao@XHfbnrW)nBFiNgHI`uGd( z^?%MFh3E||+A5|89LJ&Z@Ei{4#j(cLkk914>b7Ln+?oRv5iEqEb%Vw;C2j;=r24k> zaPLLO<`<@)i*o0I8u5ee!lSJv<|Q6BAwvAgLp)TnjBm8Ap@z)?P0jqbrOo7yHoV;uO4*W>+1h(z z28hv*wV7NI_k^slb zE%s22A47_o{kV7f`ET& zz3xT#05bxG7Yp1vPL^8!m}!{PGgf+{=FG6){l~``&y&u_r8xk$pEE6*Wz$YADAvQ9 zfU|qbJC+N1(3CJ>$%7H)QOPf{13fyFYr-s^SBhAGx+c+x+Qsqe%kZkDlWfUu4Fb1UAsEHX?rKyVCl>8y zCiU21*44Qgkco~DxEzJHbsmBe3L1RPUK@1B+5}&|yJPySNvEO}b3iefobR4Ca_4UG z+tc97Ym)zc9Sd#gluZr5*Qfmw`{$LbS6&2u^pmi+KlyIrPjA)+gqDhICwH!vdjeiM zrCVm9pV%-T&*Iy*Y#BOeBkgvR!>LZ<*GKF|Tk_^s%Q<~!%aG?u2Dp;OjxgnOlNRMJ zK2@uM)b|XGe+m@})W!-J>vizZG3gRj;iyD*Ly3c{19E-6;fX(PBIo7VontLdPtsM7 zst#K^DB6tQ+rO;e2&@4ni!8k4z#f96F16DKXg!|d+fhe-`GMxmcg0AI#+q|-T*ccM zW@m^QJtm{YpIIXNZNCf{)I^t;_!qD(|Cvs(RmdoUD?6Y|Grw1)-iq)T{0+@EzUxLi zX-DPpEN4OvS_VA38t1lRz9A4mGn9t{4zg5LK5*FXG6`gAO`5^xtj8=`1ro9%Dw z3^dujBbnHMz8$o;fwE4hd46oaN9!gEGfO7jC^>l0&8cn%d0husrhloN#L~4nO5n;y zvDC8vJ(OMvGk9GJMKKl~+%F)}?>8BzaZ><1LLuR<|9T89_Ek1Z|IhLIeNTSJmNcu@ zl7qXNp*`He*Um#OM*gjD$(YuWhV%|nEtUfb98NpPYRMMerG!PKa!O-gFcX7~S?iLo ztGKt>CA&k;vy|5t9m>@2DwZ`H|8DnV)kpJ+J-qaqA?L>zgnzxo75OU5H{Rb!!rTG8 zG~104TofIzkHy-A!qP2+w%uMh^%y+b9J`AN&8S?6P#Spb$0qjYFO;>RWroqWf5y14 z#2F%b^lOA>rI2f&z^NE6uMA7;`GJ1oB18#Gi*KG784bx^RQ%cagnYIo!Vl25=dDF> z`2i}Udb_fS3|Rr~tC5lwG{jTgxcj2DdN-v-vin|JIn+s?8Mxdx=e(eRIBR;Cc7IMG z-iJmD@-DibHA=gz-AK!c$$2@|>UzerqWJO059HTRP$zcg*_0nzF9?*|Tp+r0ZI-bK z4BYN71G^KkyJxC()vUEbCwj={zglL;_H~`LSx5dSNfG*s*rO)=4^{9Wtd0z((;lVmFD1oLG zzdl#EXX7=g;VvG`wz4eH#F;O&8dw$lfT6bL*>?sIMWfmZ-}BXl-rEtUru@m*2O}PJ z{#BEUO0;}(pflCQvAiDhOo76od%70{X`~1vsfVC+Eo#LrGS>$}u*beYNZ=-_C9VGh zQrh+9*l9qahArsQA!yIUc{BFPJvuuo=nWW{{c;E7R2Q8?zt4ID$d_0eAqgKc`lE=x zMGA8Fh^oQOPIg2YWcvoZ#m!nuanRJ&>}J$F`^>G7a(x$Fj_^2)Fu$2?OS$q}PA|uX zQg`IS;7grlSo-@C#rF8tcDB4!B(Q$|6eJ)BdNN zI>+ZtaE~U9*%M-aEK_OrUo-6KO0%X%&#bX3X!;?-7oPtFR39l!T%jE6sIrcq`n)kA zF#0LWs^Z`QqzM{KQdi{lm!nlsdm#Tw!TWGH+egcORfg&M-a9$L5SJ&(>!w4Y_k9E8 zUA{^*g=|w?38GrI9h)YhU_VxeaC7hb?`su7)2M=Oe@au(V0i=b>%D99u={!w$7ReR z8BLdF%~>Yi9cqPlBiqhLtnZu*$1S*6CY8zVPHflmnBI#Nuvo5DnXW z)@IuFls0++P?66IH#?3kR+?*`DHn&kpS=<|xk(S&7ru{eyv&k7?VF<^#qgX?5)E-w z%ac*xp>Q_D-Jm@kN|0{s(5!j7GOi9Vx&S)O;+^cHhnlf}K-ar1>Qa;_Us(|KmcVE+ ztjW`MqaX+Pg2h-cT*_A#F8qiG|Bq-XxMZuTOv()Re^>xa-2uos?Bd5Cil~enC-XZ~ zc{fqDHqwuHEEb+ls;$2pn2PK_;RAi-CtBgw<~8hegBEQ5am^M*)o#zz!}I?1@0U#w zO#!ZDo*FvSj|TpflUnNq?e*(TDeipmeo?h*4*t9SXxVG5nUWXe^Rw@{#ozY%OKwU! zR)-72vEZRFO+nOtKm=JxenmdwL>h3IJpKY-tiKhs_egK5 zd9uQh)aU-hNz@U@IWhi&_MIdsxH`dd=Z5Wm}$0)fRZ5jgKTLtmj1B0oE#S`xM3qg0fY%sNDWHb1$$a zU4)a+NPtgv;dQ1KkfRi;rP*YHT9UY8KBvKtZVQ@J|;!}dZO32(ltMTW}okw!p>>N_KrKRf9|`OI|C zrl3D2M?3wg+LCOQPOFPlkR=#(U8hntC#YIdqK9Q!b*x#Er_$sbDah!h6~j~C?A9ZcBBsgZE!F5PKqLV4|A~rj9_@ zi57L}cLT~-^rns=F_$@jJ`{kg#VqpPFy3yks22KH{9UkZfZL$} z!4qj@0d*W0<{b+rkf9$Ic_;moqXdux3<0X+|F`;Y@lFbvSp2^QXA61(JlU1l@S`3~TwYW-P&vBq^G} z5mzZc`v6!9M;>4qPl&xm=Ti2atP4*nGXU+|t)Kwe#n-|o;YMS@j}B5K!z;n)$~-h; ztd~GmsA4f^;}>g6u8_yzjKa>ECIJ&@PT=HUO7J6(DF9vTe^I<+0-9uVnDfze&c)6$ z0zLgB$RACp1z%9#3Y?ejI9B?1Jorj>v`~V21tUr78?mcVBoBB^Sz%2QyTGi@)B?Mr zH~izZ1Uz-jCdj%<0_#W$ETKeYOQ`lrev|7P29QO6Wf96KIA>yKf2b;_Yfgl&d^Ew_wbW_8c-tG; zikrQluJc_l^?CXue5kQp?U&0Z0M3z?6YDqDd;;vGA4U@!s8~DTlvhbltX02vMuR4e zaeYR=0FRNxMS&)@c0f6awULadU&@jx4;et|fL0yO-{?60@=s1)iH_qT;DNVN;A;t> z8I3{jaI*$90K;M*_>b_x)d!IrDcg6H-%XIlteRuR#L5W*oE6u4K zj&na&R92daLLDu=!jU907D0iGK;6BS=2nUpR>!48Ujs}cYudoesTPC)yc{fjk_H=^;b^r zVp`z(Zx%~09rCbZKF=6#H4rQ#2=b$QfiAtW$kFuo<0t?1emIxfC0Xc6Z#eubU$o^Z zkYyPYsz35GasLK>4-tlk8hepsha7Qh5)A?mscpt{MfaRM)raG7Rbuc(re&Q^zNeE| zo9x+2&)G>x9=7E-pCGEXd4zm*$Dgdj%AR7N&Mh{%*ZuV6={fsSz*um`3%`(^Fo0Ks zA{CF;&1fA>`qv40AOD4;&1z@p+fdDq#^ENv6g6;MIGx7QdL4iaGNZj9Xa*zpW*+*KJ&kb4ui6|!Y$T)d@ z3!wkNDe8*4Mqn;0i4lk&a6UWqbuMkAYr?H)yBD3yc8rpOl4_C^x*`}wa@YNo-kgB^ z%{fR;1wO@j+g!Z=!xnP_zL8qMa-8$vLaH9sSB}nRkSWo zmPkS@Ig0xas0_U9K34b0y(jotk;qW@S{YFyJ<&4re@9n&ugZ zHU8&AJ?F_*3Pp(Hym?rv-HWlPvS4i-Pq1KE5L5aMzapvl<^@Z%>h^7mGeuksibuZ{Zd1q?TK(`e3~(4L<$Rg-hO9(m^MlOr3)8pG)q zw-?SV``M5eUlP4I0~F6gEcu!a-*&sVXk~{?k<)ES_CShn?@qg}+jQkU?;_pj;V=t! zvv#2q9Mm*xt?LSWGFU44ti8}HRN9hb6>KKR0RON7J$c2Fu34eJ(sjs+A=81pV!#}u zwnHYA_c7sS?-y1)XBQ(ud73W~3!a6^9XxiQCB-jEDnHWx?7yI%YY1a{6U~P{iCvo@ z-zp)AKn5^cKQm@_I3c%|1tDxLs2D-Jb!cKGt=ZSOL%-N(%1E5Trbw`k_o*l@G+6My zi6RKWjVA>?`jejiYjp3f>YEc5HE&b8!GaZJ?E1wxdBuw2G9922)Rh8XfCZMzCpUS- zQ7mvkCM66NXo}!8upSF+LtQ7Fa^i9UEhXDxjd6@C%8D-p&JZsYC!weMYL)-;hXkY8 z+eJ0v_Ni{ixMUox#4C>S5r=EInV`gJMp}&wGlDu&e8EYB+=o>%96mv}%!S`8?M5(L z3)>#2D?>`)Np(B@lPv6y_5U7ukOz^Sr-T(+Q}O;Io;*Yg;rHAMag82TEu^THHrrqw zH7dTG;_Cs`wH?k%;?b3*0?XL&jAP5=U|2G7L`^@mwi^Y4ejg- zRmi8b*#5E5*%!T%=vQyI(G;z+0f28y##)2oe!K-`x|7qLU;GEVlD7^dKrvZn4|(S) zig#>LFw!_C{;e_}N{pYMfH_AgGDR@rto({5PF(rxtM@jN z;)TQ`OMmP^Ao@qgy@vz zQDJV~>*LRj~{B)dhfLzh}umi46 z@2AVsSnVnORVC-gNL0OR=BrxYeY>h#JXExJlri<-dFHGAZ`&|8E}VmN8r6{*dliZH z_3;nqe|G3Z zg>|>BO*)?*KXN7itE#Z~6kJzuNmQ$Bnu(rqUbj)@0|yly{LV?>prS-0u8(%FAXTrK zmJmC#6Qq{;vZfN*#{9=Vjm=R?j1v=Sf~|xVJw6;$XvPtzm0UuuPh>rBJFuVw3 zA#DD-m-}CEueD3X@d6jF|9DgHrEOv~mkpsSsjJCf(UH7MU!|k0T8z}mAoTjZ^h0f~ zsbsb(PiNKakn^lqnvl|u57;ne60jpql1u)`@&Y}ma4u7*v#Fz=GE!#iw#yfG6=OPv zsc@aruN=KZInJ_Ktr-qX=E`ElKwU2I?IJ~*kLRLV*QWVTtFY(kP5?%SRqWTq{6?em zBzSRwwG+mi&aOH8cO>=X@=VA@H}3MC^|S#-HQl@ZAlyhiIoDtzH6sN-5P-F40uLhNd~@9$BA(D?w#;XIWOxqTf#(wB@5K^p@TP_!%B5i%qW zB5;`?f+`VKhZ^}lfV=0O3PPV?GV?YhH@Wq=c{jLlju*}>Q$0pP)U}wVskbS4saW+Z z5ZlroQp!Uc>n4-r1_|=Lcy&diIQse`ifH7Eg8{en>0$Be-pB{`3`YGgkV=8Ez~HTa z^b?Ad=+fHs*HPnHCwlGt+o80;JYy#8a>U3;b3E-Wqj8tU!9Ud^@n zY6f%a_KqQaV%7NAVkUjWyI95tas!wJ?Ce>)?akB2E??SX6Kbw4RtvO~a+IiN%)K>% zig&O~(>LwbB-i?elEPcEW1jG%8*11WtABuz283;CK7;H))~F-TnR5{8IO4?lW!2{I zPGXh#z}-cujutju$^aP`$RH$^uerAYxkqMCK2OaX8!FN~d9(ZL)%#xJw<4^*3~kJq zjfl&KUxufv-Sh-$BLl59e$0N1t_*zso@XeZrc9EESYf2qb~$MBVQac@I#v`FtD%LX_OGE1wU&>1^0>`V8%tHeV!+)@&Q*u;G_LfPP6O-5< zxka{7iWaGrYpqgvCzcdf1LXig8TzIxdQwNdDHH8??sDo>tQDYekF3{hebyMYD^YAW zC+~VAy}gWaW9CwYyBK#Zf6BCcR)_Q$E> zt(j!-Fv-5ChNHaE_k3iU_|o-Hk05oeKQYu1Ty1K7MC=db2l7g@p)=Y48toDCGOUC; zjkuEhe~^#*ClB+XsB4@8kOpmP)^K`GE{LqrPnBf@WY*NS31GB{pkf7B#42r{y(ZZJ z?n6^b52y7V!Jx~cFT{xSK31_P8`WK->4ZJvilmKXv|0=!^DNG;$GzcL_1a$H!HF!f ze@g!XzMGi&-G-%)MEgtkIbUyrALgEzG#9iBKHA(QaA`Sp&RECjMq{-h zLR5t-lf>sfmPcvpOr2+15gqu6__PVJPpZV~7|p$M(>y4&IP!gq?&Y-_`o()#&_pWC zJV_6&9dkh1&zj%n9@1#V^IG`*StOG8D)!iV++yt+=6hOtw0g0xQEQ)R;7fU7Q8WKs z0ZrYc3er;rt&VbK7>h}#26#F<;LY7JI^o|TBr8{uypf&dOarmiaE~g^DCvK0_ zEF501_Zn#S+&gCew721ZXdY2wo(=V}JmgIFfBaZ24)k<1v~e#{4=K)fpw}*qaIB%> z)w2%%#7WqksYTHHBhr;ai+tq;QVLdiDyB3nV@OLpylhTI)~BaER9zs$Yw>LYq149L z@4xC`k1Y6=f-N382&(Z?FY$#8P(hxEyl>}!6a&Q3JS92rO%FIN6!oQqQ>E5UeJh*w z0l}HJ&-w8M;k&2c+2D1hwxvQ};hzhEOAG&ka|`HKV@;o6)nxW5U98D$NinpCa#A0- z&@*Ta{GOnty-`#dAofur#yqTV6`C)*s)GUR?Z5ve z%b-Gypo+YRXKrQPziBj(G1ih~W$0`${B+b3GjN)@{Qf>={+%T+7$1?67b%`!R{c&U zc0UZiC%S_m*^_#D940*K?|S?dh+(=|;g$OvfPsbim~y_UnA5dv;5`+sH%?D`?AsuT zgCr_?;Q;h56!JYgg!laTP0yEyd{}|bg%?wnReR)vdeP>OnFpvgSrJi&SH5KSw*o0h zp2ld()d<2Z#Lo%$_mlG7Nde%kTZd#=`eeZis`7z~xm#6ZdYRL$fdFWk+B#LnC4lQZ zWYQZ-!7$VkRt=6T_9ra|v;tgJvQ)h$01VA`QBPKFA_*sbB{!Yv>Z8|rO+{!8)o?za zc--W4XZi-@=JwGWHs=Fis+zxkCuR_}V;7ogIgJ)m(Dc|jCrCqX{k6GS4<~%Vg>z&^7P|foZjTr&xNl9I9 z9zcF@s~fBj;oAtseTTPTZeVnbx2H0P`kr8%Cs${g2_6IxHPO>dfXM zz9b^WPi=@wg1aY$e&s_w4XSGTU`-u}4w`$LxXT}*1+Q$IMKGJ8mnQvFSo03jJyg|$f^IsW}LkN6Vp&Ak0J z(U-(!rVvF?`YS3cd^_^}g8x`_3z1cGfX?TxwP{WD$906ERk{*c{yzvbi?oo|jjuE- zR2YVc?c@2o9d{6UE;G|0g8st=q;buY6{h(pF8rt#GdCLTXA1r*w8=e` zg@x2VXB_hE>B4$+OtPAis^6(3Z3^?%)oNkcCt-*;Cx40e!xz|)pCWeexN%cQ`kl}lC(Wy{c{@c116sx_#KXgd?Ja^3Mp1Sx`{6sy4Ap0 z5CS_p?o!Km8qo_)EqmAZ5PP+1=se*$ zvpI4w7A?kx;fKDIe;G9V_T)(>jsf=&U!WH)OFLrSa+!0_`ekWX!FdwoFai|QT1IXeET#_l_QcIxW>f3ef!jSVGFArceN^rfeSYYWs+z-zv|kSe$qLqL0WRxFG73u#DaLTUIbw5xL!qC)(@2&?^qW@Ujm3v_Sf z&bX(C{`Bp0nWu=0WIgH%-m8gxMh(;ZMBx%s)ivVX>ww&w8-VHlJTp75`Sw}NCD>W& z{6{Tl>dvsD9x=IV5#B3|wq^dSWb`E){Q`T?Y8zDxz#j)V23GU1I~~eh*HouUtM6Mc)zsn_r3>VI zl2y|c7$u)^TWdSc!VG7b5DWyulrrpLN=b7+%Q=5=q*aVn40YfyW7B&E!|5wyRuWI8 zE7u-5Hv!lWQ6Ji$D$pALYu+5Fs8YPdr}qYmjN*A8I0kMUk2U%WbqYsifWrIl&q67W zujd(hc8=*agi`0~sD=L=EX{o<4Ms?xGZrfIU$3T=%w7So;5)5qsMr!WG2e9I1EG1= z?TbdW{j$t*zZ8+0E8MrXt+E={HhMzbD9_UCoZ6nvu_^cxPDcb*cjARVabG&MAAha* zFbq#*?4T76FCtzKE1{iM^h5bJgSRvt{{c^6XGg5~6OZ6)zIE?4@50%}{r+J#js9Ua zW#7r@exjw-UgCp6bj8 z&8_ZXcN*04d+jc>>0v@!P}7~{7sar7{)$|EJ23ldr0L=^1go97h^07A%JH)(p)!>l zKbq!Ax^XN)+XUUaYg@O^FS-<0-ud#Am`#+RcZ;gxt53sa(JY(-ifah1)UK<_wduC4 zY_?&_(WTaj!PkbQW>|d>yUGOJ?{+B9# zs4R8f$h=4W#9rf8&a!vIQpRk~N@p`wQRbN1SKDe;Op7jle98_@n7RLg#O_Lq<42y! zB!6nLalDN!sA0I7$8HV2GSq5;cd$e{9Q7FjSWQTh`RLL}=V9Ep&oCDGgxYME^g3gwGC-ve4(4~t$RWo_N!7l7h2}uW%r`^WqW+%-w zKH04VX3xS0r0r=Bb_H4tmhuF!o})g3`T?ZMAn5e^`9N1z?^OpEH1+1c?J1i?>p_v47X9|l_XbM` zu#fQqK1-R@)%@~XI`)gEpJ%xSsFaJcnn#q7$|LR8Y^~UjoZtvZNHNUvSLoOhaRCi$V6AZx+3L8ePU&Xus)9bRKz=}M zGkykaZ|U_YLiP7X7}p5XF+{oPn1}LT;*g4RNO5EkkhreRhMD|o4uIE6KT8ynx4=!{>+$ekZ06DmrupDnk&dB`47rLHk1WCf*3H{UFe zU6pdayL)HI=aw#j#XX@UGOLgA$Gz>V8tHl)C(lCT7Q9UUKM4+0J_54-F)m1hgKhhh z5fNA54CPxZ-)Va3=2q`HduP?-q=Z~4Kz+mK33yAX$khGz}-g!cjLToY$;=tT5_YDf5vIj z^=znGv-b9!mHn*indu#(L`gS->L4n++L!78HlADxKnoYg9y`LB85Ydaqf^BO{9t>{ z%ClXrH;3wyw4W<5s8+Jk;=+l)Kh!J2q8rGj>k@KJ*fK7T#eVp})ItmQM^6fHo{YN*jZa?MP5qM2lwtC{!dMen`_Z#$$X`q-7>{X-O9~GDDfO;#~%#x%& zHT9$#W!eRpBQDoq@vC|(*wQx|xD9=y8Yt!gd-f^!wZif|MfQgonOS=c1)fFMk6hiS zJbMy;@TmLSf#Oy?>926sB7CW-8`nUw0C-DJc~?TaCiY8B)UlbR=d0-Yk;SaoId2cr zUg}pNta@6zi8{6%m_hiGRX4JMViB-EaG1Cp8#Y@ zpXWZ$s{8JKP#!#fQQkKL{kaR{x>V8&CvHR zsl;Qznypo*6~#uReIP}<;^9^+(#(=J6Ma9Uju(LqTCK<;DDx|TY7YbZ5cZ}nvn$mF zFtezxQWrpME@4lCN7V(f^z2Fcn^nY&-dzaIte6f!-_NLGAz?Q{vwppew2u&u74v}+ z8PuPdrE3)F?}i#9mJ>FCGaJ-Vq`zBW*5SwhX62wfWK0p=!10;XT)}t_KyAOId}l%b zU0nb(Ym2|B3(!Ut;AG%u>VjDQoQi(*+OF6+qdGIQwC#?*ziC&Tkx`vlNu7qi5yy@N z!0@ap%*>KDf^tlD{!CUCX65!p^o==Y{3*MtGP4}`3;KR1W_&)gnlb~vioQ|%W-+rY zd{r? ztmEsM){t30K8e1uXBE$6TsLNx56_|Rf3k}I$hdCI2J{K^jXsOGJnM=vvs}0eegBh1 zd^+ojDGP4|)Vyy2A0srtJiAj}05g-gU0r|{bps#4lm43K0YGcMOW0y^xw;@Nn1uHL zFe{tkgnK`)mIKEEKTsFM>|nx4Q|d=y&BndJ_L7-4Ucx#;_gEaIoY&`fK{OyEqQ3;hk1 zMchkR@nB}@*pvKE^6QN*;Pgyu$4u-nU_SbrD~sp`eg&+XX$_fKqox4AA>5Pdej$sP z54=6o8Zt}7R=^YJZ?ddn3Gf|Ye3q4DW`i0-cqNzT4aoY&Gr)`t>&PrQlL)g~)i+y~ z@iO7vqu`;Vxw8lG0{WXVM*I=K(=%(< z=D>v%XM$gKyo~qsHv_`EuYaPyDPu%8VI!ffGpjALV%vd})5eY)fP*utGqb$emGFA3i0}BT<6%7DH*3gv z;9~SQZ#&{f;B7!hCN*c4u1??(;AZsqT|45p_%I){rkzBwcKaGA?gNeoMrKlTX6YCS zydQV~{e9MsSOA=uNzIv++@`>t=x^$F#caYB!&5V;J2Rz8ge`ktLVsViEA9if$e`}b ztj9>gmtxW17gk3XVK0G$@#_K0mt6^K_=`NTuU1?@SkYu=5gY=%i2g=zciabj2)|FX zbd4pvvMb85yp6;Qz(K87p_vV44d4>=H@zYj5q??igP#gm8fE}z1M|?|ABwmNSgYk) zG_!^r0lbR715m}2z}NAvV%C0>fioz_a^3!74xZ>|CN&kf9(`w^j4t3h;5~SkF^gw3 z@Ls~alp^oeS5N#OFr~HXG_x#p0w+`6N08Mq5BN1)OffD5CN^8aW~Ox@FcW=8l5*S!oCWLw3~jEOEeD1Idjj92oNM(P ziN}Ebo2q3qOUfw1cBKB5GkDA=%mm;E$piSlm|VLrq+aCVg!^bHMF{ z35NIg{CO|1L|LdD7(!TUKArH&YC2)B{f&G6on_++;G@9f>VlY#2LRS<55lj3?bQV- zu*HPIzypL?)-!wle2g$)D3eKqWq8x7{@j%CrSB(F@!Udq-2ZoVK}^pB0ISwJKj5o` zT?MUu7O`SbFcWx+aK!4f!1IIw!fUe9HD&@~ABu^DFZwlsbqR+y^?up=%!c-w34EIH zp1&e4izUJX0IOvLVS|)U<4IL1Swt8NJPW)?7!b@M?9X2P=j@(;UdZd;?ar?H=LLi% zUjV~-{j#s>&wLEw%X&QFi+wyWuFszn2w(DR^@M-Hr{h)NE5Hv}@p+p89st~Sml$HX&1-?j_L~2$y4*;wc zYXYYdCJc@im&L41mJp6E{u1zvxGa`a9spPt)+9U;_#mDvXcp5P;77o>2`fR&n&tt3 zWoaz%KEhyN9dTLAisX61{G0zFKl?$LSqTjTjsfmLKRL?b$aCCHIL_F6E0|fXbO8GS zR{>qOdtwJ*sI)|us^x@V`PTqf19#vhdsbi`09e_K z0d@lR>4|OeeNbjIfj{-cAMvkzD-m~mR(2B!uM2jC|KHvnyhI&DaRC32UJAvCJQW3j zEtFW{vHaJ&WTz-JEFw~<>=J>7Vuqj{-Yo5g2wC>q&F=>uZ{T;C%kDgenR%9QM-CJI zuMY7FPq2)~a#b(Ru@nFi!>-_=DhM8_9fI^?A5Y)#LM`^oc#5q~XB06)QUFApY!W^7 zMDe034j#xZ!6@2Li~REz&s5W$h&VN?7|*u2YW#kyNBE*1)A@{#*u*D% zXyeWHJt8_VZhtBf=h1|pg+M=F#>_8yTD@~{QQcgaQf-uH3?Ev&q}nYH`fj*o?IFJ6 w0K3>%r|NfGY`5>*1LOXJ-_O~mj~HP;095VSsET)7ivR!s07*qoM6N<$f{tIqEC2ui literal 0 HcmV?d00001 diff --git a/src/main/resources/static/js/flatpickr.js b/src/main/resources/static/js/flatpickr.js new file mode 100644 index 000000000..b0f59ec21 --- /dev/null +++ b/src/main/resources/static/js/flatpickr.js @@ -0,0 +1,2 @@ +/* flatpickr v4.6.13,, @license MIT */ +!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(e="undefined"!=typeof globalThis?globalThis:e||self).flatpickr=n()}(this,(function(){"use strict";var e=function(){return(e=Object.assign||function(e){for(var n,t=1,a=arguments.length;t",noCalendar:!1,now:new Date,onChange:[],onClose:[],onDayCreate:[],onDestroy:[],onKeyDown:[],onMonthChange:[],onOpen:[],onParseConfig:[],onReady:[],onValueUpdate:[],onYearChange:[],onPreCalendarPosition:[],plugins:[],position:"auto",positionElement:void 0,prevArrow:"",shorthandCurrentMonth:!1,showMonths:1,static:!1,time_24hr:!1,weekNumbers:!1,wrap:!1},i={weekdays:{shorthand:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],longhand:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"]},months:{shorthand:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],longhand:["January","February","March","April","May","June","July","August","September","October","November","December"]},daysInMonth:[31,28,31,30,31,30,31,31,30,31,30,31],firstDayOfWeek:0,ordinal:function(e){var n=e%100;if(n>3&&n<21)return"th";switch(n%10){case 1:return"st";case 2:return"nd";case 3:return"rd";default:return"th"}},rangeSeparator:" to ",weekAbbreviation:"Wk",scrollTitle:"Scroll to increment",toggleTitle:"Click to toggle",amPM:["AM","PM"],yearAriaLabel:"Year",monthAriaLabel:"Month",hourAriaLabel:"Hour",minuteAriaLabel:"Minute",time_24hr:!1},o=function(e,n){return void 0===n&&(n=2),("000"+e).slice(-1*n)},r=function(e){return!0===e?1:0};function l(e,n){var t;return function(){var a=this,i=arguments;clearTimeout(t),t=setTimeout((function(){return e.apply(a,i)}),n)}}var c=function(e){return e instanceof Array?e:[e]};function s(e,n,t){if(!0===t)return e.classList.add(n);e.classList.remove(n)}function d(e,n,t){var a=window.document.createElement(e);return n=n||"",t=t||"",a.className=n,void 0!==t&&(a.textContent=t),a}function u(e){for(;e.firstChild;)e.removeChild(e.firstChild)}function f(e,n){return n(e)?e:e.parentNode?f(e.parentNode,n):void 0}function m(e,n){var t=d("div","numInputWrapper"),a=d("input","numInput "+e),i=d("span","arrowUp"),o=d("span","arrowDown");if(-1===navigator.userAgent.indexOf("MSIE 9.0")?a.type="number":(a.type="text",a.pattern="\\d*"),void 0!==n)for(var r in n)a.setAttribute(r,n[r]);return t.appendChild(a),t.appendChild(i),t.appendChild(o),t}function g(e){try{return"function"==typeof e.composedPath?e.composedPath()[0]:e.target}catch(n){return e.target}}var p=function(){},h=function(e,n,t){return t.months[n?"shorthand":"longhand"][e]},v={D:p,F:function(e,n,t){e.setMonth(t.months.longhand.indexOf(n))},G:function(e,n){e.setHours((e.getHours()>=12?12:0)+parseFloat(n))},H:function(e,n){e.setHours(parseFloat(n))},J:function(e,n){e.setDate(parseFloat(n))},K:function(e,n,t){e.setHours(e.getHours()%12+12*r(new RegExp(t.amPM[1],"i").test(n)))},M:function(e,n,t){e.setMonth(t.months.shorthand.indexOf(n))},S:function(e,n){e.setSeconds(parseFloat(n))},U:function(e,n){return new Date(1e3*parseFloat(n))},W:function(e,n,t){var a=parseInt(n),i=new Date(e.getFullYear(),0,2+7*(a-1),0,0,0,0);return i.setDate(i.getDate()-i.getDay()+t.firstDayOfWeek),i},Y:function(e,n){e.setFullYear(parseFloat(n))},Z:function(e,n){return new Date(n)},d:function(e,n){e.setDate(parseFloat(n))},h:function(e,n){e.setHours((e.getHours()>=12?12:0)+parseFloat(n))},i:function(e,n){e.setMinutes(parseFloat(n))},j:function(e,n){e.setDate(parseFloat(n))},l:p,m:function(e,n){e.setMonth(parseFloat(n)-1)},n:function(e,n){e.setMonth(parseFloat(n)-1)},s:function(e,n){e.setSeconds(parseFloat(n))},u:function(e,n){return new Date(parseFloat(n))},w:p,y:function(e,n){e.setFullYear(2e3+parseFloat(n))}},D={D:"",F:"",G:"(\\d\\d|\\d)",H:"(\\d\\d|\\d)",J:"(\\d\\d|\\d)\\w+",K:"",M:"",S:"(\\d\\d|\\d)",U:"(.+)",W:"(\\d\\d|\\d)",Y:"(\\d{4})",Z:"(.+)",d:"(\\d\\d|\\d)",h:"(\\d\\d|\\d)",i:"(\\d\\d|\\d)",j:"(\\d\\d|\\d)",l:"",m:"(\\d\\d|\\d)",n:"(\\d\\d|\\d)",s:"(\\d\\d|\\d)",u:"(.+)",w:"(\\d\\d|\\d)",y:"(\\d{2})"},w={Z:function(e){return e.toISOString()},D:function(e,n,t){return n.weekdays.shorthand[w.w(e,n,t)]},F:function(e,n,t){return h(w.n(e,n,t)-1,!1,n)},G:function(e,n,t){return o(w.h(e,n,t))},H:function(e){return o(e.getHours())},J:function(e,n){return void 0!==n.ordinal?e.getDate()+n.ordinal(e.getDate()):e.getDate()},K:function(e,n){return n.amPM[r(e.getHours()>11)]},M:function(e,n){return h(e.getMonth(),!0,n)},S:function(e){return o(e.getSeconds())},U:function(e){return e.getTime()/1e3},W:function(e,n,t){return t.getWeek(e)},Y:function(e){return o(e.getFullYear(),4)},d:function(e){return o(e.getDate())},h:function(e){return e.getHours()%12?e.getHours()%12:12},i:function(e){return o(e.getMinutes())},j:function(e){return e.getDate()},l:function(e,n){return n.weekdays.longhand[e.getDay()]},m:function(e){return o(e.getMonth()+1)},n:function(e){return e.getMonth()+1},s:function(e){return e.getSeconds()},u:function(e){return e.getTime()},w:function(e){return e.getDay()},y:function(e){return String(e.getFullYear()).substring(2)}},b=function(e){var n=e.config,t=void 0===n?a:n,o=e.l10n,r=void 0===o?i:o,l=e.isMobile,c=void 0!==l&&l;return function(e,n,a){var i=a||r;return void 0===t.formatDate||c?n.split("").map((function(n,a,o){return w[n]&&"\\"!==o[a-1]?w[n](e,i,t):"\\"!==n?n:""})).join(""):t.formatDate(e,n,i)}},C=function(e){var n=e.config,t=void 0===n?a:n,o=e.l10n,r=void 0===o?i:o;return function(e,n,i,o){if(0===e||e){var l,c=o||r,s=e;if(e instanceof Date)l=new Date(e.getTime());else if("string"!=typeof e&&void 0!==e.toFixed)l=new Date(e);else if("string"==typeof e){var d=n||(t||a).dateFormat,u=String(e).trim();if("today"===u)l=new Date,i=!0;else if(t&&t.parseDate)l=t.parseDate(e,d);else if(/Z$/.test(u)||/GMT$/.test(u))l=new Date(e);else{for(var f=void 0,m=[],g=0,p=0,h="";g=0?new Date:new Date(w.config.minDate.getTime()),t=E(w.config);n.setHours(t.hours,t.minutes,t.seconds,n.getMilliseconds()),w.selectedDates=[n],w.latestSelectedDateObj=n}void 0!==e&&"blur"!==e.type&&function(e){e.preventDefault();var n="keydown"===e.type,t=g(e),a=t;void 0!==w.amPM&&t===w.amPM&&(w.amPM.textContent=w.l10n.amPM[r(w.amPM.textContent===w.l10n.amPM[0])]);var i=parseFloat(a.getAttribute("min")),l=parseFloat(a.getAttribute("max")),c=parseFloat(a.getAttribute("step")),s=parseInt(a.value,10),d=e.delta||(n?38===e.which?1:-1:0),u=s+c*d;if(void 0!==a.value&&2===a.value.length){var f=a===w.hourElement,m=a===w.minuteElement;ul&&(u=a===w.hourElement?u-l-r(!w.amPM):i,m&&L(void 0,1,w.hourElement)),w.amPM&&f&&(1===c?u+s===23:Math.abs(u-s)>c)&&(w.amPM.textContent=w.l10n.amPM[r(w.amPM.textContent===w.l10n.amPM[0])]),a.value=o(u)}}(e);var a=w._input.value;O(),ye(),w._input.value!==a&&w._debouncedChange()}function O(){if(void 0!==w.hourElement&&void 0!==w.minuteElement){var e,n,t=(parseInt(w.hourElement.value.slice(-2),10)||0)%24,a=(parseInt(w.minuteElement.value,10)||0)%60,i=void 0!==w.secondElement?(parseInt(w.secondElement.value,10)||0)%60:0;void 0!==w.amPM&&(e=t,n=w.amPM.textContent,t=e%12+12*r(n===w.l10n.amPM[1]));var o=void 0!==w.config.minTime||w.config.minDate&&w.minDateHasTime&&w.latestSelectedDateObj&&0===M(w.latestSelectedDateObj,w.config.minDate,!0),l=void 0!==w.config.maxTime||w.config.maxDate&&w.maxDateHasTime&&w.latestSelectedDateObj&&0===M(w.latestSelectedDateObj,w.config.maxDate,!0);if(void 0!==w.config.maxTime&&void 0!==w.config.minTime&&w.config.minTime>w.config.maxTime){var c=y(w.config.minTime.getHours(),w.config.minTime.getMinutes(),w.config.minTime.getSeconds()),s=y(w.config.maxTime.getHours(),w.config.maxTime.getMinutes(),w.config.maxTime.getSeconds()),d=y(t,a,i);if(d>s&&d=12)]),void 0!==w.secondElement&&(w.secondElement.value=o(t)))}function N(e){var n=g(e),t=parseInt(n.value)+(e.delta||0);(t/1e3>1||"Enter"===e.key&&!/[^\d]/.test(t.toString()))&&ee(t)}function P(e,n,t,a){return n instanceof Array?n.forEach((function(n){return P(e,n,t,a)})):e instanceof Array?e.forEach((function(e){return P(e,n,t,a)})):(e.addEventListener(n,t,a),void w._handlers.push({remove:function(){return e.removeEventListener(n,t,a)}}))}function Y(){De("onChange")}function j(e,n){var t=void 0!==e?w.parseDate(e):w.latestSelectedDateObj||(w.config.minDate&&w.config.minDate>w.now?w.config.minDate:w.config.maxDate&&w.config.maxDate=0&&M(e,w.selectedDates[1])<=0)}(n)&&!be(n)&&o.classList.add("inRange"),w.weekNumbers&&1===w.config.showMonths&&"prevMonthDay"!==e&&a%7==6&&w.weekNumbers.insertAdjacentHTML("beforeend",""+w.config.getWeek(n)+""),De("onDayCreate",o),o}function W(e){e.focus(),"range"===w.config.mode&&oe(e)}function B(e){for(var n=e>0?0:w.config.showMonths-1,t=e>0?w.config.showMonths:-1,a=n;a!=t;a+=e)for(var i=w.daysContainer.children[a],o=e>0?0:i.children.length-1,r=e>0?i.children.length:-1,l=o;l!=r;l+=e){var c=i.children[l];if(-1===c.className.indexOf("hidden")&&ne(c.dateObj))return c}}function J(e,n){var t=k(),a=te(t||document.body),i=void 0!==e?e:a?t:void 0!==w.selectedDateElem&&te(w.selectedDateElem)?w.selectedDateElem:void 0!==w.todayDateElem&&te(w.todayDateElem)?w.todayDateElem:B(n>0?1:-1);void 0===i?w._input.focus():a?function(e,n){for(var t=-1===e.className.indexOf("Month")?e.dateObj.getMonth():w.currentMonth,a=n>0?w.config.showMonths:-1,i=n>0?1:-1,o=t-w.currentMonth;o!=a;o+=i)for(var r=w.daysContainer.children[o],l=t-w.currentMonth===o?e.$i+n:n<0?r.children.length-1:0,c=r.children.length,s=l;s>=0&&s0?c:-1);s+=i){var d=r.children[s];if(-1===d.className.indexOf("hidden")&&ne(d.dateObj)&&Math.abs(e.$i-s)>=Math.abs(n))return W(d)}w.changeMonth(i),J(B(i),0)}(i,n):W(i)}function K(e,n){for(var t=(new Date(e,n,1).getDay()-w.l10n.firstDayOfWeek+7)%7,a=w.utils.getDaysInMonth((n-1+12)%12,e),i=w.utils.getDaysInMonth(n,e),o=window.document.createDocumentFragment(),r=w.config.showMonths>1,l=r?"prevMonthDay hidden":"prevMonthDay",c=r?"nextMonthDay hidden":"nextMonthDay",s=a+1-t,u=0;s<=a;s++,u++)o.appendChild(R("flatpickr-day "+l,new Date(e,n-1,s),0,u));for(s=1;s<=i;s++,u++)o.appendChild(R("flatpickr-day",new Date(e,n,s),0,u));for(var f=i+1;f<=42-t&&(1===w.config.showMonths||u%7!=0);f++,u++)o.appendChild(R("flatpickr-day "+c,new Date(e,n+1,f%i),0,u));var m=d("div","dayContainer");return m.appendChild(o),m}function U(){if(void 0!==w.daysContainer){u(w.daysContainer),w.weekNumbers&&u(w.weekNumbers);for(var e=document.createDocumentFragment(),n=0;n1||"dropdown"!==w.config.monthSelectorType)){var e=function(e){return!(void 0!==w.config.minDate&&w.currentYear===w.config.minDate.getFullYear()&&ew.config.maxDate.getMonth())};w.monthsDropdownContainer.tabIndex=-1,w.monthsDropdownContainer.innerHTML="";for(var n=0;n<12;n++)if(e(n)){var t=d("option","flatpickr-monthDropdown-month");t.value=new Date(w.currentYear,n).getMonth().toString(),t.textContent=h(n,w.config.shorthandCurrentMonth,w.l10n),t.tabIndex=-1,w.currentMonth===n&&(t.selected=!0),w.monthsDropdownContainer.appendChild(t)}}}function $(){var e,n=d("div","flatpickr-month"),t=window.document.createDocumentFragment();w.config.showMonths>1||"static"===w.config.monthSelectorType?e=d("span","cur-month"):(w.monthsDropdownContainer=d("select","flatpickr-monthDropdown-months"),w.monthsDropdownContainer.setAttribute("aria-label",w.l10n.monthAriaLabel),P(w.monthsDropdownContainer,"change",(function(e){var n=g(e),t=parseInt(n.value,10);w.changeMonth(t-w.currentMonth),De("onMonthChange")})),q(),e=w.monthsDropdownContainer);var a=m("cur-year",{tabindex:"-1"}),i=a.getElementsByTagName("input")[0];i.setAttribute("aria-label",w.l10n.yearAriaLabel),w.config.minDate&&i.setAttribute("min",w.config.minDate.getFullYear().toString()),w.config.maxDate&&(i.setAttribute("max",w.config.maxDate.getFullYear().toString()),i.disabled=!!w.config.minDate&&w.config.minDate.getFullYear()===w.config.maxDate.getFullYear());var o=d("div","flatpickr-current-month");return o.appendChild(e),o.appendChild(a),t.appendChild(o),n.appendChild(t),{container:n,yearElement:i,monthElement:e}}function V(){u(w.monthNav),w.monthNav.appendChild(w.prevMonthNav),w.config.showMonths&&(w.yearElements=[],w.monthElements=[]);for(var e=w.config.showMonths;e--;){var n=$();w.yearElements.push(n.yearElement),w.monthElements.push(n.monthElement),w.monthNav.appendChild(n.container)}w.monthNav.appendChild(w.nextMonthNav)}function z(){w.weekdayContainer?u(w.weekdayContainer):w.weekdayContainer=d("div","flatpickr-weekdays");for(var e=w.config.showMonths;e--;){var n=d("div","flatpickr-weekdaycontainer");w.weekdayContainer.appendChild(n)}return G(),w.weekdayContainer}function G(){if(w.weekdayContainer){var e=w.l10n.firstDayOfWeek,t=n(w.l10n.weekdays.shorthand);e>0&&e\n "+t.join("")+"\n \n "}}function Z(e,n){void 0===n&&(n=!0);var t=n?e:e-w.currentMonth;t<0&&!0===w._hidePrevMonthArrow||t>0&&!0===w._hideNextMonthArrow||(w.currentMonth+=t,(w.currentMonth<0||w.currentMonth>11)&&(w.currentYear+=w.currentMonth>11?1:-1,w.currentMonth=(w.currentMonth+12)%12,De("onYearChange"),q()),U(),De("onMonthChange"),Ce())}function Q(e){return w.calendarContainer.contains(e)}function X(e){if(w.isOpen&&!w.config.inline){var n=g(e),t=Q(n),a=!(n===w.input||n===w.altInput||w.element.contains(n)||e.path&&e.path.indexOf&&(~e.path.indexOf(w.input)||~e.path.indexOf(w.altInput)))&&!t&&!Q(e.relatedTarget),i=!w.config.ignoredFocusElements.some((function(e){return e.contains(n)}));a&&i&&(w.config.allowInput&&w.setDate(w._input.value,!1,w.config.altInput?w.config.altFormat:w.config.dateFormat),void 0!==w.timeContainer&&void 0!==w.minuteElement&&void 0!==w.hourElement&&""!==w.input.value&&void 0!==w.input.value&&_(),w.close(),w.config&&"range"===w.config.mode&&1===w.selectedDates.length&&w.clear(!1))}}function ee(e){if(!(!e||w.config.minDate&&ew.config.maxDate.getFullYear())){var n=e,t=w.currentYear!==n;w.currentYear=n||w.currentYear,w.config.maxDate&&w.currentYear===w.config.maxDate.getFullYear()?w.currentMonth=Math.min(w.config.maxDate.getMonth(),w.currentMonth):w.config.minDate&&w.currentYear===w.config.minDate.getFullYear()&&(w.currentMonth=Math.max(w.config.minDate.getMonth(),w.currentMonth)),t&&(w.redraw(),De("onYearChange"),q())}}function ne(e,n){var t;void 0===n&&(n=!0);var a=w.parseDate(e,void 0,n);if(w.config.minDate&&a&&M(a,w.config.minDate,void 0!==n?n:!w.minDateHasTime)<0||w.config.maxDate&&a&&M(a,w.config.maxDate,void 0!==n?n:!w.maxDateHasTime)>0)return!1;if(!w.config.enable&&0===w.config.disable.length)return!0;if(void 0===a)return!1;for(var i=!!w.config.enable,o=null!==(t=w.config.enable)&&void 0!==t?t:w.config.disable,r=0,l=void 0;r=l.from.getTime()&&a.getTime()<=l.to.getTime())return i}return!i}function te(e){return void 0!==w.daysContainer&&(-1===e.className.indexOf("hidden")&&-1===e.className.indexOf("flatpickr-disabled")&&w.daysContainer.contains(e))}function ae(e){var n=e.target===w._input,t=w._input.value.trimEnd()!==Me();!n||!t||e.relatedTarget&&Q(e.relatedTarget)||w.setDate(w._input.value,!0,e.target===w.altInput?w.config.altFormat:w.config.dateFormat)}function ie(e){var n=g(e),t=w.config.wrap?p.contains(n):n===w._input,a=w.config.allowInput,i=w.isOpen&&(!a||!t),o=w.config.inline&&t&&!a;if(13===e.keyCode&&t){if(a)return w.setDate(w._input.value,!0,n===w.altInput?w.config.altFormat:w.config.dateFormat),w.close(),n.blur();w.open()}else if(Q(n)||i||o){var r=!!w.timeContainer&&w.timeContainer.contains(n);switch(e.keyCode){case 13:r?(e.preventDefault(),_(),fe()):me(e);break;case 27:e.preventDefault(),fe();break;case 8:case 46:t&&!w.config.allowInput&&(e.preventDefault(),w.clear());break;case 37:case 39:if(r||t)w.hourElement&&w.hourElement.focus();else{e.preventDefault();var l=k();if(void 0!==w.daysContainer&&(!1===a||l&&te(l))){var c=39===e.keyCode?1:-1;e.ctrlKey?(e.stopPropagation(),Z(c),J(B(1),0)):J(void 0,c)}}break;case 38:case 40:e.preventDefault();var s=40===e.keyCode?1:-1;w.daysContainer&&void 0!==n.$i||n===w.input||n===w.altInput?e.ctrlKey?(e.stopPropagation(),ee(w.currentYear-s),J(B(1),0)):r||J(void 0,7*s):n===w.currentYearElement?ee(w.currentYear-s):w.config.enableTime&&(!r&&w.hourElement&&w.hourElement.focus(),_(e),w._debouncedChange());break;case 9:if(r){var d=[w.hourElement,w.minuteElement,w.secondElement,w.amPM].concat(w.pluginElements).filter((function(e){return e})),u=d.indexOf(n);if(-1!==u){var f=d[u+(e.shiftKey?-1:1)];e.preventDefault(),(f||w._input).focus()}}else!w.config.noCalendar&&w.daysContainer&&w.daysContainer.contains(n)&&e.shiftKey&&(e.preventDefault(),w._input.focus())}}if(void 0!==w.amPM&&n===w.amPM)switch(e.key){case w.l10n.amPM[0].charAt(0):case w.l10n.amPM[0].charAt(0).toLowerCase():w.amPM.textContent=w.l10n.amPM[0],O(),ye();break;case w.l10n.amPM[1].charAt(0):case w.l10n.amPM[1].charAt(0).toLowerCase():w.amPM.textContent=w.l10n.amPM[1],O(),ye()}(t||Q(n))&&De("onKeyDown",e)}function oe(e,n){if(void 0===n&&(n="flatpickr-day"),1===w.selectedDates.length&&(!e||e.classList.contains(n)&&!e.classList.contains("flatpickr-disabled"))){for(var t=e?e.dateObj.getTime():w.days.firstElementChild.dateObj.getTime(),a=w.parseDate(w.selectedDates[0],void 0,!0).getTime(),i=Math.min(t,w.selectedDates[0].getTime()),o=Math.max(t,w.selectedDates[0].getTime()),r=!1,l=0,c=0,s=i;si&&sl)?l=s:s>a&&(!c||s ."+n)).forEach((function(n){var i,o,s,d=n.dateObj.getTime(),u=l>0&&d0&&d>c;if(u)return n.classList.add("notAllowed"),void["inRange","startRange","endRange"].forEach((function(e){n.classList.remove(e)}));r&&!u||(["startRange","inRange","endRange","notAllowed"].forEach((function(e){n.classList.remove(e)})),void 0!==e&&(e.classList.add(t<=w.selectedDates[0].getTime()?"startRange":"endRange"),at&&d===a&&n.classList.add("endRange"),d>=l&&(0===c||d<=c)&&(o=a,s=t,(i=d)>Math.min(o,s)&&i0||t.getMinutes()>0||t.getSeconds()>0),w.selectedDates&&(w.selectedDates=w.selectedDates.filter((function(e){return ne(e)})),w.selectedDates.length||"min"!==e||F(t),ye()),w.daysContainer&&(ue(),void 0!==t?w.currentYearElement[e]=t.getFullYear().toString():w.currentYearElement.removeAttribute(e),w.currentYearElement.disabled=!!a&&void 0!==t&&a.getFullYear()===t.getFullYear())}}function ce(){return w.config.wrap?p.querySelector("[data-input]"):p}function se(){"object"!=typeof w.config.locale&&void 0===I.l10ns[w.config.locale]&&w.config.errorHandler(new Error("flatpickr: invalid locale "+w.config.locale)),w.l10n=e(e({},I.l10ns.default),"object"==typeof w.config.locale?w.config.locale:"default"!==w.config.locale?I.l10ns[w.config.locale]:void 0),D.D="("+w.l10n.weekdays.shorthand.join("|")+")",D.l="("+w.l10n.weekdays.longhand.join("|")+")",D.M="("+w.l10n.months.shorthand.join("|")+")",D.F="("+w.l10n.months.longhand.join("|")+")",D.K="("+w.l10n.amPM[0]+"|"+w.l10n.amPM[1]+"|"+w.l10n.amPM[0].toLowerCase()+"|"+w.l10n.amPM[1].toLowerCase()+")",void 0===e(e({},v),JSON.parse(JSON.stringify(p.dataset||{}))).time_24hr&&void 0===I.defaultConfig.time_24hr&&(w.config.time_24hr=w.l10n.time_24hr),w.formatDate=b(w),w.parseDate=C({config:w.config,l10n:w.l10n})}function de(e){if("function"!=typeof w.config.position){if(void 0!==w.calendarContainer){De("onPreCalendarPosition");var n=e||w._positionElement,t=Array.prototype.reduce.call(w.calendarContainer.children,(function(e,n){return e+n.offsetHeight}),0),a=w.calendarContainer.offsetWidth,i=w.config.position.split(" "),o=i[0],r=i.length>1?i[1]:null,l=n.getBoundingClientRect(),c=window.innerHeight-l.bottom,d="above"===o||"below"!==o&&ct,u=window.pageYOffset+l.top+(d?-t-2:n.offsetHeight+2);if(s(w.calendarContainer,"arrowTop",!d),s(w.calendarContainer,"arrowBottom",d),!w.config.inline){var f=window.pageXOffset+l.left,m=!1,g=!1;"center"===r?(f-=(a-l.width)/2,m=!0):"right"===r&&(f-=a-l.width,g=!0),s(w.calendarContainer,"arrowLeft",!m&&!g),s(w.calendarContainer,"arrowCenter",m),s(w.calendarContainer,"arrowRight",g);var p=window.document.body.offsetWidth-(window.pageXOffset+l.right),h=f+a>window.document.body.offsetWidth,v=p+a>window.document.body.offsetWidth;if(s(w.calendarContainer,"rightMost",h),!w.config.static)if(w.calendarContainer.style.top=u+"px",h)if(v){var D=function(){for(var e=null,n=0;nw.currentMonth+w.config.showMonths-1)&&"range"!==w.config.mode;if(w.selectedDateElem=t,"single"===w.config.mode)w.selectedDates=[a];else if("multiple"===w.config.mode){var o=be(a);o?w.selectedDates.splice(parseInt(o),1):w.selectedDates.push(a)}else"range"===w.config.mode&&(2===w.selectedDates.length&&w.clear(!1,!1),w.latestSelectedDateObj=a,w.selectedDates.push(a),0!==M(a,w.selectedDates[0],!0)&&w.selectedDates.sort((function(e,n){return e.getTime()-n.getTime()})));if(O(),i){var r=w.currentYear!==a.getFullYear();w.currentYear=a.getFullYear(),w.currentMonth=a.getMonth(),r&&(De("onYearChange"),q()),De("onMonthChange")}if(Ce(),U(),ye(),i||"range"===w.config.mode||1!==w.config.showMonths?void 0!==w.selectedDateElem&&void 0===w.hourElement&&w.selectedDateElem&&w.selectedDateElem.focus():W(t),void 0!==w.hourElement&&void 0!==w.hourElement&&w.hourElement.focus(),w.config.closeOnSelect){var l="single"===w.config.mode&&!w.config.enableTime,c="range"===w.config.mode&&2===w.selectedDates.length&&!w.config.enableTime;(l||c)&&fe()}Y()}}w.parseDate=C({config:w.config,l10n:w.l10n}),w._handlers=[],w.pluginElements=[],w.loadedPlugins=[],w._bind=P,w._setHoursFromDate=F,w._positionCalendar=de,w.changeMonth=Z,w.changeYear=ee,w.clear=function(e,n){void 0===e&&(e=!0);void 0===n&&(n=!0);w.input.value="",void 0!==w.altInput&&(w.altInput.value="");void 0!==w.mobileInput&&(w.mobileInput.value="");w.selectedDates=[],w.latestSelectedDateObj=void 0,!0===n&&(w.currentYear=w._initialDate.getFullYear(),w.currentMonth=w._initialDate.getMonth());if(!0===w.config.enableTime){var t=E(w.config),a=t.hours,i=t.minutes,o=t.seconds;A(a,i,o)}w.redraw(),e&&De("onChange")},w.close=function(){w.isOpen=!1,w.isMobile||(void 0!==w.calendarContainer&&w.calendarContainer.classList.remove("open"),void 0!==w._input&&w._input.classList.remove("active"));De("onClose")},w.onMouseOver=oe,w._createElement=d,w.createDay=R,w.destroy=function(){void 0!==w.config&&De("onDestroy");for(var e=w._handlers.length;e--;)w._handlers[e].remove();if(w._handlers=[],w.mobileInput)w.mobileInput.parentNode&&w.mobileInput.parentNode.removeChild(w.mobileInput),w.mobileInput=void 0;else if(w.calendarContainer&&w.calendarContainer.parentNode)if(w.config.static&&w.calendarContainer.parentNode){var n=w.calendarContainer.parentNode;if(n.lastChild&&n.removeChild(n.lastChild),n.parentNode){for(;n.firstChild;)n.parentNode.insertBefore(n.firstChild,n);n.parentNode.removeChild(n)}}else w.calendarContainer.parentNode.removeChild(w.calendarContainer);w.altInput&&(w.input.type="text",w.altInput.parentNode&&w.altInput.parentNode.removeChild(w.altInput),delete w.altInput);w.input&&(w.input.type=w.input._type,w.input.classList.remove("flatpickr-input"),w.input.removeAttribute("readonly"));["_showTimeInput","latestSelectedDateObj","_hideNextMonthArrow","_hidePrevMonthArrow","__hideNextMonthArrow","__hidePrevMonthArrow","isMobile","isOpen","selectedDateElem","minDateHasTime","maxDateHasTime","days","daysContainer","_input","_positionElement","innerContainer","rContainer","monthNav","todayDateElem","calendarContainer","weekdayContainer","prevMonthNav","nextMonthNav","monthsDropdownContainer","currentMonthElement","currentYearElement","navigationCurrentMonth","selectedDateElem","config"].forEach((function(e){try{delete w[e]}catch(e){}}))},w.isEnabled=ne,w.jumpToDate=j,w.updateValue=ye,w.open=function(e,n){void 0===n&&(n=w._positionElement);if(!0===w.isMobile){if(e){e.preventDefault();var t=g(e);t&&t.blur()}return void 0!==w.mobileInput&&(w.mobileInput.focus(),w.mobileInput.click()),void De("onOpen")}if(w._input.disabled||w.config.inline)return;var a=w.isOpen;w.isOpen=!0,a||(w.calendarContainer.classList.add("open"),w._input.classList.add("active"),De("onOpen"),de(n));!0===w.config.enableTime&&!0===w.config.noCalendar&&(!1!==w.config.allowInput||void 0!==e&&w.timeContainer.contains(e.relatedTarget)||setTimeout((function(){return w.hourElement.select()}),50))},w.redraw=ue,w.set=function(e,n){if(null!==e&&"object"==typeof e)for(var a in Object.assign(w.config,e),e)void 0!==ge[a]&&ge[a].forEach((function(e){return e()}));else w.config[e]=n,void 0!==ge[e]?ge[e].forEach((function(e){return e()})):t.indexOf(e)>-1&&(w.config[e]=c(n));w.redraw(),ye(!0)},w.setDate=function(e,n,t){void 0===n&&(n=!1);void 0===t&&(t=w.config.dateFormat);if(0!==e&&!e||e instanceof Array&&0===e.length)return w.clear(n);pe(e,t),w.latestSelectedDateObj=w.selectedDates[w.selectedDates.length-1],w.redraw(),j(void 0,n),F(),0===w.selectedDates.length&&w.clear(!1);ye(n),n&&De("onChange")},w.toggle=function(e){if(!0===w.isOpen)return w.close();w.open(e)};var ge={locale:[se,G],showMonths:[V,S,z],minDate:[j],maxDate:[j],positionElement:[ve],clickOpens:[function(){!0===w.config.clickOpens?(P(w._input,"focus",w.open),P(w._input,"click",w.open)):(w._input.removeEventListener("focus",w.open),w._input.removeEventListener("click",w.open))}]};function pe(e,n){var t=[];if(e instanceof Array)t=e.map((function(e){return w.parseDate(e,n)}));else if(e instanceof Date||"number"==typeof e)t=[w.parseDate(e,n)];else if("string"==typeof e)switch(w.config.mode){case"single":case"time":t=[w.parseDate(e,n)];break;case"multiple":t=e.split(w.config.conjunction).map((function(e){return w.parseDate(e,n)}));break;case"range":t=e.split(w.l10n.rangeSeparator).map((function(e){return w.parseDate(e,n)}))}else w.config.errorHandler(new Error("Invalid date supplied: "+JSON.stringify(e)));w.selectedDates=w.config.allowInvalidPreload?t:t.filter((function(e){return e instanceof Date&&ne(e,!1)})),"range"===w.config.mode&&w.selectedDates.sort((function(e,n){return e.getTime()-n.getTime()}))}function he(e){return e.slice().map((function(e){return"string"==typeof e||"number"==typeof e||e instanceof Date?w.parseDate(e,void 0,!0):e&&"object"==typeof e&&e.from&&e.to?{from:w.parseDate(e.from,void 0),to:w.parseDate(e.to,void 0)}:e})).filter((function(e){return e}))}function ve(){w._positionElement=w.config.positionElement||w._input}function De(e,n){if(void 0!==w.config){var t=w.config[e];if(void 0!==t&&t.length>0)for(var a=0;t[a]&&a1||"static"===w.config.monthSelectorType?w.monthElements[n].textContent=h(t.getMonth(),w.config.shorthandCurrentMonth,w.l10n)+" ":w.monthsDropdownContainer.value=t.getMonth().toString(),e.value=t.getFullYear().toString()})),w._hidePrevMonthArrow=void 0!==w.config.minDate&&(w.currentYear===w.config.minDate.getFullYear()?w.currentMonth<=w.config.minDate.getMonth():w.currentYearw.config.maxDate.getMonth():w.currentYear>w.config.maxDate.getFullYear()))}function Me(e){var n=e||(w.config.altInput?w.config.altFormat:w.config.dateFormat);return w.selectedDates.map((function(e){return w.formatDate(e,n)})).filter((function(e,n,t){return"range"!==w.config.mode||w.config.enableTime||t.indexOf(e)===n})).join("range"!==w.config.mode?w.config.conjunction:w.l10n.rangeSeparator)}function ye(e){void 0===e&&(e=!0),void 0!==w.mobileInput&&w.mobileFormatStr&&(w.mobileInput.value=void 0!==w.latestSelectedDateObj?w.formatDate(w.latestSelectedDateObj,w.mobileFormatStr):""),w.input.value=Me(w.config.dateFormat),void 0!==w.altInput&&(w.altInput.value=Me(w.config.altFormat)),!1!==e&&De("onValueUpdate")}function xe(e){var n=g(e),t=w.prevMonthNav.contains(n),a=w.nextMonthNav.contains(n);t||a?Z(t?-1:1):w.yearElements.indexOf(n)>=0?n.select():n.classList.contains("arrowUp")?w.changeYear(w.currentYear+1):n.classList.contains("arrowDown")&&w.changeYear(w.currentYear-1)}return function(){w.element=w.input=p,w.isOpen=!1,function(){var n=["wrap","weekNumbers","allowInput","allowInvalidPreload","clickOpens","time_24hr","enableTime","noCalendar","altInput","shorthandCurrentMonth","inline","static","enableSeconds","disableMobile"],i=e(e({},JSON.parse(JSON.stringify(p.dataset||{}))),v),o={};w.config.parseDate=i.parseDate,w.config.formatDate=i.formatDate,Object.defineProperty(w.config,"enable",{get:function(){return w.config._enable},set:function(e){w.config._enable=he(e)}}),Object.defineProperty(w.config,"disable",{get:function(){return w.config._disable},set:function(e){w.config._disable=he(e)}});var r="time"===i.mode;if(!i.dateFormat&&(i.enableTime||r)){var l=I.defaultConfig.dateFormat||a.dateFormat;o.dateFormat=i.noCalendar||r?"H:i"+(i.enableSeconds?":S":""):l+" H:i"+(i.enableSeconds?":S":"")}if(i.altInput&&(i.enableTime||r)&&!i.altFormat){var s=I.defaultConfig.altFormat||a.altFormat;o.altFormat=i.noCalendar||r?"h:i"+(i.enableSeconds?":S K":" K"):s+" h:i"+(i.enableSeconds?":S":"")+" K"}Object.defineProperty(w.config,"minDate",{get:function(){return w.config._minDate},set:le("min")}),Object.defineProperty(w.config,"maxDate",{get:function(){return w.config._maxDate},set:le("max")});var d=function(e){return function(n){w.config["min"===e?"_minTime":"_maxTime"]=w.parseDate(n,"H:i:S")}};Object.defineProperty(w.config,"minTime",{get:function(){return w.config._minTime},set:d("min")}),Object.defineProperty(w.config,"maxTime",{get:function(){return w.config._maxTime},set:d("max")}),"time"===i.mode&&(w.config.noCalendar=!0,w.config.enableTime=!0);Object.assign(w.config,o,i);for(var u=0;u-1?w.config[m]=c(f[m]).map(T).concat(w.config[m]):void 0===i[m]&&(w.config[m]=f[m])}i.altInputClass||(w.config.altInputClass=ce().className+" "+w.config.altInputClass);De("onParseConfig")}(),se(),function(){if(w.input=ce(),!w.input)return void w.config.errorHandler(new Error("Invalid input element specified"));w.input._type=w.input.type,w.input.type="text",w.input.classList.add("flatpickr-input"),w._input=w.input,w.config.altInput&&(w.altInput=d(w.input.nodeName,w.config.altInputClass),w._input=w.altInput,w.altInput.placeholder=w.input.placeholder,w.altInput.disabled=w.input.disabled,w.altInput.required=w.input.required,w.altInput.tabIndex=w.input.tabIndex,w.altInput.type="text",w.input.setAttribute("type","hidden"),!w.config.static&&w.input.parentNode&&w.input.parentNode.insertBefore(w.altInput,w.input.nextSibling));w.config.allowInput||w._input.setAttribute("readonly","readonly");ve()}(),function(){w.selectedDates=[],w.now=w.parseDate(w.config.now)||new Date;var e=w.config.defaultDate||("INPUT"!==w.input.nodeName&&"TEXTAREA"!==w.input.nodeName||!w.input.placeholder||w.input.value!==w.input.placeholder?w.input.value:null);e&&pe(e,w.config.dateFormat);w._initialDate=w.selectedDates.length>0?w.selectedDates[0]:w.config.minDate&&w.config.minDate.getTime()>w.now.getTime()?w.config.minDate:w.config.maxDate&&w.config.maxDate.getTime()0&&(w.latestSelectedDateObj=w.selectedDates[0]);void 0!==w.config.minTime&&(w.config.minTime=w.parseDate(w.config.minTime,"H:i"));void 0!==w.config.maxTime&&(w.config.maxTime=w.parseDate(w.config.maxTime,"H:i"));w.minDateHasTime=!!w.config.minDate&&(w.config.minDate.getHours()>0||w.config.minDate.getMinutes()>0||w.config.minDate.getSeconds()>0),w.maxDateHasTime=!!w.config.maxDate&&(w.config.maxDate.getHours()>0||w.config.maxDate.getMinutes()>0||w.config.maxDate.getSeconds()>0)}(),w.utils={getDaysInMonth:function(e,n){return void 0===e&&(e=w.currentMonth),void 0===n&&(n=w.currentYear),1===e&&(n%4==0&&n%100!=0||n%400==0)?29:w.l10n.daysInMonth[e]}},w.isMobile||function(){var e=window.document.createDocumentFragment();if(w.calendarContainer=d("div","flatpickr-calendar"),w.calendarContainer.tabIndex=-1,!w.config.noCalendar){if(e.appendChild((w.monthNav=d("div","flatpickr-months"),w.yearElements=[],w.monthElements=[],w.prevMonthNav=d("span","flatpickr-prev-month"),w.prevMonthNav.innerHTML=w.config.prevArrow,w.nextMonthNav=d("span","flatpickr-next-month"),w.nextMonthNav.innerHTML=w.config.nextArrow,V(),Object.defineProperty(w,"_hidePrevMonthArrow",{get:function(){return w.__hidePrevMonthArrow},set:function(e){w.__hidePrevMonthArrow!==e&&(s(w.prevMonthNav,"flatpickr-disabled",e),w.__hidePrevMonthArrow=e)}}),Object.defineProperty(w,"_hideNextMonthArrow",{get:function(){return w.__hideNextMonthArrow},set:function(e){w.__hideNextMonthArrow!==e&&(s(w.nextMonthNav,"flatpickr-disabled",e),w.__hideNextMonthArrow=e)}}),w.currentYearElement=w.yearElements[0],Ce(),w.monthNav)),w.innerContainer=d("div","flatpickr-innerContainer"),w.config.weekNumbers){var n=function(){w.calendarContainer.classList.add("hasWeeks");var e=d("div","flatpickr-weekwrapper");e.appendChild(d("span","flatpickr-weekday",w.l10n.weekAbbreviation));var n=d("div","flatpickr-weeks");return e.appendChild(n),{weekWrapper:e,weekNumbers:n}}(),t=n.weekWrapper,a=n.weekNumbers;w.innerContainer.appendChild(t),w.weekNumbers=a,w.weekWrapper=t}w.rContainer=d("div","flatpickr-rContainer"),w.rContainer.appendChild(z()),w.daysContainer||(w.daysContainer=d("div","flatpickr-days"),w.daysContainer.tabIndex=-1),U(),w.rContainer.appendChild(w.daysContainer),w.innerContainer.appendChild(w.rContainer),e.appendChild(w.innerContainer)}w.config.enableTime&&e.appendChild(function(){w.calendarContainer.classList.add("hasTime"),w.config.noCalendar&&w.calendarContainer.classList.add("noCalendar");var e=E(w.config);w.timeContainer=d("div","flatpickr-time"),w.timeContainer.tabIndex=-1;var n=d("span","flatpickr-time-separator",":"),t=m("flatpickr-hour",{"aria-label":w.l10n.hourAriaLabel});w.hourElement=t.getElementsByTagName("input")[0];var a=m("flatpickr-minute",{"aria-label":w.l10n.minuteAriaLabel});w.minuteElement=a.getElementsByTagName("input")[0],w.hourElement.tabIndex=w.minuteElement.tabIndex=-1,w.hourElement.value=o(w.latestSelectedDateObj?w.latestSelectedDateObj.getHours():w.config.time_24hr?e.hours:function(e){switch(e%24){case 0:case 12:return 12;default:return e%12}}(e.hours)),w.minuteElement.value=o(w.latestSelectedDateObj?w.latestSelectedDateObj.getMinutes():e.minutes),w.hourElement.setAttribute("step",w.config.hourIncrement.toString()),w.minuteElement.setAttribute("step",w.config.minuteIncrement.toString()),w.hourElement.setAttribute("min",w.config.time_24hr?"0":"1"),w.hourElement.setAttribute("max",w.config.time_24hr?"23":"12"),w.hourElement.setAttribute("maxlength","2"),w.minuteElement.setAttribute("min","0"),w.minuteElement.setAttribute("max","59"),w.minuteElement.setAttribute("maxlength","2"),w.timeContainer.appendChild(t),w.timeContainer.appendChild(n),w.timeContainer.appendChild(a),w.config.time_24hr&&w.timeContainer.classList.add("time24hr");if(w.config.enableSeconds){w.timeContainer.classList.add("hasSeconds");var i=m("flatpickr-second");w.secondElement=i.getElementsByTagName("input")[0],w.secondElement.value=o(w.latestSelectedDateObj?w.latestSelectedDateObj.getSeconds():e.seconds),w.secondElement.setAttribute("step",w.minuteElement.getAttribute("step")),w.secondElement.setAttribute("min","0"),w.secondElement.setAttribute("max","59"),w.secondElement.setAttribute("maxlength","2"),w.timeContainer.appendChild(d("span","flatpickr-time-separator",":")),w.timeContainer.appendChild(i)}w.config.time_24hr||(w.amPM=d("span","flatpickr-am-pm",w.l10n.amPM[r((w.latestSelectedDateObj?w.hourElement.value:w.config.defaultHour)>11)]),w.amPM.title=w.l10n.toggleTitle,w.amPM.tabIndex=-1,w.timeContainer.appendChild(w.amPM));return w.timeContainer}());s(w.calendarContainer,"rangeMode","range"===w.config.mode),s(w.calendarContainer,"animate",!0===w.config.animate),s(w.calendarContainer,"multiMonth",w.config.showMonths>1),w.calendarContainer.appendChild(e);var i=void 0!==w.config.appendTo&&void 0!==w.config.appendTo.nodeType;if((w.config.inline||w.config.static)&&(w.calendarContainer.classList.add(w.config.inline?"inline":"static"),w.config.inline&&(!i&&w.element.parentNode?w.element.parentNode.insertBefore(w.calendarContainer,w._input.nextSibling):void 0!==w.config.appendTo&&w.config.appendTo.appendChild(w.calendarContainer)),w.config.static)){var l=d("div","flatpickr-wrapper");w.element.parentNode&&w.element.parentNode.insertBefore(l,w.element),l.appendChild(w.element),w.altInput&&l.appendChild(w.altInput),l.appendChild(w.calendarContainer)}w.config.static||w.config.inline||(void 0!==w.config.appendTo?w.config.appendTo:window.document.body).appendChild(w.calendarContainer)}(),function(){w.config.wrap&&["open","close","toggle","clear"].forEach((function(e){Array.prototype.forEach.call(w.element.querySelectorAll("[data-"+e+"]"),(function(n){return P(n,"click",w[e])}))}));if(w.isMobile)return void function(){var e=w.config.enableTime?w.config.noCalendar?"time":"datetime-local":"date";w.mobileInput=d("input",w.input.className+" flatpickr-mobile"),w.mobileInput.tabIndex=1,w.mobileInput.type=e,w.mobileInput.disabled=w.input.disabled,w.mobileInput.required=w.input.required,w.mobileInput.placeholder=w.input.placeholder,w.mobileFormatStr="datetime-local"===e?"Y-m-d\\TH:i:S":"date"===e?"Y-m-d":"H:i:S",w.selectedDates.length>0&&(w.mobileInput.defaultValue=w.mobileInput.value=w.formatDate(w.selectedDates[0],w.mobileFormatStr));w.config.minDate&&(w.mobileInput.min=w.formatDate(w.config.minDate,"Y-m-d"));w.config.maxDate&&(w.mobileInput.max=w.formatDate(w.config.maxDate,"Y-m-d"));w.input.getAttribute("step")&&(w.mobileInput.step=String(w.input.getAttribute("step")));w.input.type="hidden",void 0!==w.altInput&&(w.altInput.type="hidden");try{w.input.parentNode&&w.input.parentNode.insertBefore(w.mobileInput,w.input.nextSibling)}catch(e){}P(w.mobileInput,"change",(function(e){w.setDate(g(e).value,!1,w.mobileFormatStr),De("onChange"),De("onClose")}))}();var e=l(re,50);w._debouncedChange=l(Y,300),w.daysContainer&&!/iPhone|iPad|iPod/i.test(navigator.userAgent)&&P(w.daysContainer,"mouseover",(function(e){"range"===w.config.mode&&oe(g(e))}));P(w._input,"keydown",ie),void 0!==w.calendarContainer&&P(w.calendarContainer,"keydown",ie);w.config.inline||w.config.static||P(window,"resize",e);void 0!==window.ontouchstart?P(window.document,"touchstart",X):P(window.document,"mousedown",X);P(window.document,"focus",X,{capture:!0}),!0===w.config.clickOpens&&(P(w._input,"focus",w.open),P(w._input,"click",w.open));void 0!==w.daysContainer&&(P(w.monthNav,"click",xe),P(w.monthNav,["keyup","increment"],N),P(w.daysContainer,"click",me));if(void 0!==w.timeContainer&&void 0!==w.minuteElement&&void 0!==w.hourElement){var n=function(e){return g(e).select()};P(w.timeContainer,["increment"],_),P(w.timeContainer,"blur",_,{capture:!0}),P(w.timeContainer,"click",H),P([w.hourElement,w.minuteElement],["focus","click"],n),void 0!==w.secondElement&&P(w.secondElement,"focus",(function(){return w.secondElement&&w.secondElement.select()})),void 0!==w.amPM&&P(w.amPM,"click",(function(e){_(e)}))}w.config.allowInput&&P(w._input,"blur",ae)}(),(w.selectedDates.length||w.config.noCalendar)&&(w.config.enableTime&&F(w.config.noCalendar?w.latestSelectedDateObj:void 0),ye(!1)),S();var n=/^((?!chrome|android).)*safari/i.test(navigator.userAgent);!w.isMobile&&n&&de(),De("onReady")}(),w}function T(e,n){for(var t=Array.prototype.slice.call(e).filter((function(e){return e instanceof HTMLElement})),a=[],i=0;i { + requestRead('/themes/ranks') // 인기 테마 목록 조회 API endpoint + .then(render) + .catch(error => console.error('Error fetching times:', error)); +}); + +function render(data) { + const container = document.getElementById('theme-ranking'); + + data.forEach(theme => { + const name = theme.name; + const thumbnail = theme.thumbnail; + const description = theme.description; + + const htmlContent = ` + ${name} +
+
${name}
+ ${description} +
+ `; + + const div = document.createElement('li'); + div.className = 'media my-4'; + div.innerHTML = htmlContent; + + container.appendChild(div); + }) +} + +function requestRead(endpoint) { + return fetch(endpoint) + .then(response => { + if (response.status === 200) return response.json(); + throw new Error('Read failed'); + }); +} diff --git a/src/main/resources/static/js/reservation-legacy.js b/src/main/resources/static/js/reservation-legacy.js new file mode 100644 index 000000000..e896878fe --- /dev/null +++ b/src/main/resources/static/js/reservation-legacy.js @@ -0,0 +1,146 @@ +let isEditing = false; +const RESERVATION_API_ENDPOINT = '/reservations'; + +document.addEventListener('DOMContentLoaded', () => { + document.getElementById('add-button').addEventListener('click', addInputRow); + + requestRead(RESERVATION_API_ENDPOINT) + .then(render) + .catch(error => console.error('Error fetching reservations:', error)); +}); + +function render(data) { + const tableBody = document.getElementById('table-body'); + tableBody.innerHTML = ''; + + data.forEach(item => { + const row = tableBody.insertRow(); + + row.insertCell(0).textContent = item.id; + row.insertCell(1).textContent = item.name; + row.insertCell(2).textContent = item.date; + row.insertCell(3).textContent = item.time; + + const actionCell = row.insertCell(row.cells.length); + actionCell.appendChild(createActionButton('삭제', 'btn-danger', deleteRow)); + }); +} + +function createActionButton(label, className, eventListener) { + const button = document.createElement('button'); + button.textContent = label; + button.classList.add('btn', className, 'mr-2'); + button.addEventListener('click', eventListener); + return button; +} + +function addInputRow() { + if (isEditing) return; // 이미 편집 중인 경우 추가하지 않음 + + const tableBody = document.getElementById('table-body'); + const row = tableBody.insertRow(); + isEditing = true; + + const nameInput = createInput('text'); + const dateInput = createInput('date'); + const timeInput = createInput('time'); + + const cellFieldsToCreate = ['', nameInput, dateInput, timeInput]; + + cellFieldsToCreate.forEach((field, index) => { + const cell = row.insertCell(index); + if (typeof field === 'string') { + cell.textContent = field; + } else { + cell.appendChild(field); + } + }); + + const actionCell = row.insertCell(row.cells.length); + actionCell.appendChild(createActionButton('확인', 'btn-custom', saveRow)); + actionCell.appendChild(createActionButton('취소', 'btn-secondary', () => { + row.remove(); + isEditing = false; + })); +} + +function createInput(type) { + const input = document.createElement('input'); + input.type = type; + input.className = 'form-control'; + return input; +} + +function createActionButton(label, className, eventListener) { + const button = document.createElement('button'); + button.textContent = label; + button.classList.add('btn', className, 'mr-2'); + button.addEventListener('click', eventListener); + return button; +} + +function saveRow(event) { + // 이벤트 전파를 막는다 + event.stopPropagation(); + + const row = event.target.parentNode.parentNode; + const nameInput = row.querySelector('input[type="text"]'); + const dateInput = row.querySelector('input[type="date"]'); + const timeInput = row.querySelector('input[type="time"]'); + + const reservation = { + name: nameInput.value, + date: dateInput.value, + time: timeInput.value + }; + + requestCreate(reservation) + .then(() => { + location.reload(); + }) + .catch(error => console.error('Error:', error)); + + isEditing = false; // isEditing 값을 false로 설정 +} + +function deleteRow(event) { + const row = event.target.closest('tr'); + const reservationId = row.cells[0].textContent; + + requestDelete(reservationId) + .then(() => row.remove()) + .catch(error => console.error('Error:', error)); +} + +function requestCreate(reservation) { + const requestOptions = { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(reservation) + }; + + return fetch(RESERVATION_API_ENDPOINT, requestOptions) + .then(response => { + if (response.status === 200) return response.json(); + throw new Error('Create failed'); + }); +} + +function requestDelete(id) { + const requestOptions = { + method: 'DELETE', + }; + + return fetch(`${RESERVATION_API_ENDPOINT}/${id}`, requestOptions) + .then(response => { + if (response.status !== 200) throw new Error('Delete failed'); + }); +} + +function requestRead(endpoint) { + return fetch(endpoint) + .then(response => { + if (response.status === 200) return response.json(); + throw new Error('Read failed'); + }); +} diff --git a/src/main/resources/static/js/reservation-new.js b/src/main/resources/static/js/reservation-new.js new file mode 100644 index 000000000..7fec66226 --- /dev/null +++ b/src/main/resources/static/js/reservation-new.js @@ -0,0 +1,194 @@ +let isEditing = false; +const RESERVATION_API_ENDPOINT = '/reservations'; +const TIME_API_ENDPOINT = '/times'; +const THEME_API_ENDPOINT = '/themes'; +const timesOptions = []; +const themesOptions = []; + +document.addEventListener('DOMContentLoaded', () => { + document.getElementById('add-button').addEventListener('click', addInputRow); + + requestRead(RESERVATION_API_ENDPOINT) + .then(render) + .catch(error => console.error('Error fetching reservations:', error)); + + fetchTimes(); + fetchThemes(); +}); + +function render(data) { + const tableBody = document.getElementById('table-body'); + tableBody.innerHTML = ''; + + data.forEach(item => { + const row = tableBody.insertRow(); + + row.insertCell(0).textContent = item.id; // 예약 id + row.insertCell(1).textContent = item.name; // 예약자명 + row.insertCell(2).textContent = item.theme.name; // 테마명 + row.insertCell(3).textContent = item.date; // 예약 날짜 + row.insertCell(4).textContent = item.time.startAt; // 시작 시간 + + const actionCell = row.insertCell(row.cells.length); + actionCell.appendChild(createActionButton('삭제', 'btn-danger', deleteRow)); + }); +} + +function fetchTimes() { + requestRead(TIME_API_ENDPOINT) + .then(data => { + timesOptions.push(...data); + }) + .catch(error => console.error('Error fetching time:', error)); +} + +function fetchThemes() { + requestRead(THEME_API_ENDPOINT) + .then(data => { + themesOptions.push(...data); + }) + .catch(error => console.error('Error fetching theme:', error)); +} + +function createSelect(options, defaultText, selectId, textProperty) { + const select = document.createElement('select'); + select.className = 'form-control'; + select.id = selectId; + + // 기본 옵션 추가 + const defaultOption = document.createElement('option'); + defaultOption.textContent = defaultText; + select.appendChild(defaultOption); + + // 넘겨받은 옵션을 바탕으로 드롭다운 메뉴 아이템 생성 + options.forEach(optionData => { + const option = document.createElement('option'); + option.value = optionData.id; + option.textContent = optionData[textProperty]; // 동적 속성 접근 + select.appendChild(option); + }); + + return select; +} + +function createActionButton(label, className, eventListener) { + const button = document.createElement('button'); + button.textContent = label; + button.classList.add('btn', className, 'mr-2'); + button.addEventListener('click', eventListener); + return button; +} + +function addInputRow() { + if (isEditing) return; // 이미 편집 중인 경우 추가하지 않음 + + const tableBody = document.getElementById('table-body'); + const row = tableBody.insertRow(); + isEditing = true; + + const nameInput = createInput('text'); + const dateInput = createInput('date'); + const timeDropdown = createSelect(timesOptions, "시간 선택", 'time-select', 'startAt'); + const themeDropdown = createSelect(themesOptions, "테마 선택", 'theme-select', 'name'); + + const cellFieldsToCreate = ['', nameInput, themeDropdown, dateInput, timeDropdown]; + + cellFieldsToCreate.forEach((field, index) => { + const cell = row.insertCell(index); + if (typeof field === 'string') { + cell.textContent = field; + } else { + cell.appendChild(field); + } + }); + + const actionCell = row.insertCell(row.cells.length); + actionCell.appendChild(createActionButton('확인', 'btn-custom', saveRow)); + actionCell.appendChild(createActionButton('취소', 'btn-secondary', () => { + row.remove(); + isEditing = false; + })); +} + +function createInput(type) { + const input = document.createElement('input'); + input.type = type; + input.className = 'form-control'; + return input; +} + +function createActionButton(label, className, eventListener) { + const button = document.createElement('button'); + button.textContent = label; + button.classList.add('btn', className, 'mr-2'); + button.addEventListener('click', eventListener); + return button; +} + +function saveRow(event) { + // 이벤트 전파를 막는다 + event.stopPropagation(); + + const row = event.target.parentNode.parentNode; + const nameInput = row.querySelector('input[type="text"]'); + const themeSelect = row.querySelector('#theme-select'); + const dateInput = row.querySelector('input[type="date"]'); + const timeSelect = row.querySelector('#time-select'); + + const reservation = { + name: nameInput.value, + themeId: themeSelect.value, + date: dateInput.value, + timeId: timeSelect.value + }; + + requestCreate(reservation) + .then(() => { + location.reload(); + }) + .catch(error => console.error('Error:', error)); + + isEditing = false; // isEditing 값을 false로 설정 +} + +function deleteRow(event) { + const row = event.target.closest('tr'); + const reservationId = row.cells[0].textContent; + + requestDelete(reservationId) + .then(() => row.remove()) + .catch(error => console.error('Error:', error)); +} + +function requestCreate(reservation) { + const requestOptions = { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(reservation) + }; + + return fetch(RESERVATION_API_ENDPOINT, requestOptions) + .then(response => { + if (response.status === 201) return response.json(); + throw new Error('Create failed'); + }); +} + +function requestDelete(id) { + const requestOptions = { + method: 'DELETE', + }; + + return fetch(`${RESERVATION_API_ENDPOINT}/${id}`, requestOptions) + .then(response => { + if (response.status !== 204) throw new Error('Delete failed'); + }); +} + +function requestRead(endpoint) { + return fetch(endpoint) + .then(response => { + if (response.status === 200) return response.json(); + throw new Error('Read failed'); + }); +} diff --git a/src/main/resources/static/js/reservation-with-member.js b/src/main/resources/static/js/reservation-with-member.js new file mode 100644 index 000000000..b2aa85c6e --- /dev/null +++ b/src/main/resources/static/js/reservation-with-member.js @@ -0,0 +1,238 @@ +let isEditing = false; +const RESERVATION_API_ENDPOINT = '/reservations'; +const TIME_API_ENDPOINT = '/times'; +const THEME_API_ENDPOINT = '/themes'; +const MEMBER_API_ENDPOINT = '/members'; +const timesOptions = []; +const themesOptions = []; +const membersOptions = []; + +document.addEventListener('DOMContentLoaded', () => { + document.getElementById('add-button').addEventListener('click', addInputRow); + document.getElementById('filter-form').addEventListener('submit', applyFilter); + + requestRead(RESERVATION_API_ENDPOINT) + .then(render) + .catch(error => console.error('Error fetching reservations:', error)); + + fetchTimes(); + fetchThemes(); + fetchMembers(); +}); + +function render(data) { + const tableBody = document.getElementById('table-body'); + tableBody.innerHTML = ''; + + data.forEach(item => { + const row = tableBody.insertRow(); + + row.insertCell(0).textContent = item.id; // 예약 id + row.insertCell(1).textContent = item.member.name; // 사용자 name + row.insertCell(2).textContent = item.theme.name; // 테마 name + row.insertCell(3).textContent = item.date; // date + row.insertCell(4).textContent = item.time.startAt; // 예약 시간 startAt + + const actionCell = row.insertCell(row.cells.length); + actionCell.appendChild(createActionButton('삭제', 'btn-danger', deleteRow)); + }); +} + +function fetchTimes() { + requestRead(TIME_API_ENDPOINT) + .then(data => { + timesOptions.push(...data); + }) + .catch(error => console.error('Error fetching time:', error)); +} + +function fetchThemes() { + requestRead(THEME_API_ENDPOINT) + .then(data => { + themesOptions.push(...data); + populateSelect('theme', themesOptions, 'name'); + }) + .catch(error => console.error('Error fetching theme:', error)); +} + +function fetchMembers() { + requestRead(MEMBER_API_ENDPOINT) + .then(data => { + membersOptions.push(...data); + populateSelect('member', membersOptions, 'name'); + }) + .catch(error => console.error('Error fetching member:', error)); +} + +function populateSelect(selectId, options, textProperty) { + const select = document.getElementById(selectId); + options.forEach(optionData => { + const option = document.createElement('option'); + option.value = optionData.id; + option.textContent = optionData[textProperty]; + select.appendChild(option); + }); +} + +function createSelect(options, defaultText, selectId, textProperty) { + const select = document.createElement('select'); + select.className = 'form-control'; + select.id = selectId; + + // 기본 옵션 추가 + const defaultOption = document.createElement('option'); + defaultOption.textContent = defaultText; + select.appendChild(defaultOption); + + // 넘겨받은 옵션을 바탕으로 드롭다운 메뉴 아이템 생성 + options.forEach(optionData => { + const option = document.createElement('option'); + option.value = optionData.id; + option.textContent = optionData[textProperty]; // 동적 속성 접근 + select.appendChild(option); + }); + + return select; +} + +function createActionButton(label, className, eventListener) { + const button = document.createElement('button'); + button.textContent = label; + button.classList.add('btn', className, 'mr-2'); + button.addEventListener('click', eventListener); + return button; +} + +function addInputRow() { + if (isEditing) return; // 이미 편집 중인 경우 추가하지 않음 + + const tableBody = document.getElementById('table-body'); + const row = tableBody.insertRow(); + isEditing = true; + + const dateInput = createInput('date'); + const timeDropdown = createSelect(timesOptions, "시간 선택", 'time-select', 'startAt'); + const themeDropdown = createSelect(themesOptions, "테마 선택", 'theme-select', 'name'); + const memberDropdown = createSelect(membersOptions, "멤버 선택", 'member-select', 'name'); + + const cellFieldsToCreate = ['', memberDropdown, themeDropdown, dateInput, timeDropdown]; + + cellFieldsToCreate.forEach((field, index) => { + const cell = row.insertCell(index); + if (typeof field === 'string') { + cell.textContent = field; + } else { + cell.appendChild(field); + } + }); + + const actionCell = row.insertCell(row.cells.length); + actionCell.appendChild(createActionButton('확인', 'btn-custom', saveRow)); + actionCell.appendChild(createActionButton('취소', 'btn-secondary', () => { + row.remove(); + isEditing = false; + })); +} + +function createInput(type) { + const input = document.createElement('input'); + input.type = type; + input.className = 'form-control'; + return input; +} + +function createActionButton(label, className, eventListener) { + const button = document.createElement('button'); + button.textContent = label; + button.classList.add('btn', className, 'mr-2'); + button.addEventListener('click', eventListener); + return button; +} + +function saveRow(event) { + // 이벤트 전파를 막는다 + event.stopPropagation(); + + const row = event.target.parentNode.parentNode; + const dateInput = row.querySelector('input[type="date"]'); + const memberSelect = row.querySelector('#member-select'); + const themeSelect = row.querySelector('#theme-select'); + const timeSelect = row.querySelector('#time-select'); + + const reservation = { + date: dateInput.value, + themeId: themeSelect.value, + timeId: timeSelect.value, + memberId: memberSelect.value, + }; + + requestCreate(reservation) + .then(() => { + location.reload(); + }) + .catch(error => console.error('Error:', error)); + + isEditing = false; // isEditing 값을 false로 설정 +} + +function deleteRow(event) { + const row = event.target.closest('tr'); + const reservationId = row.cells[0].textContent; + + requestDelete(reservationId) + .then(() => row.remove()) + .catch(error => console.error('Error:', error)); +} + +function applyFilter(event) { + event.preventDefault(); + + const themeId = document.getElementById('theme').value; + const memberId = document.getElementById('member').value; + const dateFrom = document.getElementById('date-from').value; + const dateTo = document.getElementById('date-to').value; + + fetch(`/admin/reservations/search?memberId=${memberId}&themeId=${themeId}&dateFrom=${dateFrom}&dateTo=${dateTo}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + }, + }).then(response => { + if (response.status === 200) return response.json(); + throw new Error('Read failed'); + }).then(render) + .catch(error => console.error("Error fetching available times:", error)); +} + +function requestCreate(reservation) { + const requestOptions = { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(reservation) + }; + + return fetch('/admin/reservations', requestOptions) + .then(response => { + if (response.status === 201) return response.json(); + throw new Error('Create failed'); + }); +} + +function requestDelete(id) { + const requestOptions = { + method: 'DELETE', + }; + + return fetch(`${RESERVATION_API_ENDPOINT}/${id}`, requestOptions) + .then(response => { + if (response.status !== 204) throw new Error('Delete failed'); + }); +} + +function requestRead(endpoint) { + return fetch(endpoint) + .then(response => { + if (response.status === 200) return response.json(); + throw new Error('Read failed'); + }); +} diff --git a/src/main/resources/static/js/reservation.js b/src/main/resources/static/js/reservation.js new file mode 100644 index 000000000..909d517ac --- /dev/null +++ b/src/main/resources/static/js/reservation.js @@ -0,0 +1,179 @@ +let isEditing = false; +const RESERVATION_API_ENDPOINT = '/reservations'; +const TIME_API_ENDPOINT = '/times'; +const timesOptions = []; + +document.addEventListener('DOMContentLoaded', () => { + document.getElementById('add-button').addEventListener('click', addInputRow); + + requestRead(RESERVATION_API_ENDPOINT) + .then(render) + .catch(error => console.error('Error fetching reservations:', error)); + + fetchTimes(); +}); + +function render(data) { + const tableBody = document.getElementById('table-body'); + tableBody.innerHTML = ''; + + data.forEach(item => { + const row = tableBody.insertRow(); + + row.insertCell(0).textContent = item.id; + row.insertCell(1).textContent = item.name; + row.insertCell(2).textContent = item.date; + row.insertCell(3).textContent = item.time.startAt; + + const actionCell = row.insertCell(row.cells.length); + actionCell.appendChild(createActionButton('삭제', 'btn-danger', deleteRow)); + }); +} + +function fetchTimes() { + requestRead(TIME_API_ENDPOINT) + .then(data => { + timesOptions.push(...data); + }) + .catch(error => console.error('Error fetching time:', error)); +} + +function createSelect(options, defaultText, selectId, textProperty) { + const select = document.createElement('select'); + select.className = 'form-control'; + select.id = selectId; + + // 기본 옵션 추가 + const defaultOption = document.createElement('option'); + defaultOption.textContent = defaultText; + select.appendChild(defaultOption); + + // 넘겨받은 옵션을 바탕으로 드롭다운 메뉴 아이템 생성 + options.forEach(optionData => { + const option = document.createElement('option'); + option.value = optionData.id; + option.textContent = optionData[textProperty]; // 동적 속성 접근 + select.appendChild(option); + }); + + return select; +} + +function createActionButton(label, className, eventListener) { + const button = document.createElement('button'); + button.textContent = label; + button.classList.add('btn', className, 'mr-2'); + button.addEventListener('click', eventListener); + return button; +} + +function addInputRow() { + if (isEditing) return; // 이미 편집 중인 경우 추가하지 않음 + + const tableBody = document.getElementById('table-body'); + const row = tableBody.insertRow(); + isEditing = true; + + const nameInput = createInput('text'); + const dateInput = createInput('date'); + const timeDropdown = createSelect(timesOptions, "시간 선택", 'time-select', 'startAt'); + + const cellFieldsToCreate = ['', nameInput, dateInput, timeDropdown]; + + cellFieldsToCreate.forEach((field, index) => { + const cell = row.insertCell(index); + if (typeof field === 'string') { + cell.textContent = field; + } else { + cell.appendChild(field); + } + }); + + const actionCell = row.insertCell(row.cells.length); + actionCell.appendChild(createActionButton('확인', 'btn-custom', saveRow)); + actionCell.appendChild(createActionButton('취소', 'btn-secondary', () => { + row.remove(); + isEditing = false; + })); +} + +function createInput(type) { + const input = document.createElement('input'); + input.type = type; + input.className = 'form-control'; + return input; +} + +function createActionButton(label, className, eventListener) { + const button = document.createElement('button'); + button.textContent = label; + button.classList.add('btn', className, 'mr-2'); + button.addEventListener('click', eventListener); + return button; +} + +function saveRow(event) { + // 이벤트 전파를 막는다 + event.stopPropagation(); + + const row = event.target.parentNode.parentNode; + const nameInput = row.querySelector('input[type="text"]'); + const dateInput = row.querySelector('input[type="date"]'); + const timeSelect = row.querySelector('select'); + + const reservation = { + name: nameInput.value, + date: dateInput.value, + timeId: timeSelect.value + }; + + requestCreate(reservation) + .then(() => { + location.reload(); + }) + .catch(error => console.error('Error:', error)); + + isEditing = false; // isEditing 값을 false로 설정 +} + +function deleteRow(event) { + const row = event.target.closest('tr'); + const reservationId = row.cells[0].textContent; + + requestDelete(reservationId) + .then(() => row.remove()) + .catch(error => console.error('Error:', error)); +} + +function requestCreate(reservation) { + const requestOptions = { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(reservation) + }; + + return fetch(RESERVATION_API_ENDPOINT, requestOptions) + .then(response => { + if (response.status === 201) return response.json(); + throw new Error('Create failed'); + }); +} + +function requestDelete(id) { + const requestOptions = { + method: 'DELETE', + }; + + return fetch(`${RESERVATION_API_ENDPOINT}/${id}`, requestOptions) + .then(response => { + if (response.status !== 204) throw new Error('Delete failed'); + }); +} + +function requestRead(endpoint) { + return fetch(endpoint) + .then(response => { + if (response.status === 200) return response.json(); + throw new Error('Read failed'); + }); +} diff --git a/src/main/resources/static/js/scripts.js b/src/main/resources/static/js/scripts.js new file mode 100644 index 000000000..e69de29bb diff --git a/src/main/resources/static/js/theme.js b/src/main/resources/static/js/theme.js new file mode 100644 index 000000000..a2ddee91e --- /dev/null +++ b/src/main/resources/static/js/theme.js @@ -0,0 +1,136 @@ +let isEditing = false; +const API_ENDPOINT = '/themes'; +const cellFields = ['id', 'name', 'description', 'thumbnail']; +const createCellFields = ['', createInput(), createInput(), createInput()]; + +function createBody(inputs) { + return { + name: inputs[0].value, + description: inputs[1].value, + thumbnail: inputs[2].value, + }; +} + +document.addEventListener('DOMContentLoaded', () => { + document.getElementById('add-button').addEventListener('click', addRow); + requestRead() + .then(render) + .catch(error => console.error('Error fetching times:', error)); +}); + +function render(data) { + const tableBody = document.getElementById('table-body'); + tableBody.innerHTML = ''; + + data.forEach(item => { + const row = tableBody.insertRow(); + + cellFields.forEach((field, index) => { + row.insertCell(index).textContent = item[field]; + }); + + const actionCell = row.insertCell(row.cells.length); + actionCell.appendChild(createActionButton('삭제', 'btn-danger', deleteRow)); + }); +} + +function addRow() { + if (isEditing) return; // 이미 편집 중인 경우 추가하지 않음 + + const tableBody = document.getElementById('table-body'); + const row = tableBody.insertRow(); + isEditing = true; + + createAddField(row); +} + +function createAddField(row) { + createCellFields.forEach((field, index) => { + const cell = row.insertCell(index); + if (typeof field === 'string') { + cell.textContent = field; + } else { + cell.appendChild(field); + } + }); + + const actionCell = row.insertCell(row.cells.length); + actionCell.appendChild(createActionButton('확인', 'btn-custom', saveRow)); + actionCell.appendChild(createActionButton('취소', 'btn-secondary', () => { + row.remove(); + isEditing = false; + })); +} + +function createInput() { + const input = document.createElement('input'); + input.className = 'form-control'; + return input; +} + +function createActionButton(label, className, eventListener) { + const button = document.createElement('button'); + button.textContent = label; + button.classList.add('btn', className, 'mr-2'); + button.addEventListener('click', eventListener); + return button; +} + +function saveRow(event) { + const row = event.target.parentNode.parentNode; + const inputs = row.querySelectorAll('input'); + const body = createBody(inputs); + + requestCreate(body) + .then(() => { + location.reload(); + }) + .catch(error => console.error('Error:', error)); + + isEditing = false; // isEditing 값을 false로 설정 +} + +function deleteRow(event) { + const row = event.target.closest('tr'); + const id = row.cells[0].textContent; + + requestDelete(id) + .then(() => row.remove()) + .catch(error => console.error('Error:', error)); +} + + +// request + +function requestCreate(data) { + const requestOptions = { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(data) + }; + + return fetch(API_ENDPOINT, requestOptions) + .then(response => { + if (response.status === 201) return response.json(); + throw new Error('Create failed'); + }); +} + +function requestRead() { + return fetch(API_ENDPOINT) + .then(response => { + if (response.status === 200) return response.json(); + throw new Error('Read failed'); + }); +} + +function requestDelete(id) { + const requestOptions = { + method: 'DELETE', + }; + + return fetch(`${API_ENDPOINT}/${id}`, requestOptions) + .then(response => { + if (response.status !== 204) throw new Error('Delete failed'); + }); +} diff --git a/src/main/resources/static/js/time.js b/src/main/resources/static/js/time.js new file mode 100644 index 000000000..641023a6d --- /dev/null +++ b/src/main/resources/static/js/time.js @@ -0,0 +1,135 @@ +let isEditing = false; +const API_ENDPOINT = '/times'; +const cellFields = ['id', 'startAt']; +const createCellFields = ['', createInput()]; + +function createBody(inputs) { + return { + startAt: inputs[0].value, + }; +} + +document.addEventListener('DOMContentLoaded', () => { + document.getElementById('add-button').addEventListener('click', addRow); + requestRead() + .then(render) + .catch(error => console.error('Error fetching times:', error)); +}); + +function render(data) { + const tableBody = document.getElementById('table-body'); + tableBody.innerHTML = ''; + + data.forEach(item => { + const row = tableBody.insertRow(); + + cellFields.forEach((field, index) => { + row.insertCell(index).textContent = item[field]; + }); + + const actionCell = row.insertCell(row.cells.length); + actionCell.appendChild(createActionButton('삭제', 'btn-danger', deleteRow)); + }); +} + +function addRow() { + if (isEditing) return; // 이미 편집 중인 경우 추가하지 않음 + + const tableBody = document.getElementById('table-body'); + const row = tableBody.insertRow(); + isEditing = true; + + createAddField(row); +} + +function createAddField(row) { + createCellFields.forEach((field, index) => { + const cell = row.insertCell(index); + if (typeof field === 'string') { + cell.textContent = field; + } else { + cell.appendChild(field); + } + }); + + const actionCell = row.insertCell(row.cells.length); + actionCell.appendChild(createActionButton('확인', 'btn-custom', saveRow)); + actionCell.appendChild(createActionButton('취소', 'btn-secondary', () => { + row.remove(); + isEditing = false; + })); +} + +function createInput() { + const input = document.createElement('input'); + input.type = 'time' + input.className = 'form-control'; + return input; +} + +function createActionButton(label, className, eventListener) { + const button = document.createElement('button'); + button.textContent = label; + button.classList.add('btn', className, 'mr-2'); + button.addEventListener('click', eventListener); + return button; +} + +function saveRow(event) { + const row = event.target.parentNode.parentNode; + const inputs = row.querySelectorAll('input'); + const body = createBody(inputs); + + requestCreate(body) + .then(() => { + location.reload(); + }) + .catch(error => console.error('Error:', error)); + + isEditing = false; // isEditing 값을 false로 설정 +} + +function deleteRow(event) { + const row = event.target.closest('tr'); + const id = row.cells[0].textContent; + + requestDelete(id) + .then(() => row.remove()) + .catch(error => console.error('Error:', error)); +} + + +// request + +function requestCreate(data) { + const requestOptions = { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(data) + }; + + return fetch(API_ENDPOINT, requestOptions) + .then(response => { + if (response.status === 201) return response.json(); + throw new Error('Create failed'); + }); +} + +function requestRead() { + return fetch(API_ENDPOINT) + .then(response => { + if (response.status === 200) return response.json(); + throw new Error('Read failed'); + }); +} + +function requestDelete(id) { + const requestOptions = { + method: 'DELETE', + }; + + return fetch(`${API_ENDPOINT}/${id}`, requestOptions) + .then(response => { + if (response.status !== 204) throw new Error('Delete failed'); + }); +} diff --git a/src/main/resources/static/js/user-reservation.js b/src/main/resources/static/js/user-reservation.js new file mode 100644 index 000000000..1c324a2b8 --- /dev/null +++ b/src/main/resources/static/js/user-reservation.js @@ -0,0 +1,182 @@ +const THEME_API_ENDPOINT = '/themes'; + +document.addEventListener('DOMContentLoaded', () => { + requestRead(THEME_API_ENDPOINT) + .then(renderTheme) + .catch(error => console.error('Error fetching times:', error)); + + flatpickr("#datepicker", { + inline: true, + onChange: function (selectedDates, dateStr, instance) { + if (dateStr === '') return; + checkDate(); + } + }); + + document.getElementById('theme-slots').addEventListener('click', event => { + if (event.target.classList.contains('theme-slot')) { + document.querySelectorAll('.theme-slot').forEach(slot => slot.classList.remove('active')); + event.target.classList.add('active'); + checkDateAndTheme(); + } + }); + + document.getElementById('time-slots').addEventListener('click', event => { + if (event.target.classList.contains('time-slot') && !event.target.classList.contains('disabled')) { + document.querySelectorAll('.time-slot').forEach(slot => slot.classList.remove('active')); + event.target.classList.add('active'); + checkDateAndThemeAndTime(); + } + }); + + document.getElementById('reserve-button').addEventListener('click', onReservationButtonClick); +}); + +function renderTheme(themes) { + const themeSlots = document.getElementById('theme-slots'); + themeSlots.innerHTML = ''; + themes.forEach(theme => { + const name = theme.name; + const themeId = theme.id; + themeSlots.appendChild(createSlot('theme', name, themeId)); + }); +} + +function createSlot(type, text, id, booked) { + const div = document.createElement('div'); + div.className = type + '-slot cursor-pointer bg-light border rounded p-3 mb-2'; + div.textContent = text; + div.setAttribute('data-' + type + '-id', id); + if (type === 'time') { + div.setAttribute('data-time-booked', booked); + if (booked) { + div.classList.add('disabled'); + } + } + return div; +} + +function checkDate() { + const selectedDate = document.getElementById("datepicker").value; + if (selectedDate) { + const themeSection = document.getElementById("theme-section"); + if (themeSection.classList.contains("disabled")) { + themeSection.classList.remove("disabled"); + } + const timeSlots = document.getElementById('time-slots'); + timeSlots.innerHTML = ''; + + requestRead(THEME_API_ENDPOINT) + .then(renderTheme) + .catch(error => console.error('Error fetching times:', error)); + } +} + +function checkDateAndTheme() { + const selectedDate = document.getElementById("datepicker").value; + const selectedThemeElement = document.querySelector('.theme-slot.active'); + if (selectedDate && selectedThemeElement) { + const selectedThemeId = selectedThemeElement.getAttribute('data-theme-id'); + fetchAvailableTimes(selectedDate, selectedThemeId); + } +} + +function fetchAvailableTimes(date, themeId) { + fetch(`/times/available?date=${date}&themeId=${themeId}`, { // 예약 가능 시간 조회 API endpoint + method: 'GET', + headers: { + 'Content-Type': 'application/json', + } + }).then(response => { + if (response.status === 200) return response.json(); + throw new Error('Read failed'); + }).then(renderAvailableTimes) + .catch(error => console.error("Error fetching available times:", error)); +} + +function renderAvailableTimes(times) { + const timeSection = document.getElementById("time-section"); + if (timeSection.classList.contains("disabled")) { + timeSection.classList.remove("disabled"); + } + + const timeSlots = document.getElementById('time-slots'); + timeSlots.innerHTML = ''; + if (times.length === 0) { + timeSlots.innerHTML = '
선택할 수 있는 시간이 없습니다.
'; + return; + } + times.forEach(time => { + const startAt = time.startAt; + const timeId = time.timeId; + const alreadyBooked = time.alreadyBooked; + + const div = createSlot('time', startAt, timeId, alreadyBooked); // createSlot('time', 시작 시간, time id, 예약 여부) + timeSlots.appendChild(div); + }); +} + +function checkDateAndThemeAndTime() { + const selectedDate = document.getElementById("datepicker").value; + const selectedThemeElement = document.querySelector('.theme-slot.active'); + const selectedTimeElement = document.querySelector('.time-slot.active'); + const reserveButton = document.getElementById("reserve-button"); + + if (selectedDate && selectedThemeElement && selectedTimeElement) { + if (selectedTimeElement.getAttribute('data-time-booked') === 'true') { + // 선택된 시간이 이미 예약된 경우 + reserveButton.classList.add("disabled"); + } else { + // 선택된 시간이 예약 가능한 경우 + reserveButton.classList.remove("disabled"); + } + } else { + // 날짜, 테마, 시간 중 하나라도 선택되지 않은 경우 + reserveButton.classList.add("disabled"); + } +} + +function onReservationButtonClick() { + const selectedDate = document.getElementById("datepicker").value; + const selectedThemeId = document.querySelector('.theme-slot.active')?.getAttribute('data-theme-id'); + const selectedTimeId = document.querySelector('.time-slot.active')?.getAttribute('data-time-id'); + + if (selectedDate && selectedThemeId && selectedTimeId) { + + const reservationData = { + date: selectedDate, + themeId: selectedThemeId, + timeId: selectedTimeId, + }; + + fetch('/reservations', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(reservationData) + }) + .then(response => { + if (!response.ok) throw new Error('Reservation failed'); + return response.json(); + }) + .then(data => { + alert("Reservation successful!"); + location.reload(); + }) + .catch(error => { + alert("An error occurred while making the reservation."); + console.error(error); + }); + } else { + alert("Please select a date, theme, and time before making a reservation."); + } +} + +function requestRead(endpoint) { + return fetch(endpoint) + .then(response => { + if (response.status === 200) return response.json(); + throw new Error('Read failed'); + }); +} diff --git a/src/main/resources/static/js/user-scripts.js b/src/main/resources/static/js/user-scripts.js new file mode 100644 index 000000000..cb181e9d8 --- /dev/null +++ b/src/main/resources/static/js/user-scripts.js @@ -0,0 +1,152 @@ +document.addEventListener('DOMContentLoaded', function () { + updateUIBasedOnLogin(); +}); + +document.getElementById('logout-btn').addEventListener('click', function (event) { + event.preventDefault(); + fetch('/logout', { + method: 'POST', // 또는 서버 설정에 따라 GET 일 수도 있음 + credentials: 'include' // 쿠키를 포함시키기 위해 필요 + }) + .then(response => { + if (response.ok) { + // 로그아웃 성공, 페이지 새로고침 또는 리다이렉트 + window.location.reload(); + } else { + // 로그아웃 실패 처리 + console.error('Logout failed'); + } + }) + .catch(error => { + console.error('Error:', error); + }); +}); + +function updateUIBasedOnLogin() { + fetch('/login/check') // 로그인 상태 확인 API 호출 + .then(response => { + if (!response.ok) { // 요청이 실패하거나 로그인 상태가 아닌 경우 + throw new Error('Not logged in or other error'); + } + return response.json(); // 응답 본문을 JSON으로 파싱 + }) + .then(data => { + // 응답에서 사용자 이름을 추출하여 UI 업데이트 + document.getElementById('profile-name').textContent = data.name; // 프로필 이름 설정 + document.querySelector('.nav-item.dropdown').style.display = 'block'; // 드롭다운 메뉴 표시 + document.querySelector('.nav-item a[href="/login"]').parentElement.style.display = 'none'; // 로그인 버튼 숨김 + }) + .catch(error => { + // 에러 처리 또는 로그아웃 상태일 때 UI 업데이트 + console.error('Error:', error); + document.getElementById('profile-name').textContent = 'Profile'; // 기본 텍스트로 재설정 + document.querySelector('.nav-item.dropdown').style.display = 'none'; // 드롭다운 메뉴 숨김 + document.querySelector('.nav-item a[href="/login"]').parentElement.style.display = 'block'; // 로그인 버튼 표시 + }); +} + +// 드롭다운 메뉴 토글 +document.getElementById("navbarDropdown").addEventListener('click', function (e) { + e.preventDefault(); + const dropdownMenu = e.target.closest('.nav-item.dropdown').querySelector('.dropdown-menu'); + dropdownMenu.classList.toggle('show'); // Bootstrap 4에서는 data-toggle 사용, Bootstrap 5에서는 JS로 처리 +}); + + +function login() { + const email = document.getElementById('email').value; + const password = document.getElementById('password').value; + + // 입력 필드 검증 + if (!email || !password) { + alert('Please fill in all fields.'); + return; // 필수 입력 필드가 비어있으면 여기서 함수 실행을 중단 + } + + fetch('/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email: email, + password: password + }) + }) + .then(response => { + if (response.status !== 200) { + alert('Login failed'); // 로그인 실패 시 경고창 표시 + throw new Error('Login failed'); + } + }) + .then(() => { + updateUIBasedOnLogin(); // UI 업데이트 + window.location.href = '/'; + }) + .catch(error => { + console.error('Error during login:', error); + }); +} + +function signup() { + // Redirect to signup page + window.location.href = '/signup'; +} + +function register(event) { + // 폼 데이터 수집 + const email = document.getElementById('email').value; + const password = document.getElementById('password').value; + const name = document.getElementById('name').value; + + // 입력 필드 검증 + if (!email || !password || !name) { + alert('Please fill in all fields.'); + return; // 필수 입력 필드가 비어있으면 여기서 함수 실행을 중단 + } + + // 요청 데이터 포맷팅 + const formData = { + email: email, + password: password, + name: name + }; + + // AJAX 요청 생성 및 전송 + fetch('/members', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(formData) + }) + .then(response => { + if (!response.ok) { + alert('Signup request failed'); + throw new Error('Signup request failed'); + } + return response.json(); // 여기서 응답을 JSON 형태로 변환 + }) + .then(data => { + // 성공적인 응답 처리 + console.log('Signup successful:', data); + window.location.href = '/login'; + }) + .catch(error => { + // 에러 처리 + console.error('Error during signup:', error); + }); + + // 폼 제출에 의한 페이지 리로드 방지 + event.preventDefault(); +} + +function base64DecodeUnicode(str) { + // Base64 디코딩 + const decodedBytes = atob(str); + // UTF-8 바이트를 문자열로 변환 + const encodedUriComponent = decodedBytes.split('').map(function (c) { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); + }).join(''); + return decodeURIComponent(encodedUriComponent); +} diff --git a/src/main/resources/templates/admin/index.html b/src/main/resources/templates/admin/index.html new file mode 100644 index 000000000..ae67b26f7 --- /dev/null +++ b/src/main/resources/templates/admin/index.html @@ -0,0 +1,58 @@ + + + + + + 방탈출 어드민 + + + + + + + + +
+

방탈출 어드민

+
+ + + + diff --git a/src/main/resources/templates/admin/reservation-legacy.html b/src/main/resources/templates/admin/reservation-legacy.html new file mode 100644 index 000000000..320ef3c70 --- /dev/null +++ b/src/main/resources/templates/admin/reservation-legacy.html @@ -0,0 +1,57 @@ + + + + + + 방탈출 어드민 + + + + + + + + +
+

방탈출 예약 페이지

+
+ +
+
+ + + + + + + + + + + + +
예약번호예약자날짜시간
+
+ + + + diff --git a/src/main/resources/templates/admin/reservation-new.html b/src/main/resources/templates/admin/reservation-new.html new file mode 100644 index 000000000..4e47e8025 --- /dev/null +++ b/src/main/resources/templates/admin/reservation-new.html @@ -0,0 +1,105 @@ + + + + + + 방탈출 어드민 + + + + + + + + +
+

방탈출 예약 페이지

+
+
+
+ +
+ + + + + + + + + + + + + +
예약번호예약자테마날짜시간
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+ + + + + diff --git a/src/main/resources/templates/admin/reservation.html b/src/main/resources/templates/admin/reservation.html new file mode 100644 index 000000000..9c29ca187 --- /dev/null +++ b/src/main/resources/templates/admin/reservation.html @@ -0,0 +1,60 @@ + + + + + + 방탈출 어드민 + + + + + + + + +
+

방탈출 예약 페이지

+
+ +
+
+ + + + + + + + + + + + +
예약번호예약자날짜시간
+
+ + + + diff --git a/src/main/resources/templates/admin/theme.html b/src/main/resources/templates/admin/theme.html new file mode 100644 index 000000000..58c583a61 --- /dev/null +++ b/src/main/resources/templates/admin/theme.html @@ -0,0 +1,76 @@ + + + + + + 방탈출 어드민 + + + + + + + + +
+

테마 관리 페이지

+
+ +
+
+ + + + + + + + + + + + +
순서제목설명썸네일 URL
+
+ + + + + diff --git a/src/main/resources/templates/admin/time.html b/src/main/resources/templates/admin/time.html new file mode 100644 index 000000000..9eb5ae77d --- /dev/null +++ b/src/main/resources/templates/admin/time.html @@ -0,0 +1,74 @@ + + + + + + 방탈출 어드민 + + + + + + + + +
+

시간 관리 페이지

+
+ +
+
+ + + + + + + + + + +
순서시간
+
+ + + + + diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html new file mode 100644 index 000000000..9740e2ef8 --- /dev/null +++ b/src/main/resources/templates/index.html @@ -0,0 +1,56 @@ + + + + + + 방탈출 예약 페이지 + + + + + + + + +
+

인기 테마

+
    +
+
+ + + + + + diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html new file mode 100644 index 000000000..8faab43fc --- /dev/null +++ b/src/main/resources/templates/login.html @@ -0,0 +1,64 @@ + + + + + + Login + + + + + + + + +
+

Login

+
+
+ +
+
+ +
+
+ + +
+
+
+ + + + diff --git a/src/main/resources/templates/reservation.html b/src/main/resources/templates/reservation.html new file mode 100644 index 000000000..d905724b9 --- /dev/null +++ b/src/main/resources/templates/reservation.html @@ -0,0 +1,89 @@ + + + + + + 방탈출 예약 페이지 + + + + + + + + + + + + +
+

예약 페이지

+
+ +
+

날짜 선택

+
+
+
+
+ + +
+

테마 선택

+
+ +
+
+ + +
+

시간 선택

+
+ +
+
+
+ + +
+ +
+
+
+ + + + + + diff --git a/src/main/resources/templates/signup.html b/src/main/resources/templates/signup.html new file mode 100644 index 000000000..6f044d2f0 --- /dev/null +++ b/src/main/resources/templates/signup.html @@ -0,0 +1,67 @@ + + + + + + Signup + + + + + + + + +
+

Signup

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + + + diff --git a/src/test/java/roomescape/JDBCTest.java b/src/test/java/roomescape/JDBCTest.java new file mode 100644 index 000000000..3dbc8b2fc --- /dev/null +++ b/src/test/java/roomescape/JDBCTest.java @@ -0,0 +1,31 @@ +package roomescape; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.sql.Connection; +import java.sql.SQLException; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +public class JDBCTest { + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Test + @DisplayName("예약 테이블이 데이터베이스에 정상적으로 생성된다.") + void makeReservationTable_Success() { + try (Connection connection = jdbcTemplate.getDataSource().getConnection()) { + assertThat(connection).isNotNull(); + assertThat(connection.getCatalog()).isEqualTo("DATABASE"); + assertThat(connection.getMetaData().getTables(null, null, "RESERVATION", null).next()).isTrue(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/roomescape/RoomescapeApplicationTest.java b/src/test/java/roomescape/RoomescapeApplicationTest.java deleted file mode 100644 index 326a3ff67..000000000 --- a/src/test/java/roomescape/RoomescapeApplicationTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package roomescape; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class RoomescapeApplicationTest { - - @Test - void contextLoads() { - } - -} diff --git a/src/test/java/roomescape/controller/api/AuthApiControllerTest.java b/src/test/java/roomescape/controller/api/AuthApiControllerTest.java new file mode 100644 index 000000000..15e8bd80a --- /dev/null +++ b/src/test/java/roomescape/controller/api/AuthApiControllerTest.java @@ -0,0 +1,58 @@ +package roomescape.controller.api; + +import io.restassured.RestAssured; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import roomescape.service.dto.request.LoginRequest; +import roomescape.service.dto.response.MemberIdAndNameResponse; +import roomescape.util.TokenGenerator; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +class AuthApiControllerTest { + + @Test + @DisplayName("올바른 로그인 정보를 입력할 시, 로그인에 성공한다.") + void authenticatedMemberLogin_Success() { + RestAssured.given().log().all() + .body(new LoginRequest("user@naver.com", "1234")) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .when().post("/login") + .then().log().all() + .statusCode(200); + } + + @Test + @DisplayName("잘못된 로그인 정보를 입력할 시, 로그인에 실패한다.") + void authenticatedMemberLogin_Failure() { + RestAssured.given().log().all() + .body(new LoginRequest("wrong@naver.com", "1234")) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .when().post("/login") + .then().log().all() + .statusCode(401); + } + + @Test + @DisplayName("로그인한 사용자의 인증 정보 조회 시, 성공한다.") + void authenticatedMemberLoginCheck_Success() { + MemberIdAndNameResponse response = RestAssured.given().log().all() + .cookie("token", TokenGenerator.makeUserToken()) + .when().get("/login/check") + .then().log().all() + .statusCode(200).extract().as(MemberIdAndNameResponse.class); + + Assertions.assertThat(response.name()).isEqualTo("testUser"); + } + + @Test + @DisplayName("로그인하지 않은 사용자의 인증 정보 조회 시, 실패한다.") + void authenticatedMemberLoginCheck_Failure() { + RestAssured.given().log().all() + .when().get("/login/check") + .then().log().all() + .statusCode(401); + } +} diff --git a/src/test/java/roomescape/controller/api/MemberApiControllerTest.java b/src/test/java/roomescape/controller/api/MemberApiControllerTest.java new file mode 100644 index 000000000..eb8248359 --- /dev/null +++ b/src/test/java/roomescape/controller/api/MemberApiControllerTest.java @@ -0,0 +1,24 @@ +package roomescape.controller.api; + +import io.restassured.RestAssured; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import roomescape.util.TokenGenerator; + +import static org.hamcrest.Matchers.is; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +class MemberApiControllerTest { + + @Test + @DisplayName("유저 목록 조회 요청이 정상적으로 수행된다.") + void selectMembers_Success() { + RestAssured.given().log().all() + .cookie("token", TokenGenerator.makeAdminToken()) + .when().get("/members") + .then().log().all() + .statusCode(200) + .body("size()", is(2)); + } +} diff --git a/src/test/java/roomescape/controller/api/ReservationApiControllerTest.java b/src/test/java/roomescape/controller/api/ReservationApiControllerTest.java new file mode 100644 index 000000000..ac48e44df --- /dev/null +++ b/src/test/java/roomescape/controller/api/ReservationApiControllerTest.java @@ -0,0 +1,110 @@ +package roomescape.controller.api; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.annotation.DirtiesContext; +import roomescape.util.TokenGenerator; + +import java.lang.reflect.Field; +import java.time.LocalDate; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.is; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +public class ReservationApiControllerTest { + + @Autowired + private ReservationApiController reservationApiController; + + @Test + @DisplayName("관리자 예약 페이지 요청이 정상적으로 수행된다.") + void moveToReservationPage_Success() { + RestAssured.given().log().all() + .cookie("token", TokenGenerator.makeAdminToken()) + .when().get("/admin/reservation") + .then().log().all() + .statusCode(200); + } + + @Test + @DisplayName("관리자 예약 페이지에 권한이 없는 유저는 401을 받는다.") + void moveToReservationPage_Failure() { + RestAssured.given().log().all() + .when().get("/admin/reservation") + .then().log().all() + .statusCode(401); + } + + @Test + @DisplayName("예약 목록 조회 요청이 정상석으로 수행된다.") + void selectReservationListRequest_Success() { + RestAssured.given().log().all() + .when().get("/reservations") + .then().log().all() + .statusCode(200) + .body("size()", is(1)); + } + + @Test + @DisplayName("예약 추가, 조회를 정상적으로 수행한다.") + void ReservationTime_CREATE_READ_Success() { + Map reservation = Map.of("name", "브라운", + "date", LocalDate.now().plusDays(2L).toString(), + "timeId", 1, + "themeId", 1 + ); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .cookie("token", TokenGenerator.makeUserToken()) + .body(reservation) + .when().post("/reservations") + .then().log().all() + .statusCode(201); + + + RestAssured.given().log().all() + .when().get("/reservations") + .then().log().all() + .statusCode(200) + .body("size()", is(2)); + } + + @Test + @DisplayName("DB에 저장된 예약을 정상적으로 삭제한다.") + void deleteReservation_InDatabase_Success() { + RestAssured.given().log().all() + .when().delete("/reservations/1") + .then().log().all() + .statusCode(204); + + RestAssured.given().log().all() + .when().get("/reservations") + .then().log().all() + .statusCode(200) + .body("size()", is(0)); + } + + @Test + @DisplayName("데이터베이스 관련 로직을 컨트롤러에서 분리하였다.") + void layerRefactoring() { + boolean isJdbcTemplateInjected = false; + + for (Field field : reservationApiController.getClass().getDeclaredFields()) { + if (field.getType().equals(JdbcTemplate.class)) { + isJdbcTemplateInjected = true; + break; + } + } + + assertThat(isJdbcTemplateInjected).isFalse(); + } +} diff --git a/src/test/java/roomescape/controller/api/ReservationTimeApiControllerTest.java b/src/test/java/roomescape/controller/api/ReservationTimeApiControllerTest.java new file mode 100644 index 000000000..aea42437f --- /dev/null +++ b/src/test/java/roomescape/controller/api/ReservationTimeApiControllerTest.java @@ -0,0 +1,43 @@ +package roomescape.controller.api; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; + +import java.util.Map; + +import static org.hamcrest.Matchers.is; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +public class ReservationTimeApiControllerTest { + + @Test + @DisplayName("예약 시간 추가, 조회, 삭제를 정상적으로 수행한다.") + void ReservationTime_CREATE_READ_DELETE_Success() { + Map time = Map.of( + "startAt", "12:00" + ); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(time) + .when().post("/times") + .then().log().all() + .statusCode(201); + + RestAssured.given().log().all() + .when().get("/times") + .then().log().all() + .statusCode(200) + .body("size()", is(3)); + + RestAssured.given().log().all() + .when().delete("/times/3") + .then().log().all() + .statusCode(204); + } +} diff --git a/src/test/java/roomescape/controller/api/ThemeApiControllerTest.java b/src/test/java/roomescape/controller/api/ThemeApiControllerTest.java new file mode 100644 index 000000000..0f1e2f105 --- /dev/null +++ b/src/test/java/roomescape/controller/api/ThemeApiControllerTest.java @@ -0,0 +1,65 @@ +package roomescape.controller.api; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; + +import java.util.Map; + +import static org.hamcrest.Matchers.is; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +public class ThemeApiControllerTest { + + @Test + @DisplayName("테마 조회를 정상적으로 수행한다.") + void findTheme_Success() { + RestAssured.given().log().all() + .when().get("/themes") + .then().log().all() + .statusCode(200) + .body("size()", is(2)); + } + + @Test + @DisplayName("테마 추가를 정상적으로 수행한다.") + void addTheme_Success() { + Map params = Map.of("name", "레벨2 탈출", + "description", "우테코 레벨2를 탈출하는 내용입니다.", + "thumbnail", "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg" + ); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(params) + .when().post("/themes") + .then().log().all() + .statusCode(201) + .body("id", is(3)); + + RestAssured.given().log().all() + .when().get("/themes") + .then().log().all() + .statusCode(200) + .body("size()", is(3)); + } + + @Test + @DisplayName("테마 삭제를 정상적으로 수행한다.") + void deleteTheme_Success() { + RestAssured.given().log().all() + .when().delete("/themes/2") + .then().log().all() + .statusCode(204); + + RestAssured.given().log().all() + .when().get("/themes") + .then().log().all() + .statusCode(200) + .body("size()", is(1)); + } +} diff --git a/src/test/java/roomescape/controller/web/AdminControllerTest.java b/src/test/java/roomescape/controller/web/AdminControllerTest.java new file mode 100644 index 000000000..d589ef127 --- /dev/null +++ b/src/test/java/roomescape/controller/web/AdminControllerTest.java @@ -0,0 +1,37 @@ +package roomescape.controller.web; + +import io.restassured.RestAssured; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import roomescape.service.dto.request.LoginRequest; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +class AdminControllerTest { + + @Test + @DisplayName("어드민 메인 페이지로 정상적으로 이동한다.") + void moveToAdminMainPage_Success() { + String token = RestAssured.given().log().all() + .body(new LoginRequest("admin@naver.com", "1234")) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .when().post("/login") + .then().log().cookies().extract().cookie("token"); + + RestAssured.given().log().all() + .cookie("token", token) + .when().get("/admin") + .then().log().all() + .statusCode(200); + } + + @Test + @DisplayName("예약 페이지 요청이 정상적으로 수행된다.") + void moveToAdminMainPage_Failure() { + RestAssured.given().log().all() + .when().get("/admin") + .then().log().all() + .statusCode(401); + } +} diff --git a/src/test/java/roomescape/domain/ReservationStatusTest.java b/src/test/java/roomescape/domain/ReservationStatusTest.java new file mode 100644 index 000000000..8297d444e --- /dev/null +++ b/src/test/java/roomescape/domain/ReservationStatusTest.java @@ -0,0 +1,31 @@ +package roomescape.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +class ReservationStatusTest { + + @Test + @DisplayName("예약된 시간과 전체 시간을 비교하여 예약 여부를 구한다.") + void checkSameReservation_Success() { + ReservationTime reservationTime1 = new ReservationTime(1L, LocalTime.of(10, 0)); + ReservationTime reservationTime2 = new ReservationTime(2L, LocalTime.of(11, 0)); + List reservedTimes = List.of(reservationTime1); + List reservationTimes = List.of( + reservationTime1, + reservationTime2 + ); + ReservationStatus reservationStatus = ReservationStatus.of(reservedTimes, reservationTimes); + + assertAll( + () -> assertThat(reservationStatus.findReservationStatusBy(reservationTime1)).isTrue(), + () -> assertThat(reservationStatus.findReservationStatusBy(reservationTime2)).isFalse() + ); + } +} diff --git a/src/test/java/roomescape/service/dto/ReservationSaveRequestTest.java b/src/test/java/roomescape/service/dto/ReservationSaveRequestTest.java new file mode 100644 index 000000000..bc47df209 --- /dev/null +++ b/src/test/java/roomescape/service/dto/ReservationSaveRequestTest.java @@ -0,0 +1,19 @@ +package roomescape.service.dto; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import roomescape.service.dto.request.ReservationSaveRequest; + +import java.time.LocalDate; + +import static org.assertj.core.api.Assertions.assertThatCode; + +class ReservationSaveRequestTest { + + @Test + @DisplayName("이름이 정상 입력될 경우 성공한다.") + void checkNameBlank_Success() { + assertThatCode(() -> new ReservationSaveRequest(LocalDate.now(), 1L, 1L)) + .doesNotThrowAnyException(); + } +} diff --git a/src/test/java/roomescape/service/dto/ThemeSaveRequestTest.java b/src/test/java/roomescape/service/dto/ThemeSaveRequestTest.java new file mode 100644 index 000000000..73f7719f5 --- /dev/null +++ b/src/test/java/roomescape/service/dto/ThemeSaveRequestTest.java @@ -0,0 +1,17 @@ +package roomescape.service.dto; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import roomescape.service.dto.request.ThemeSaveRequest; + +import static org.assertj.core.api.Assertions.assertThatCode; + +public class ThemeSaveRequestTest { + + @Test + @DisplayName("테마 이름이 정상 입력될 경우 성공한다.") + void checkThemeNameBlank_Success() { + assertThatCode(() -> new ThemeSaveRequest("capy", "description", "thumbnail")) + .doesNotThrowAnyException(); + } +} diff --git a/src/test/java/roomescape/service/reservation/ReservationCreateServiceTest.java b/src/test/java/roomescape/service/reservation/ReservationCreateServiceTest.java new file mode 100644 index 000000000..f3e9cc055 --- /dev/null +++ b/src/test/java/roomescape/service/reservation/ReservationCreateServiceTest.java @@ -0,0 +1,73 @@ +package roomescape.service.reservation; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.jdbc.core.JdbcTemplate; +import roomescape.domain.Member; +import roomescape.domain.Role; +import roomescape.repository.MemberRepository; +import roomescape.repository.ReservationRepository; +import roomescape.repository.ReservationTimeRepository; +import roomescape.repository.ThemeRepository; +import roomescape.service.dto.request.ReservationSaveRequest; + +import java.time.LocalDate; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@JdbcTest +class ReservationCreateServiceTest { + + private ReservationCreateService reservationCreateService; + + @Autowired + public ReservationCreateServiceTest(JdbcTemplate jdbcTemplate) { + reservationCreateService = new ReservationCreateService( + new ReservationCreateValidator( + new ReservationRepository(jdbcTemplate), + new ReservationTimeRepository(jdbcTemplate), + new ThemeRepository(jdbcTemplate), + new MemberRepository(jdbcTemplate) + ), + new ReservationRepository(jdbcTemplate) + ); + } + + @Test + @DisplayName("예약 가능한 시간인 경우 성공한다.") + void checkDuplicateReservationTime_Success() { + ReservationSaveRequest request = new ReservationSaveRequest( + LocalDate.now().plusDays(1L), 2L, 2L); + Member member = new Member(1L, "capy", "test@naver.com", "1234", Role.USER); + + assertThatCode(() -> reservationCreateService.createReservation(request, member)) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("이미 예약된 시간인 경우 예외가 발생한다.") + void checkDuplicateReservationTime_Failure() { + ReservationSaveRequest request = new ReservationSaveRequest( + LocalDate.now().plusDays(1L), 1L, 1L); + Member member = new Member("capy", "abc@gmail.com", "1234", Role.USER); + + assertThatThrownBy(() -> reservationCreateService.createReservation(request, member)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("해당 시간에 이미 예약된 테마입니다."); + } + + @Test + @DisplayName("지나간 날짜와 시간에 대한 예약 생성시 예외가 발생한다.") + void checkReservationDateTimeIsFuture_Failure() { + ReservationSaveRequest request = new ReservationSaveRequest( + LocalDate.now().minusDays(1L), 2L, 2L); + Member member = new Member("capy", "abc@gmail.com", "1234", Role.USER); + + assertThatThrownBy(() -> reservationCreateService.createReservation(request, member)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("지나간 날짜와 시간에 대한 예약 생성은 불가능합니다."); + } +} diff --git a/src/test/java/roomescape/service/reservationtime/ReservationTimeCreateServiceTest.java b/src/test/java/roomescape/service/reservationtime/ReservationTimeCreateServiceTest.java new file mode 100644 index 000000000..09340e272 --- /dev/null +++ b/src/test/java/roomescape/service/reservationtime/ReservationTimeCreateServiceTest.java @@ -0,0 +1,46 @@ +package roomescape.service.reservationtime; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.jdbc.core.JdbcTemplate; +import roomescape.repository.ReservationTimeRepository; +import roomescape.service.dto.request.ReservationTimeSaveRequest; + +import java.time.LocalTime; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@JdbcTest +class ReservationTimeCreateServiceTest { + + private ReservationTimeCreateService reservationTimeCreateService; + + @Autowired + public ReservationTimeCreateServiceTest(JdbcTemplate jdbcTemplate) { + reservationTimeCreateService = new ReservationTimeCreateService( + new ReservationTimeRepository(jdbcTemplate) + ); + } + + @Test + @DisplayName("존재하지 않는 예약 시간인 경우 성공한다") + void checkDuplicateTime_Success() { + ReservationTimeSaveRequest request = new ReservationTimeSaveRequest(LocalTime.of(12, 0)); + + assertThatCode(() -> reservationTimeCreateService.createReservationTime(request)) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("이미 존재하는 예약 시간인 경우 예외가 발생한다.") + void checkDuplicateTime_Failure() { + ReservationTimeSaveRequest request = new ReservationTimeSaveRequest(LocalTime.of(11, 0)); + + assertThatThrownBy(() -> reservationTimeCreateService.createReservationTime(request)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이미 존재하는 예약 시간입니다."); + } +} diff --git a/src/test/java/roomescape/service/reservationtime/ReservationTimeDeleteServiceTest.java b/src/test/java/roomescape/service/reservationtime/ReservationTimeDeleteServiceTest.java new file mode 100644 index 000000000..0913fb106 --- /dev/null +++ b/src/test/java/roomescape/service/reservationtime/ReservationTimeDeleteServiceTest.java @@ -0,0 +1,41 @@ +package roomescape.service.reservationtime; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.jdbc.core.JdbcTemplate; +import roomescape.repository.ReservationRepository; +import roomescape.repository.ReservationTimeRepository; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@JdbcTest +class ReservationTimeDeleteServiceTest { + + private ReservationTimeDeleteService reservationTimeDeleteService; + + @Autowired + public ReservationTimeDeleteServiceTest(JdbcTemplate jdbcTemplate) { + reservationTimeDeleteService = new ReservationTimeDeleteService( + new ReservationTimeRepository(jdbcTemplate), + new ReservationRepository(jdbcTemplate) + ); + } + + @Test + @DisplayName("예약 중이 아닌 시간을 삭제할 시 성공한다.") + void deleteNotReservedTime_Success() { + assertThatCode(() -> reservationTimeDeleteService.deleteReservationTime(2L)) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("이미 예약 중인 시간을 삭제할 시 예외가 발생한다.") + void deleteReservedTime_Failure() { + assertThatThrownBy(() -> reservationTimeDeleteService.deleteReservationTime(1L)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이미 예약중인 시간은 삭제할 수 없습니다."); + } +} diff --git a/src/test/java/roomescape/service/reservationtime/ReservationTimeFindServiceTest.java b/src/test/java/roomescape/service/reservationtime/ReservationTimeFindServiceTest.java new file mode 100644 index 000000000..b2daf1235 --- /dev/null +++ b/src/test/java/roomescape/service/reservationtime/ReservationTimeFindServiceTest.java @@ -0,0 +1,41 @@ +package roomescape.service.reservationtime; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.jdbc.core.JdbcTemplate; +import roomescape.domain.ReservationStatus; +import roomescape.domain.ReservationTime; +import roomescape.repository.ReservationTimeRepository; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +@JdbcTest +class ReservationTimeFindServiceTest { + + private ReservationTimeFindService reservationTimeFindService; + + @Autowired + public ReservationTimeFindServiceTest(JdbcTemplate jdbcTemplate) { + reservationTimeFindService = new ReservationTimeFindService( + new ReservationTimeRepository(jdbcTemplate) + ); + } + + @Test + @DisplayName("날짜와 테마가 주어지면 각 시간의 예약 여부를 구한다.") + void findAvailabilityByDateAndTheme() { + LocalDate date = LocalDate.now().plusDays(1L); + ReservationStatus reservationStatus = reservationTimeFindService.findIsBooked(date, 1L); + assertThat(reservationStatus.getReservationStatus()) + .isEqualTo(Map.of( + new ReservationTime(1L, LocalTime.of(10, 0)), true, + new ReservationTime(2L, LocalTime.of(11, 0)), false + )); + } +} diff --git a/src/test/java/roomescape/service/theme/ThemeDeleteServiceTest.java b/src/test/java/roomescape/service/theme/ThemeDeleteServiceTest.java new file mode 100644 index 000000000..8cc777228 --- /dev/null +++ b/src/test/java/roomescape/service/theme/ThemeDeleteServiceTest.java @@ -0,0 +1,41 @@ +package roomescape.service.theme; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.jdbc.core.JdbcTemplate; +import roomescape.repository.ReservationRepository; +import roomescape.repository.ThemeRepository; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@JdbcTest +class ThemeDeleteServiceTest { + + private ThemeDeleteService themeDeleteService; + + @Autowired + public ThemeDeleteServiceTest(JdbcTemplate jdbcTemplate) { + themeDeleteService = new ThemeDeleteService( + new ThemeRepository(jdbcTemplate), + new ReservationRepository(jdbcTemplate) + ); + } + + @Test + @DisplayName("예약 중이 아닌 테마를 삭제할 시 성공한다.") + void deleteNotReservedTime_Success() { + assertThatCode(() -> themeDeleteService.deleteTheme(2L)) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("이미 예약 중인 테마를 삭제할 시 예외가 발생한다.") + void deleteReservedTime_Failure() { + assertThatThrownBy(() -> themeDeleteService.deleteTheme(1L)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이미 예약중인 테마는 삭제할 수 없습니다."); + } +} diff --git a/src/test/java/roomescape/util/TokenGenerator.java b/src/test/java/roomescape/util/TokenGenerator.java new file mode 100644 index 000000000..90a511b44 --- /dev/null +++ b/src/test/java/roomescape/util/TokenGenerator.java @@ -0,0 +1,24 @@ +package roomescape.util; + +import io.restassured.RestAssured; +import org.springframework.http.MediaType; +import roomescape.service.dto.request.LoginRequest; + +public class TokenGenerator { + + public static String makeUserToken() { + return RestAssured.given().log().all() + .body(new LoginRequest("user@naver.com", "1234")) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .when().post("/login") + .then().log().cookies().extract().cookie("token"); + } + + public static String makeAdminToken() { + return RestAssured.given().log().all() + .body(new LoginRequest("admin@naver.com", "1234")) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .when().post("/login") + .then().log().cookies().extract().cookie("token"); + } +} diff --git a/src/test/resources/data.sql b/src/test/resources/data.sql new file mode 100644 index 000000000..48d4754e0 --- /dev/null +++ b/src/test/resources/data.sql @@ -0,0 +1,27 @@ +DELETE +FROM reservation; +DELETE +FROM member; +DELETE +FROM theme; +DELETE +FROM reservation_time; + +INSERT INTO theme (name, description, thumbnail) +VALUES ('theme1', 'description1', 'thumbnail1'); +INSERT INTO theme (name, description, thumbnail) +VALUES ('theme2', 'description2', 'thumbnail2'); + +INSERT INTO reservation_time (start_at) +VALUES ('10:00'); +INSERT INTO reservation_time (start_at) +VALUES ('11:00'); + +INSERT INTO member +VALUES (1, 'testUser', 'user@naver.com', '1234', 'USER'); +INSERT INTO member +VALUES (2, 'testAdmin', 'admin@naver.com', '1234', 'ADMIN'); + +INSERT INTO reservation (id, member_id, date, reservation_time_id, theme_Id) +VALUES (1, 1, CURRENT_DATE + INTERVAL '1' DAY, 1, 1); + From a93899628beff61ce6a54c85f6fe934ed5da9da2 Mon Sep 17 00:00:00 2001 From: Minjoo Date: Tue, 14 May 2024 16:49:50 +0900 Subject: [PATCH 02/42] =?UTF-8?q?chore:=20=EC=9D=98=EC=A1=B4=EC=84=B1=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 Co-authored-by: tkdgur0906 --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 603e9418a..a04e9e271 100644 --- a/build.gradle +++ b/build.gradle @@ -18,6 +18,7 @@ dependencies { 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 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'io.jsonwebtoken:jjwt-api:0.12.5' implementation 'io.jsonwebtoken:jjwt-jackson:0.12.5' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5' From 84f3c9594977a7169fe4d5265a99b44ceeabc73b Mon Sep 17 00:00:00 2001 From: Minjoo Date: Tue, 14 May 2024 16:50:49 +0900 Subject: [PATCH 03/42] =?UTF-8?q?chore:=20yml=EC=9D=84=20properties?= =?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 Co-authored-by: tkdgur0906 --- src/main/resources/application.properties | 8 ++++++++ src/main/resources/application.yml | 10 ---------- 2 files changed, 8 insertions(+), 10 deletions(-) create mode 100644 src/main/resources/application.properties delete mode 100644 src/main/resources/application.yml diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 000000000..311a930da --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,8 @@ +spring.h2.console.enabled=true +spring.h2.console.path=/h2-consoles +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 +security.jwt.token.secret-key=Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml deleted file mode 100644 index b70507cf1..000000000 --- a/src/main/resources/application.yml +++ /dev/null @@ -1,10 +0,0 @@ -spring: - h2: - console: - enabled: true - path: /h2-consoles - datasource: - url: jdbc:h2:mem:database - -jwt: - secret: Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E= From 20e8418c95fdd715ba032b4ff314bd773eb158ee Mon Sep 17 00:00:00 2001 From: Minjoo Date: Tue, 14 May 2024 16:56:34 +0900 Subject: [PATCH 04/42] =?UTF-8?q?refactor:=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EB=A7=8C=EB=A3=8C=EA=B8=B0=ED=95=9C=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: tkdgur0906 --- .../java/roomescape/service/auth/TokenProvider.java | 10 +++++++++- src/main/resources/application.properties | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/roomescape/service/auth/TokenProvider.java b/src/main/java/roomescape/service/auth/TokenProvider.java index d7854756e..ef1f6ad1f 100644 --- a/src/main/java/roomescape/service/auth/TokenProvider.java +++ b/src/main/java/roomescape/service/auth/TokenProvider.java @@ -3,6 +3,7 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import jakarta.servlet.http.Cookie; +import java.util.Date; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import roomescape.exception.AuthenticationException; @@ -16,15 +17,22 @@ public class TokenProvider { private static final String TOKEN = "token"; private final Key key; + private final long validityInMilliseconds; - public TokenProvider(@Value("${jwt.secret}") String secretKey) { + public TokenProvider(@Value("${security.jwt.token.secret-key}") String secretKey, + @Value("${security.jwt.token.expire-length}") long validityInMilliseconds) { this.key = Keys.hmacShaKeyFor(secretKey.getBytes()); + this.validityInMilliseconds = validityInMilliseconds; } public String generateAccessToken(long memberId) { + Date now = new Date(); + Date validity = new Date(now.getTime() + validityInMilliseconds); + return Jwts.builder() .setSubject(String.valueOf(memberId)) .signWith(key) + .expiration(validity) .compact(); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 311a930da..534d22104 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -6,3 +6,4 @@ spring.jpa.properties.hibernate.format_sql=true spring.jpa.ddl-auto=create-drop spring.jpa.defer-datasource-initialization=true security.jwt.token.secret-key=Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E +security.jwt.token.expire-length=3600000 From 00b100a61ab0e3b74756bd01cb78046d5eab9e81 Mon Sep 17 00:00:00 2001 From: Minjoo Date: Tue, 14 May 2024 16:57:42 +0900 Subject: [PATCH 05/42] =?UTF-8?q?feat:=20Member=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=EC=97=90=20JPA=20=EB=8F=84=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: tkdgur0906 --- src/main/java/roomescape/domain/Member.java | 24 ++++++++-- .../repository/MemberRepository.java | 48 ++----------------- src/main/resources/data.sql | 25 +++++----- src/main/resources/schema.sql | 10 ---- .../ReservationCreateServiceTest.java | 28 ++++++----- .../ReservationTimeCreateServiceTest.java | 11 ++--- .../ReservationTimeDeleteServiceTest.java | 14 ++---- .../ReservationTimeFindServiceTest.java | 13 ++--- .../service/theme/ThemeDeleteServiceTest.java | 12 ++--- src/test/resources/data.sql | 27 ----------- 10 files changed, 68 insertions(+), 144 deletions(-) delete mode 100644 src/test/resources/data.sql diff --git a/src/main/java/roomescape/domain/Member.java b/src/main/java/roomescape/domain/Member.java index ecd10061b..f62d1f9c9 100644 --- a/src/main/java/roomescape/domain/Member.java +++ b/src/main/java/roomescape/domain/Member.java @@ -1,12 +1,24 @@ package roomescape.domain; +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 public class Member { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - private final String name; - private final String email; - private final String password; - private final Role role; + private String name; + private String email; + private String password; + + @Enumerated(value = EnumType.STRING) + private Role role; public Member(Long id, String name, String email, String password, Role role) { this.id = id; @@ -20,6 +32,10 @@ public Member(String name, String email, String password, Role role) { this(null, name, email, password, role); } + public Member() { + + } + public Long getId() { return id; } diff --git a/src/main/java/roomescape/repository/MemberRepository.java b/src/main/java/roomescape/repository/MemberRepository.java index 896b47c1b..9de7aa59d 100644 --- a/src/main/java/roomescape/repository/MemberRepository.java +++ b/src/main/java/roomescape/repository/MemberRepository.java @@ -1,52 +1,12 @@ package roomescape.repository; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import roomescape.domain.Member; -import roomescape.domain.Role; - -import java.util.List; -import java.util.Optional; @Repository -public class MemberRepository { - - private final JdbcTemplate jdbcTemplate; - - private final RowMapper memberRowMapper = (resultSet, rowNum) -> new Member( - resultSet.getLong("id"), - resultSet.getString("name"), - resultSet.getString("email"), - resultSet.getString("password"), - Role.valueOf(resultSet.getString("role")) - ); - - - public MemberRepository(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } - - public Optional findById(long id) { - String sql = "SELECT id, name, email, password, role " + - "FROM member " + - "WHERE id = ?"; - List members = jdbcTemplate.query(sql, memberRowMapper, id); - return members.isEmpty() ? Optional.empty() : Optional.of(members.get(0)); - } - - public Optional findByEmailAndPassword(String email, String password) { - String sql = "SELECT id, name, email, password, role " + - "FROM member " + - "WHERE email = ? " + - "AND password = ?"; - List members = jdbcTemplate.query(sql, memberRowMapper, email, password); - return members.isEmpty() ? Optional.empty() : Optional.of(members.get(0)); - } +public interface MemberRepository extends JpaRepository { - public List findAll() { - String sql = "SELECT id, name, email, password, role " + - "FROM member "; - return jdbcTemplate.query(sql, memberRowMapper); - } + Optional findByEmailAndPassword(String email, String password); } diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 5b32770c0..72058ed9a 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -1,18 +1,17 @@ -INSERT INTO theme (id, name, description, thumbnail) -VALUES (1, 'theme1', 'description1', 'thumbnail1'); -INSERT INTO theme (id, name, description, thumbnail) -VALUES (2, 'theme2', 'description2', 'thumbnail2'); +INSERT INTO theme (name, description, thumbnail) +VALUES ('theme1', 'description1', 'thumbnail1'); +INSERT INTO theme (name, description, thumbnail) +VALUES ('theme2', 'description2', 'thumbnail2'); -INSERT INTO reservation_time (id, start_at) -VALUES (1, '10:00'); -INSERT INTO reservation_time (id, start_at) -VALUES (2, '11:00'); - -INSERT INTO member -VALUES (1, 'testUser', 'test@naver.com', '1234', 'USER'); -INSERT INTO member -VALUES (2, 'testAdmin', 'admin@naver.com', '1234', 'ADMIN'); +INSERT INTO reservation_time (start_at) +VALUES ('10:00'); +INSERT INTO reservation_time (start_at) +VALUES ('11:00'); +INSERT INTO member (name, email, password, `role`) +VALUES ('testUser', 'user@naver.com', '1234', 'USER'); +INSERT INTO member (name, email, password, `role`) +VALUES ('testAdmin', 'admin@naver.com', '1234', 'ADMIN'); INSERT INTO reservation (member_id, date, reservation_time_id, theme_Id) VALUES (1, CURRENT_DATE + INTERVAL '1' DAY, 1, 1); diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 45d3dd909..b22c5d7e5 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -26,13 +26,3 @@ CREATE TABLE reservation FOREIGN KEY (reservation_time_id) REFERENCES reservation_time (id), FOREIGN KEY (theme_id) REFERENCES theme (id) ); - -CREATE TABLE member -( - id BIGINT NOT NULL AUTO_INCREMENT, - name VARCHAR(255) NOT NULL, - email VARCHAR(255) NOT NULL UNIQUE, - password VARCHAR(255) NOT NULL, - role VARCHAR(255), - PRIMARY KEY (id) -); diff --git a/src/test/java/roomescape/service/reservation/ReservationCreateServiceTest.java b/src/test/java/roomescape/service/reservation/ReservationCreateServiceTest.java index f3e9cc055..bc6b4a766 100644 --- a/src/test/java/roomescape/service/reservation/ReservationCreateServiceTest.java +++ b/src/test/java/roomescape/service/reservation/ReservationCreateServiceTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.jdbc.core.JdbcTemplate; import roomescape.domain.Member; import roomescape.domain.Role; @@ -18,23 +19,24 @@ import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; -@JdbcTest +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) class ReservationCreateServiceTest { + @Autowired private ReservationCreateService reservationCreateService; - @Autowired - public ReservationCreateServiceTest(JdbcTemplate jdbcTemplate) { - reservationCreateService = new ReservationCreateService( - new ReservationCreateValidator( - new ReservationRepository(jdbcTemplate), - new ReservationTimeRepository(jdbcTemplate), - new ThemeRepository(jdbcTemplate), - new MemberRepository(jdbcTemplate) - ), - new ReservationRepository(jdbcTemplate) - ); - } +// @Autowired +// public ReservationCreateServiceTest(JdbcTemplate jdbcTemplate) { +// reservationCreateService = new ReservationCreateService( +// new ReservationCreateValidator( +// new ReservationRepository(jdbcTemplate), +// new ReservationTimeRepository(jdbcTemplate), +// new ThemeRepository(jdbcTemplate), +// new MemberRepository(jdbcTemplate) +// ), +// new ReservationRepository(jdbcTemplate) +// ); +// } @Test @DisplayName("예약 가능한 시간인 경우 성공한다.") diff --git a/src/test/java/roomescape/service/reservationtime/ReservationTimeCreateServiceTest.java b/src/test/java/roomescape/service/reservationtime/ReservationTimeCreateServiceTest.java index 09340e272..32f624078 100644 --- a/src/test/java/roomescape/service/reservationtime/ReservationTimeCreateServiceTest.java +++ b/src/test/java/roomescape/service/reservationtime/ReservationTimeCreateServiceTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.jdbc.core.JdbcTemplate; import roomescape.repository.ReservationTimeRepository; import roomescape.service.dto.request.ReservationTimeSaveRequest; @@ -13,17 +14,13 @@ import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; -@JdbcTest +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) class ReservationTimeCreateServiceTest { + @Autowired private ReservationTimeCreateService reservationTimeCreateService; - @Autowired - public ReservationTimeCreateServiceTest(JdbcTemplate jdbcTemplate) { - reservationTimeCreateService = new ReservationTimeCreateService( - new ReservationTimeRepository(jdbcTemplate) - ); - } + @Test @DisplayName("존재하지 않는 예약 시간인 경우 성공한다") diff --git a/src/test/java/roomescape/service/reservationtime/ReservationTimeDeleteServiceTest.java b/src/test/java/roomescape/service/reservationtime/ReservationTimeDeleteServiceTest.java index 0913fb106..08352ce67 100644 --- a/src/test/java/roomescape/service/reservationtime/ReservationTimeDeleteServiceTest.java +++ b/src/test/java/roomescape/service/reservationtime/ReservationTimeDeleteServiceTest.java @@ -4,25 +4,21 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.annotation.DirtiesContext; import roomescape.repository.ReservationRepository; import roomescape.repository.ReservationTimeRepository; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; -@JdbcTest +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) class ReservationTimeDeleteServiceTest { - private ReservationTimeDeleteService reservationTimeDeleteService; - @Autowired - public ReservationTimeDeleteServiceTest(JdbcTemplate jdbcTemplate) { - reservationTimeDeleteService = new ReservationTimeDeleteService( - new ReservationTimeRepository(jdbcTemplate), - new ReservationRepository(jdbcTemplate) - ); - } + private ReservationTimeDeleteService reservationTimeDeleteService; @Test @DisplayName("예약 중이 아닌 시간을 삭제할 시 성공한다.") diff --git a/src/test/java/roomescape/service/reservationtime/ReservationTimeFindServiceTest.java b/src/test/java/roomescape/service/reservationtime/ReservationTimeFindServiceTest.java index b2daf1235..ee9452699 100644 --- a/src/test/java/roomescape/service/reservationtime/ReservationTimeFindServiceTest.java +++ b/src/test/java/roomescape/service/reservationtime/ReservationTimeFindServiceTest.java @@ -4,7 +4,9 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.annotation.DirtiesContext; import roomescape.domain.ReservationStatus; import roomescape.domain.ReservationTime; import roomescape.repository.ReservationTimeRepository; @@ -15,17 +17,12 @@ import static org.assertj.core.api.Assertions.assertThat; -@JdbcTest +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) class ReservationTimeFindServiceTest { - private ReservationTimeFindService reservationTimeFindService; - @Autowired - public ReservationTimeFindServiceTest(JdbcTemplate jdbcTemplate) { - reservationTimeFindService = new ReservationTimeFindService( - new ReservationTimeRepository(jdbcTemplate) - ); - } + private ReservationTimeFindService reservationTimeFindService; @Test @DisplayName("날짜와 테마가 주어지면 각 시간의 예약 여부를 구한다.") diff --git a/src/test/java/roomescape/service/theme/ThemeDeleteServiceTest.java b/src/test/java/roomescape/service/theme/ThemeDeleteServiceTest.java index 8cc777228..378bfc01a 100644 --- a/src/test/java/roomescape/service/theme/ThemeDeleteServiceTest.java +++ b/src/test/java/roomescape/service/theme/ThemeDeleteServiceTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.jdbc.core.JdbcTemplate; import roomescape.repository.ReservationRepository; import roomescape.repository.ThemeRepository; @@ -11,18 +12,11 @@ import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; -@JdbcTest +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) class ThemeDeleteServiceTest { - private ThemeDeleteService themeDeleteService; - @Autowired - public ThemeDeleteServiceTest(JdbcTemplate jdbcTemplate) { - themeDeleteService = new ThemeDeleteService( - new ThemeRepository(jdbcTemplate), - new ReservationRepository(jdbcTemplate) - ); - } + private ThemeDeleteService themeDeleteService; @Test @DisplayName("예약 중이 아닌 테마를 삭제할 시 성공한다.") diff --git a/src/test/resources/data.sql b/src/test/resources/data.sql deleted file mode 100644 index 48d4754e0..000000000 --- a/src/test/resources/data.sql +++ /dev/null @@ -1,27 +0,0 @@ -DELETE -FROM reservation; -DELETE -FROM member; -DELETE -FROM theme; -DELETE -FROM reservation_time; - -INSERT INTO theme (name, description, thumbnail) -VALUES ('theme1', 'description1', 'thumbnail1'); -INSERT INTO theme (name, description, thumbnail) -VALUES ('theme2', 'description2', 'thumbnail2'); - -INSERT INTO reservation_time (start_at) -VALUES ('10:00'); -INSERT INTO reservation_time (start_at) -VALUES ('11:00'); - -INSERT INTO member -VALUES (1, 'testUser', 'user@naver.com', '1234', 'USER'); -INSERT INTO member -VALUES (2, 'testAdmin', 'admin@naver.com', '1234', 'ADMIN'); - -INSERT INTO reservation (id, member_id, date, reservation_time_id, theme_Id) -VALUES (1, 1, CURRENT_DATE + INTERVAL '1' DAY, 1, 1); - From 424401814629cb7d2f5852dc1b79011c773378f7 Mon Sep 17 00:00:00 2001 From: Minjoo Date: Tue, 14 May 2024 20:46:10 +0900 Subject: [PATCH 06/42] =?UTF-8?q?feat:=20Reservation,=20ReservationTime,?= =?UTF-8?q?=20Theme=20=EC=97=94=ED=8B=B0=ED=8B=B0=EC=97=90=20JPA=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 Co-authored-by: tkdgur0906 --- .../java/roomescape/domain/Reservation.java | 45 +++-- .../roomescape/domain/ReservationTime.java | 20 +- src/main/java/roomescape/domain/Theme.java | 24 ++- .../repository/ReservationRepository.java | 186 +----------------- .../repository/ReservationTimeRepository.java | 97 ++------- .../repository/ThemeRepository.java | 96 ++------- .../reservation/ReservationFindService.java | 7 +- .../ReservationTimeDeleteService.java | 2 +- .../ReservationTimeFindService.java | 7 +- .../service/theme/ThemeDeleteService.java | 2 +- src/main/resources/data.sql | 3 +- src/main/resources/schema.sql | 28 --- .../service/theme/ThemeDeleteServiceTest.java | 12 +- 13 files changed, 128 insertions(+), 401 deletions(-) delete mode 100644 src/main/resources/schema.sql diff --git a/src/main/java/roomescape/domain/Reservation.java b/src/main/java/roomescape/domain/Reservation.java index 76f2db1d4..1f17f4685 100644 --- a/src/main/java/roomescape/domain/Reservation.java +++ b/src/main/java/roomescape/domain/Reservation.java @@ -1,21 +1,39 @@ package roomescape.domain; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; import java.time.LocalDate; import java.util.Objects; +@Entity public class Reservation { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - private final Member member; - private final LocalDate date; - private final ReservationTime reservationTime; - private final Theme theme; - public Reservation(Long id, Member member, LocalDate date, ReservationTime reservationTime, Theme theme) { + @ManyToOne + private Member member; + + private LocalDate date; + + @ManyToOne + private ReservationTime time; + + @ManyToOne + private Theme theme; + + public Reservation() { + } + + public Reservation(Long id, Member member, LocalDate date, ReservationTime time, Theme theme) { this.id = id; this.member = member; this.date = date; - this.reservationTime = reservationTime; + this.time = time; this.theme = theme; } @@ -36,7 +54,7 @@ public LocalDate getDate() { } public ReservationTime getReservationTime() { - return reservationTime; + return time; } public Theme getTheme() { @@ -45,14 +63,19 @@ public Theme getTheme() { @Override public boolean equals(Object object) { - if (this == object) return true; - if (object == null || getClass() != object.getClass()) return false; + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } Reservation that = (Reservation) object; - return Objects.equals(id, that.id) && Objects.equals(member, that.member) && Objects.equals(date, that.date) && Objects.equals(reservationTime, that.reservationTime) && Objects.equals(theme, that.theme); + return Objects.equals(id, that.id) && Objects.equals(member, that.member) && Objects.equals(date, that.date) + && Objects.equals(time, that.time) && Objects.equals(theme, that.theme); } @Override public int hashCode() { - return Objects.hash(id, member, date, reservationTime, theme); + return Objects.hash(id, member, date, time, theme); } } diff --git a/src/main/java/roomescape/domain/ReservationTime.java b/src/main/java/roomescape/domain/ReservationTime.java index 99b3e8756..fdfcb3c37 100644 --- a/src/main/java/roomescape/domain/ReservationTime.java +++ b/src/main/java/roomescape/domain/ReservationTime.java @@ -1,12 +1,22 @@ package roomescape.domain; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; import java.time.LocalTime; import java.util.Objects; +@Entity public class ReservationTime { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - private final LocalTime startAt; + private LocalTime startAt; + + public ReservationTime() { + } public ReservationTime(LocalTime startAt) { this.startAt = startAt; @@ -27,8 +37,12 @@ public LocalTime getStartAt() { @Override public boolean equals(Object object) { - if (this == object) return true; - if (object == null || getClass() != object.getClass()) return false; + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } ReservationTime that = (ReservationTime) object; return Objects.equals(id, that.id) && Objects.equals(startAt, that.startAt); } diff --git a/src/main/java/roomescape/domain/Theme.java b/src/main/java/roomescape/domain/Theme.java index 6a410e405..a1806515c 100644 --- a/src/main/java/roomescape/domain/Theme.java +++ b/src/main/java/roomescape/domain/Theme.java @@ -1,13 +1,23 @@ package roomescape.domain; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; import java.util.Objects; +@Entity public class Theme { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - private final String name; - private final String description; - private final String thumbnail; + private String name; + private String description; + private String thumbnail; + + public Theme() { + } public Theme(Long id, String name, String description, String thumbnail) { this.id = id; @@ -38,8 +48,12 @@ public String getThumbnail() { @Override public boolean equals(Object object) { - if (this == object) return true; - if (object == null || getClass() != object.getClass()) return false; + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } Theme theme = (Theme) object; return Objects.equals(id, theme.id) && Objects.equals(name, theme.name) && diff --git a/src/main/java/roomescape/repository/ReservationRepository.java b/src/main/java/roomescape/repository/ReservationRepository.java index 7a1b27570..0cdfe0f39 100644 --- a/src/main/java/roomescape/repository/ReservationRepository.java +++ b/src/main/java/roomescape/repository/ReservationRepository.java @@ -1,187 +1,19 @@ package roomescape.repository; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.stereotype.Repository; -import roomescape.domain.Member; -import roomescape.domain.Reservation; -import roomescape.domain.ReservationTime; -import roomescape.domain.Role; -import roomescape.domain.Theme; - -import java.sql.Date; -import java.sql.PreparedStatement; import java.time.LocalDate; import java.util.List; -import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import roomescape.domain.Reservation; @Repository -public class ReservationRepository { - - private final JdbcTemplate jdbcTemplate; - - private final RowMapper reservationRowMapper = (resultSet, rowNum) -> new Reservation( - resultSet.getLong("id"), - new Member(resultSet.getLong("member_id"), - resultSet.getString("member_name"), - resultSet.getString("member_email"), - resultSet.getString("member_password"), - Role.valueOf(resultSet.getString("member_role"))), - resultSet.getDate("date").toLocalDate(), - new ReservationTime(resultSet.getLong("reservation_time_id"), - resultSet.getTime("time_value").toLocalTime()), - new Theme(resultSet.getLong("theme_id"), - resultSet.getString("theme_name"), - resultSet.getString("theme_description"), - resultSet.getString("theme_thumbnail") - ) - ); - - public ReservationRepository(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } - - public Reservation save(Reservation reservation) { - String sql = "INSERT INTO reservation " + - "(member_id, date, reservation_time_id, theme_id) " + - "VALUES (?, ?, ?, ?)"; - GeneratedKeyHolder keyHolder = new GeneratedKeyHolder(); - - jdbcTemplate.update(con -> { - PreparedStatement ps = con.prepareStatement( - sql, - new String[]{"id"}); - ps.setLong(1, reservation.getMember().getId()); - ps.setDate(2, Date.valueOf(reservation.getDate())); - ps.setLong(3, reservation.getReservationTime().getId()); - ps.setLong(4, reservation.getTheme().getId()); - return ps; - }, keyHolder); - - return new Reservation(keyHolder.getKey().longValue(), reservation.getMember(), - reservation.getDate(), reservation.getReservationTime(), reservation.getTheme()); - } - - public Optional findById(long id) { - String sql = "SELECT " + - " m.id AS member_id, " + - " m.name AS member_name, " + - " m.email AS member_email, " + - " m.password AS member_password, " + - " m.role AS member_role, " + - " r.id, " + - " r.member_id, " + - " r.date, " + - " r.reservation_time_id, " + - " r.theme_id, " + - " t.start_at AS time_value, " + - " th.id AS theme_id, " + - " th.name AS theme_name, " + - " th.description AS theme_description, " + - " th.thumbnail AS theme_thumbnail " + - "FROM reservation AS r " + - "INNER JOIN reservation_time AS t " + - "ON r.reservation_time_id = t.id " + - "INNER JOIN theme AS th " + - "ON r.theme_id = th.id " + - "INNER JOIN member AS m " + - "ON r.member_id = m.id " + - "WHERE r.id = ?"; - List reservations = jdbcTemplate.query(sql, reservationRowMapper, id); - return reservations.isEmpty() ? Optional.empty() : Optional.of(reservations.get(0)); - } - - public List searchReservations(long memberId, long themeId, LocalDate dateFrom, LocalDate dateTo) { - String sql = "SELECT " + - " m.id AS member_id, " + - " m.name AS member_name, " + - " m.email AS member_email, " + - " m.password AS member_password, " + - " m.role AS member_role, " + - " r.id, " + - " r.member_id, " + - " r.date, " + - " r.reservation_time_id, " + - " r.theme_id, " + - " t.start_at AS time_value, " + - " th.id AS theme_id, " + - " th.name AS theme_name, " + - " th.description AS theme_description, " + - " th.thumbnail AS theme_thumbnail " + - "FROM reservation AS r " + - "INNER JOIN reservation_time AS t " + - "ON r.reservation_time_id = t.id " + - "INNER JOIN theme AS th " + - "ON r.theme_id = th.id " + - "INNER JOIN member AS m " + - "ON r.member_id = m.id " + - "WHERE r.member_id = ? " + - "AND r.theme_id = ? " + - "AND r.date BETWEEN ? AND ?"; - return jdbcTemplate.query(sql, reservationRowMapper, - memberId, themeId, Date.valueOf(dateFrom), Date.valueOf(dateTo)); - } - - public List findAll() { - String sql = "SELECT " + - " m.id AS member_id, " + - " m.name AS member_name, " + - " m.email AS member_email, " + - " m.password AS member_password, " + - " m.role AS member_role, " + - " r.id, " + - " r.member_id, " + - " r.date, " + - " r.reservation_time_id, " + - " r.theme_id, " + - " t.start_at AS time_value, " + - " th.id AS theme_id, " + - " th.name AS theme_name, " + - " th.description AS theme_description, " + - " th.thumbnail AS theme_thumbnail " + - "FROM reservation AS r " + - "INNER JOIN reservation_time AS t " + - "ON r.reservation_time_id = t.id " + - "INNER JOIN theme AS th " + - "ON r.theme_id = th.id " + - "INNER JOIN member AS m " + - "ON r.member_id = m.id"; - return jdbcTemplate.query(sql, reservationRowMapper); - } - - public void deleteById(long id) { - String sql = "DELETE FROM reservation " + - "WHERE id = ?"; - jdbcTemplate.update(sql, id); - } - - public boolean existsByReservationTimeId(long reservationTimeId) { - String sql = "SELECT exists(" + - "SELECT 1 " + - "FROM reservation " + - "WHERE reservation_time_id = ?)"; - - return jdbcTemplate.queryForObject(sql, Boolean.class, reservationTimeId); - } - - public boolean existsByReservationThemeId(long themeId) { - String sql = "SELECT exists(" + - "SELECT 1 " + - "FROM reservation " + - "WHERE theme_id = ?)"; +public interface ReservationRepository extends JpaRepository { + boolean existsByThemeId(long themeId); - return jdbcTemplate.queryForObject(sql, Boolean.class, themeId); - } + List findByMemberIdAndThemeIdAndDateBetween(long memberId, long themeId, LocalDate dateFrom, + LocalDate dateTo); - public boolean existsByDateAndTimeIdAndThemeId(LocalDate date, long reservationTimeId, long themeId) { - String sql = "SELECT exists(" + - "SELECT 1 " + - "FROM reservation " + - "WHERE date = ? " + - "AND reservation_time_id = ? " + - "AND theme_id = ?)"; + boolean existsByDateAndTimeIdAndThemeId(LocalDate date, long timeId, long themeId); - return jdbcTemplate.queryForObject(sql, Boolean.class, Date.valueOf(date), reservationTimeId, themeId); - } + boolean existsByTimeId(long timeId); } diff --git a/src/main/java/roomescape/repository/ReservationTimeRepository.java b/src/main/java/roomescape/repository/ReservationTimeRepository.java index 339345a55..b4c7f474b 100644 --- a/src/main/java/roomescape/repository/ReservationTimeRepository.java +++ b/src/main/java/roomescape/repository/ReservationTimeRepository.java @@ -1,89 +1,26 @@ package roomescape.repository; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.stereotype.Repository; -import roomescape.domain.ReservationTime; - -import java.sql.Date; -import java.sql.PreparedStatement; -import java.sql.Time; import java.time.LocalDate; import java.time.LocalTime; import java.util.List; import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import roomescape.domain.ReservationTime; @Repository -public class ReservationTimeRepository { - - private final JdbcTemplate jdbcTemplate; - - private final RowMapper reservationTimeRowMapper = (resultSet, rowNum) -> new ReservationTime( - resultSet.getLong("id"), - resultSet.getTime("start_at").toLocalTime() - ); - - - public ReservationTimeRepository(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } - - public ReservationTime save(ReservationTime time) { - String sql = "INSERT INTO reservation_time " + - "(start_at) " + - "VALUES (?)"; - GeneratedKeyHolder keyHolder = new GeneratedKeyHolder(); - - jdbcTemplate.update(con -> { - PreparedStatement ps = con.prepareStatement( - sql, - new String[]{"id"}); - ps.setTime(1, Time.valueOf(time.getStartAt())); - return ps; - }, keyHolder); - return new ReservationTime(keyHolder.getKey().longValue(), time.getStartAt()); - } - - public Optional findById(long id) { - String sql = "SELECT id, start_at " + - "FROM reservation_time " + - "WHERE id = ?"; - List reservationTimes = jdbcTemplate.query(sql, reservationTimeRowMapper, id); - return reservationTimes.isEmpty() ? Optional.empty() : Optional.of(reservationTimes.get(0)); - } - - public Optional findByStartAt(LocalTime startAt) { - String sql = "SELECT id, start_at " + - "FROM reservation_time " + - "WHERE start_at = ?"; - List reservationTimes = jdbcTemplate.query(sql, reservationTimeRowMapper, Time.valueOf(startAt)); - return reservationTimes.isEmpty() ? Optional.empty() : Optional.of(reservationTimes.get(0)); - } - - public List findReservedBy(LocalDate date, long themeId) { - String sql = "SELECT rt.id, rt.start_at " + - "FROM reservation_time AS rt " + - "INNER JOIN reservation AS r " + - "ON rt.id = r.reservation_time_id " + - "WHERE r.date = ? " + - "AND r.theme_id = ?"; - return jdbcTemplate.query(sql, reservationTimeRowMapper, Date.valueOf(date), themeId); - } - - public List findAll() { - String sql = "SELECT id, start_at " + - "FROM reservation_time"; - return jdbcTemplate.query(sql, reservationTimeRowMapper); - } - - public void deleteById(long id) { - String sql = "DELETE FROM reservation_time " + - "WHERE id = ?"; - jdbcTemplate.update(con -> { - PreparedStatement ps = con.prepareStatement(sql); - ps.setLong(1, id); - return ps; - }); - } +public interface ReservationTimeRepository extends JpaRepository { + + Optional findByStartAt(LocalTime startAt); + + @Query("SELECT rt " + + "FROM ReservationTime rt " + + "INNER JOIN Reservation r " + + "ON rt.id = r.time.id " + + "WHERE r.date = :date " + + "AND r.theme.id = :themeId") + List findReservationByThemeIdAndDate(@Param("date") LocalDate date, + @Param("themeId") long themeId); } diff --git a/src/main/java/roomescape/repository/ThemeRepository.java b/src/main/java/roomescape/repository/ThemeRepository.java index 45403a4a7..800c049c3 100644 --- a/src/main/java/roomescape/repository/ThemeRepository.java +++ b/src/main/java/roomescape/repository/ThemeRepository.java @@ -1,85 +1,25 @@ package roomescape.repository; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.stereotype.Repository; -import roomescape.domain.Theme; - -import java.sql.Date; -import java.sql.PreparedStatement; import java.time.LocalDate; import java.util.List; -import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import roomescape.domain.Theme; @Repository -public class ThemeRepository { - - private final JdbcTemplate jdbcTemplate; - - private final RowMapper themeRowMapper = (resultSet, rowNum) -> new Theme( - resultSet.getLong("id"), - resultSet.getString("name"), - resultSet.getString("description"), - resultSet.getString("thumbnail") - ); - - public ThemeRepository(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } - - public Theme save(Theme theme) { - String sql = "INSERT INTO theme " + - "(name, description, thumbnail) " + - "VALUES (?, ? ,?)"; - GeneratedKeyHolder keyHolder = new GeneratedKeyHolder(); - - jdbcTemplate.update(con -> { - PreparedStatement ps = con.prepareStatement( - sql, - new String[]{"id"}); - ps.setString(1, theme.getName()); - ps.setString(2, theme.getDescription()); - ps.setString(3, theme.getThumbnail()); - return ps; - }, keyHolder); - return new Theme(keyHolder.getKey().longValue(), theme.getName(), - theme.getDescription(), theme.getThumbnail()); - } - - public Optional findById(long id) { - String sql = "SELECT id, name, description, thumbnail " + - "FROM theme " + - "WHERE id = ?"; - List themes = jdbcTemplate.query(sql, themeRowMapper, id); - return themes.isEmpty() ? Optional.empty() : Optional.of(themes.get(0)); - } - - public List findAll() { - String sql = "SELECT id, name, description, thumbnail " + - "FROM theme"; - return jdbcTemplate.query(sql, themeRowMapper); - } - - public List findRanksByPeriodAndCount(LocalDate start, LocalDate end, int count) { - String sql = "SELECT t.id, t.name, t.description, t.thumbnail " + - "FROM theme AS t " + - "INNER JOIN reservation AS r " + - "ON t.id = r.theme_id " + - "WHERE r.date BETWEEN ? AND ? " + - "GROUP BY t.id " + - "ORDER BY count(t.id) DESC " + - "LIMIT ?"; - return jdbcTemplate.query(sql, themeRowMapper, Date.valueOf(start), Date.valueOf(end), count); - } - - public void deleteById(long id) { - String sql = "DELETE FROM theme " + - "WHERE id = ?"; - jdbcTemplate.update(con -> { - PreparedStatement ps = con.prepareStatement(sql); - ps.setLong(1, id); - return ps; - }); - } +public interface ThemeRepository extends JpaRepository { + + @Query("SELECT t " + + "FROM Theme t " + + "INNER JOIN Reservation r " + + "ON t.id = r.theme.id " + + "WHERE r.date BETWEEN :dateFrom AND :dateTo " + + "GROUP BY t.id " + + "ORDER BY COUNT(t.id) DESC " + + "LIMIT :limit") + List findRanksByPeriodAndCount(@Param("dateFrom") LocalDate dateFrom, + @Param("dateTo") LocalDate dateTo, + @Param("limit") int limit); } diff --git a/src/main/java/roomescape/service/reservation/ReservationFindService.java b/src/main/java/roomescape/service/reservation/ReservationFindService.java index ac2319339..9a1e8af13 100644 --- a/src/main/java/roomescape/service/reservation/ReservationFindService.java +++ b/src/main/java/roomescape/service/reservation/ReservationFindService.java @@ -1,12 +1,11 @@ package roomescape.service.reservation; +import java.time.LocalDate; +import java.util.List; import org.springframework.stereotype.Service; import roomescape.domain.Reservation; import roomescape.repository.ReservationRepository; -import java.time.LocalDate; -import java.util.List; - @Service public class ReservationFindService { @@ -22,6 +21,6 @@ public List findReservations() { public List searchReservations(long memberId, long themeId, LocalDate dateFrom, LocalDate dateTo) { - return reservationRepository.searchReservations(memberId, themeId, dateFrom, dateTo); + return reservationRepository.findByMemberIdAndThemeIdAndDateBetween(memberId, themeId, dateFrom, dateTo); } } diff --git a/src/main/java/roomescape/service/reservationtime/ReservationTimeDeleteService.java b/src/main/java/roomescape/service/reservationtime/ReservationTimeDeleteService.java index 229162098..65a7e7462 100644 --- a/src/main/java/roomescape/service/reservationtime/ReservationTimeDeleteService.java +++ b/src/main/java/roomescape/service/reservationtime/ReservationTimeDeleteService.java @@ -20,7 +20,7 @@ public void deleteReservationTime(long id) { reservationTimeRepository.findById(id) .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 예약 시간 아이디 입니다.")); - if (reservationRepository.existsByReservationTimeId(id)) { + if (reservationRepository.existsByTimeId(id)) { throw new IllegalArgumentException("이미 예약중인 시간은 삭제할 수 없습니다."); } reservationTimeRepository.deleteById(id); diff --git a/src/main/java/roomescape/service/reservationtime/ReservationTimeFindService.java b/src/main/java/roomescape/service/reservationtime/ReservationTimeFindService.java index 4419d8a0d..bc434b6ba 100644 --- a/src/main/java/roomescape/service/reservationtime/ReservationTimeFindService.java +++ b/src/main/java/roomescape/service/reservationtime/ReservationTimeFindService.java @@ -1,13 +1,12 @@ package roomescape.service.reservationtime; +import java.time.LocalDate; +import java.util.List; import org.springframework.stereotype.Service; import roomescape.domain.ReservationStatus; import roomescape.domain.ReservationTime; import roomescape.repository.ReservationTimeRepository; -import java.time.LocalDate; -import java.util.List; - @Service public class ReservationTimeFindService { @@ -22,7 +21,7 @@ public List findReservationTimes() { } public ReservationStatus findIsBooked(LocalDate date, long themeId) { - List reservedTimes = reservationTimeRepository.findReservedBy(date, themeId); + List reservedTimes = reservationTimeRepository.findReservationByThemeIdAndDate(date, themeId); List reservationTimes = reservationTimeRepository.findAll(); return ReservationStatus.of(reservedTimes, reservationTimes); } diff --git a/src/main/java/roomescape/service/theme/ThemeDeleteService.java b/src/main/java/roomescape/service/theme/ThemeDeleteService.java index 17f0bf771..7760886d8 100644 --- a/src/main/java/roomescape/service/theme/ThemeDeleteService.java +++ b/src/main/java/roomescape/service/theme/ThemeDeleteService.java @@ -20,7 +20,7 @@ public void deleteTheme(long id) { themeRepository.findById(id) .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 테마 아이디 입니다.")); - if (reservationRepository.existsByReservationThemeId(id)) { + if (reservationRepository.existsByThemeId(id)) { throw new IllegalArgumentException("이미 예약중인 테마는 삭제할 수 없습니다."); } diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 72058ed9a..f8166d62e 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -12,6 +12,5 @@ INSERT INTO member (name, email, password, `role`) VALUES ('testUser', 'user@naver.com', '1234', 'USER'); INSERT INTO member (name, email, password, `role`) VALUES ('testAdmin', 'admin@naver.com', '1234', 'ADMIN'); -INSERT INTO reservation (member_id, date, reservation_time_id, theme_Id) +INSERT INTO reservation (member_id, date, time_id, theme_id) VALUES (1, CURRENT_DATE + INTERVAL '1' DAY, 1, 1); - diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql deleted file mode 100644 index b22c5d7e5..000000000 --- a/src/main/resources/schema.sql +++ /dev/null @@ -1,28 +0,0 @@ -CREATE TABLE reservation_time -( - id BIGINT NOT NULL AUTO_INCREMENT, - start_at TIME NOT NULL, - PRIMARY KEY (id) -); - -CREATE TABLE theme -( - id BIGINT NOT NULL AUTO_INCREMENT, - name VARCHAR(255) NOT NULL, - description VARCHAR(255) NOT NULL, - thumbnail VARCHAR(255) NOT NULL, - PRIMARY KEY (id) -); - -CREATE TABLE reservation -( - id BIGINT NOT NULL AUTO_INCREMENT, - member_id BIGINT, - date DATE NOT NULL, - reservation_time_id BIGINT, - theme_id BIGINT, - UNIQUE (date, reservation_time_id, theme_id), - PRIMARY KEY (id), - FOREIGN KEY (reservation_time_id) REFERENCES reservation_time (id), - FOREIGN KEY (theme_id) REFERENCES theme (id) -); diff --git a/src/test/java/roomescape/service/theme/ThemeDeleteServiceTest.java b/src/test/java/roomescape/service/theme/ThemeDeleteServiceTest.java index 378bfc01a..629594f3e 100644 --- a/src/test/java/roomescape/service/theme/ThemeDeleteServiceTest.java +++ b/src/test/java/roomescape/service/theme/ThemeDeleteServiceTest.java @@ -1,18 +1,16 @@ package roomescape.service.theme; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.jdbc.core.JdbcTemplate; -import roomescape.repository.ReservationRepository; -import roomescape.repository.ThemeRepository; - -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import org.springframework.test.annotation.DirtiesContext; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) class ThemeDeleteServiceTest { @Autowired From aec9a5a9960f57592731587e5c81e3053d6c2256 Mon Sep 17 00:00:00 2001 From: woowabrie Date: Sat, 11 May 2024 12:39:43 +0900 Subject: [PATCH 07/42] =?UTF-8?q?feat:=20[2=EB=8B=A8=EA=B3=84]=20=EB=82=B4?= =?UTF-8?q?=20=EC=98=88=EC=95=BD=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=ED=81=B4=EB=9D=BC=EC=9D=B4=EC=96=B8?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/static/js/reservation-mine.js | 65 ++++++++++++++++++ .../resources/templates/reservation-mine.html | 67 +++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 src/main/resources/static/js/reservation-mine.js create mode 100644 src/main/resources/templates/reservation-mine.html diff --git a/src/main/resources/static/js/reservation-mine.js b/src/main/resources/static/js/reservation-mine.js new file mode 100644 index 000000000..a7a2c412d --- /dev/null +++ b/src/main/resources/static/js/reservation-mine.js @@ -0,0 +1,65 @@ +document.addEventListener('DOMContentLoaded', () => { + /* + TODO: [2단계] 내 예약 목록 조회 기능 + endpoint 설정 + */ + fetch('') // 내 예약 목록 조회 API 호출 + .then(response => { + if (response.status === 200) return response.json(); + throw new Error('Read failed'); + }) + .then(render) + .catch(error => console.error('Error fetching reservations:', error)); +}); + +function render(data) { + const tableBody = document.getElementById('table-body'); + tableBody.innerHTML = ''; + + data.forEach(item => { + const row = tableBody.insertRow(); + + /* + TODO: [2단계] 내 예약 목록 조회 기능 + response 명세에 맞춰 값 설정 + */ + const theme = ''; + const date = ''; + const time = ''; + const status = ''; + + row.insertCell(0).textContent = theme; + row.insertCell(1).textContent = date; + row.insertCell(2).textContent = time; + row.insertCell(3).textContent = status; + + /* + TODO: [3단계] 예약 대기 기능 - 예약 대기 취소 기능 구현 후 활성화 + */ + if (status !== '예약') { // 예약 대기 상태일 때 예약 대기 취소 버튼 추가하는 코드, 상태 값은 변경 가능 + const cancelCell = row.insertCell(4); + const cancelButton = document.createElement('button'); + cancelButton.textContent = '취소'; + cancelButton.className = 'btn btn-danger'; + cancelButton.onclick = function () { + requestDeleteWaiting(item.id).then(() => window.location.reload()); + }; + cancelCell.appendChild(cancelButton); + } else { // 예약 완료 상태일 때 + row.insertCell(4).textContent = ''; + } + }); +} + +function requestDeleteWaiting(id) { + /* + TODO: [3단계] 예약 대기 기능 - 예약 대기 취소 API 호출 + */ + const endpoint = ''; + return fetch(endpoint, { + method: 'DELETE' + }).then(response => { + if (response.status === 204) return; + throw new Error('Delete failed'); + }); +} diff --git a/src/main/resources/templates/reservation-mine.html b/src/main/resources/templates/reservation-mine.html new file mode 100644 index 000000000..d938c6f46 --- /dev/null +++ b/src/main/resources/templates/reservation-mine.html @@ -0,0 +1,67 @@ + + + + + + 방탈출 어드민 + + + + + + + + +
+

내 예약

+
+ + + + + + + + + + + + +
테마날짜시간상태
+
+ + + + + From 5c1237099ff0c7c935ebe5628cd4ba9851fc169e Mon Sep 17 00:00:00 2001 From: Minjoo Date: Wed, 15 May 2024 14:38:10 +0900 Subject: [PATCH 08/42] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=EC=98=88?= =?UTF-8?q?=EC=95=BD=20=EC=A1=B0=ED=9A=8C=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EB=A0=8C=EB=8D=94=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: tkdgur0906 --- src/main/java/roomescape/controller/web/UserController.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/roomescape/controller/web/UserController.java b/src/main/java/roomescape/controller/web/UserController.java index 0be99253c..07af6bae1 100644 --- a/src/main/java/roomescape/controller/web/UserController.java +++ b/src/main/java/roomescape/controller/web/UserController.java @@ -15,4 +15,9 @@ public String reservationUserPage() { public String mainUserPage() { return "index"; } + + @GetMapping("/reservation-mine") + public String reservationMinePage() { + return "reservation-mine"; + } } From c420dcda47bf3c946967abcec268bcf8c51dd77c Mon Sep 17 00:00:00 2001 From: Minjoo Date: Wed, 15 May 2024 15:02:51 +0900 Subject: [PATCH 09/42] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=EC=98=88?= =?UTF-8?q?=EC=95=BD=20=EC=A1=B0=ED=9A=8C=20api=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: tkdgur0906 --- .../api/ReservationApiController.java | 11 ++++++++++ .../repository/ReservationRepository.java | 2 ++ .../dto/response/UserReservationResponse.java | 21 +++++++++++++++++++ .../reservation/ReservationFindService.java | 4 ++++ .../resources/static/js/reservation-mine.js | 18 +++++----------- .../api/ReservationApiControllerTest.java | 11 ++++++++++ 6 files changed, 54 insertions(+), 13 deletions(-) create mode 100644 src/main/java/roomescape/service/dto/response/UserReservationResponse.java diff --git a/src/main/java/roomescape/controller/api/ReservationApiController.java b/src/main/java/roomescape/controller/api/ReservationApiController.java index 56abe14da..c95ca02fb 100644 --- a/src/main/java/roomescape/controller/api/ReservationApiController.java +++ b/src/main/java/roomescape/controller/api/ReservationApiController.java @@ -17,6 +17,7 @@ import roomescape.service.dto.request.ReservationAdminSaveRequest; import roomescape.service.dto.request.ReservationSaveRequest; import roomescape.service.dto.response.ReservationResponse; +import roomescape.service.dto.response.UserReservationResponse; import roomescape.service.reservation.AdminReservationCreateService; import roomescape.service.reservation.ReservationCreateService; import roomescape.service.reservation.ReservationDeleteService; @@ -69,6 +70,16 @@ public ResponseEntity> getSearchingReservations(@Reque ); } + @GetMapping("/reservations-mine") + public ResponseEntity> getUserReservations(@AuthenticatedMember Member member) { + List userReservations = reservationFindService.findUserReservations(member.getId()); + return ResponseEntity.ok( + userReservations.stream() + .map(UserReservationResponse::new) + .toList() + ); + } + @PostMapping("/reservations") public ResponseEntity addReservationByUser(@RequestBody @Valid ReservationSaveRequest request, @AuthenticatedMember Member member) { diff --git a/src/main/java/roomescape/repository/ReservationRepository.java b/src/main/java/roomescape/repository/ReservationRepository.java index 0cdfe0f39..94c307994 100644 --- a/src/main/java/roomescape/repository/ReservationRepository.java +++ b/src/main/java/roomescape/repository/ReservationRepository.java @@ -13,6 +13,8 @@ public interface ReservationRepository extends JpaRepository List findByMemberIdAndThemeIdAndDateBetween(long memberId, long themeId, LocalDate dateFrom, LocalDate dateTo); + List findByMemberId(long memberId); + boolean existsByDateAndTimeIdAndThemeId(LocalDate date, long timeId, long themeId); boolean existsByTimeId(long timeId); diff --git a/src/main/java/roomescape/service/dto/response/UserReservationResponse.java b/src/main/java/roomescape/service/dto/response/UserReservationResponse.java new file mode 100644 index 000000000..137918170 --- /dev/null +++ b/src/main/java/roomescape/service/dto/response/UserReservationResponse.java @@ -0,0 +1,21 @@ +package roomescape.service.dto.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import java.time.LocalDate; +import java.time.LocalTime; +import roomescape.domain.Reservation; + +public record UserReservationResponse(Long reservationId, + String theme, + @JsonFormat(pattern = "YYYY-MM-dd") LocalDate date, + @JsonFormat(pattern = "HH:mm") LocalTime time, + String status) { + + public UserReservationResponse(Reservation reservation) { + this(reservation.getId(), + reservation.getTheme().getName(), + reservation.getDate(), + reservation.getReservationTime().getStartAt(), + "예약"); + } +} diff --git a/src/main/java/roomescape/service/reservation/ReservationFindService.java b/src/main/java/roomescape/service/reservation/ReservationFindService.java index 9a1e8af13..2be99bb55 100644 --- a/src/main/java/roomescape/service/reservation/ReservationFindService.java +++ b/src/main/java/roomescape/service/reservation/ReservationFindService.java @@ -23,4 +23,8 @@ public List searchReservations(long memberId, long themeId, LocalDate dateFrom, LocalDate dateTo) { return reservationRepository.findByMemberIdAndThemeIdAndDateBetween(memberId, themeId, dateFrom, dateTo); } + + public List findUserReservations(long memberId) { + return reservationRepository.findByMemberId(memberId); + } } diff --git a/src/main/resources/static/js/reservation-mine.js b/src/main/resources/static/js/reservation-mine.js index a7a2c412d..96be821db 100644 --- a/src/main/resources/static/js/reservation-mine.js +++ b/src/main/resources/static/js/reservation-mine.js @@ -1,9 +1,5 @@ document.addEventListener('DOMContentLoaded', () => { - /* - TODO: [2단계] 내 예약 목록 조회 기능 - endpoint 설정 - */ - fetch('') // 내 예약 목록 조회 API 호출 + fetch('/reservations-mine') // 내 예약 목록 조회 API 호출 .then(response => { if (response.status === 200) return response.json(); throw new Error('Read failed'); @@ -19,14 +15,10 @@ function render(data) { data.forEach(item => { const row = tableBody.insertRow(); - /* - TODO: [2단계] 내 예약 목록 조회 기능 - response 명세에 맞춰 값 설정 - */ - const theme = ''; - const date = ''; - const time = ''; - const status = ''; + const theme = item.theme; + const date = item.date; + const time = item.time; + const status = item.status; row.insertCell(0).textContent = theme; row.insertCell(1).textContent = date; diff --git a/src/test/java/roomescape/controller/api/ReservationApiControllerTest.java b/src/test/java/roomescape/controller/api/ReservationApiControllerTest.java index ac48e44df..8031bbdff 100644 --- a/src/test/java/roomescape/controller/api/ReservationApiControllerTest.java +++ b/src/test/java/roomescape/controller/api/ReservationApiControllerTest.java @@ -53,6 +53,17 @@ void selectReservationListRequest_Success() { .body("size()", is(1)); } + @Test + @DisplayName("유저 예약 목록 조회를 정상적으로 수행한다.") + void selectUserReservationListRequest_Success() { + RestAssured.given().log().all() + .cookie("token", TokenGenerator.makeUserToken()) + .when().get("/reservations-mine") + .then().log().all() + .statusCode(200) + .body("size()", is(1)); + } + @Test @DisplayName("예약 추가, 조회를 정상적으로 수행한다.") void ReservationTime_CREATE_READ_Success() { From 4f9e9a3abf69681c786cdbd0cc4c89720eecbc6f Mon Sep 17 00:00:00 2001 From: Minjoo Date: Wed, 15 May 2024 15:06:57 +0900 Subject: [PATCH 10/42] =?UTF-8?q?style:=20=EC=BD=94=EB=93=9C=20=ED=8F=AC?= =?UTF-8?q?=EB=A9=A7=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: tkdgur0906 --- .../java/roomescape/config/AuthWebConfig.java | 5 ++-- .../controller/api/MemberApiController.java | 3 +- .../api/ReservationApiController.java | 7 ++--- .../api/ReservationTimeApiController.java | 10 +++---- .../controller/api/ThemeApiController.java | 5 ++-- .../roomescape/domain/ReservationStatus.java | 8 ++++-- .../service/auth/TokenProvider.java | 5 ++-- .../request/ReservationAdminSaveRequest.java | 3 +- .../dto/request/ReservationSaveRequest.java | 3 +- .../request/ReservationTimeSaveRequest.java | 3 +- .../dto/response/ReservationResponse.java | 7 +++-- .../response/ReservationStatusResponse.java | 3 +- .../dto/response/ReservationTimeResponse.java | 3 +- .../service/member/MemberService.java | 3 +- .../ReservationCreateValidator.java | 5 ++-- .../service/theme/ThemeFindService.java | 5 ++-- src/test/java/roomescape/JDBCTest.java | 9 +++--- .../api/MemberApiControllerTest.java | 4 +-- .../api/ReservationApiControllerTest.java | 14 ++++------ .../api/ReservationTimeApiControllerTest.java | 7 ++--- .../api/ThemeApiControllerTest.java | 7 ++--- .../domain/ReservationStatusTest.java | 9 +++--- .../dto/ReservationSaveRequestTest.java | 7 ++--- .../service/dto/ThemeSaveRequestTest.java | 4 +-- .../ReservationCreateServiceTest.java | 28 +++---------------- .../ReservationTimeCreateServiceTest.java | 13 +++------ .../ReservationTimeDeleteServiceTest.java | 10 ++----- .../ReservationTimeFindServiceTest.java | 14 ++++------ 28 files changed, 78 insertions(+), 126 deletions(-) diff --git a/src/main/java/roomescape/config/AuthWebConfig.java b/src/main/java/roomescape/config/AuthWebConfig.java index 40fbc88b4..8617436cd 100644 --- a/src/main/java/roomescape/config/AuthWebConfig.java +++ b/src/main/java/roomescape/config/AuthWebConfig.java @@ -1,5 +1,6 @@ package roomescape.config; +import java.util.List; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; @@ -7,8 +8,6 @@ import roomescape.auth.AdminAuthHandlerInterceptor; import roomescape.auth.AuthenticatedMemberArgumentResolver; -import java.util.List; - @Configuration public class AuthWebConfig implements WebMvcConfigurer { @@ -28,6 +27,6 @@ public void addArgumentResolvers(List resolvers) @Override public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(adminAuthHandlerInterceptor).addPathPatterns("/admin/**","/members"); + registry.addInterceptor(adminAuthHandlerInterceptor).addPathPatterns("/admin/**", "/members"); } } diff --git a/src/main/java/roomescape/controller/api/MemberApiController.java b/src/main/java/roomescape/controller/api/MemberApiController.java index 6c50651a8..aa9dbdce0 100644 --- a/src/main/java/roomescape/controller/api/MemberApiController.java +++ b/src/main/java/roomescape/controller/api/MemberApiController.java @@ -1,5 +1,6 @@ package roomescape.controller.api; +import java.util.List; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @@ -7,8 +8,6 @@ import roomescape.service.dto.response.MemberIdAndNameResponse; import roomescape.service.member.MemberService; -import java.util.List; - @RestController public class MemberApiController { diff --git a/src/main/java/roomescape/controller/api/ReservationApiController.java b/src/main/java/roomescape/controller/api/ReservationApiController.java index c95ca02fb..32eee9703 100644 --- a/src/main/java/roomescape/controller/api/ReservationApiController.java +++ b/src/main/java/roomescape/controller/api/ReservationApiController.java @@ -2,6 +2,9 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.Positive; +import java.net.URI; +import java.time.LocalDate; +import java.util.List; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; @@ -23,10 +26,6 @@ import roomescape.service.reservation.ReservationDeleteService; import roomescape.service.reservation.ReservationFindService; -import java.net.URI; -import java.time.LocalDate; -import java.util.List; - @Validated @RestController public class ReservationApiController { diff --git a/src/main/java/roomescape/controller/api/ReservationTimeApiController.java b/src/main/java/roomescape/controller/api/ReservationTimeApiController.java index 28d67b13c..b14d9a481 100644 --- a/src/main/java/roomescape/controller/api/ReservationTimeApiController.java +++ b/src/main/java/roomescape/controller/api/ReservationTimeApiController.java @@ -2,6 +2,9 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.Positive; +import java.net.URI; +import java.time.LocalDate; +import java.util.List; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; @@ -20,10 +23,6 @@ import roomescape.service.reservationtime.ReservationTimeDeleteService; import roomescape.service.reservationtime.ReservationTimeFindService; -import java.net.URI; -import java.time.LocalDate; -import java.util.List; - @Validated @RestController public class ReservationTimeApiController { @@ -66,7 +65,8 @@ public ResponseEntity> getReservationTimesIsBook } @PostMapping("/times") - public ResponseEntity addReservationTime(@RequestBody @Valid ReservationTimeSaveRequest request) { + public ResponseEntity addReservationTime( + @RequestBody @Valid ReservationTimeSaveRequest request) { ReservationTime reservationTime = reservationTimeCreateService.createReservationTime(request); return ResponseEntity.created(URI.create("times/" + reservationTime.getId())) .body(new ReservationTimeResponse(reservationTime)); diff --git a/src/main/java/roomescape/controller/api/ThemeApiController.java b/src/main/java/roomescape/controller/api/ThemeApiController.java index af5ee3879..b61872bb6 100644 --- a/src/main/java/roomescape/controller/api/ThemeApiController.java +++ b/src/main/java/roomescape/controller/api/ThemeApiController.java @@ -2,6 +2,8 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.Positive; +import java.net.URI; +import java.util.List; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; @@ -17,9 +19,6 @@ import roomescape.service.theme.ThemeDeleteService; import roomescape.service.theme.ThemeFindService; -import java.net.URI; -import java.util.List; - @Validated @RestController public class ThemeApiController { diff --git a/src/main/java/roomescape/domain/ReservationStatus.java b/src/main/java/roomescape/domain/ReservationStatus.java index f9f1ecec7..7aa414841 100644 --- a/src/main/java/roomescape/domain/ReservationStatus.java +++ b/src/main/java/roomescape/domain/ReservationStatus.java @@ -36,8 +36,12 @@ public Map getReservationStatus() { @Override public boolean equals(Object object) { - if (this == object) return true; - if (object == null || getClass() != object.getClass()) return false; + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } ReservationStatus that = (ReservationStatus) object; return Objects.equals(reservationStatus, that.reservationStatus); } diff --git a/src/main/java/roomescape/service/auth/TokenProvider.java b/src/main/java/roomescape/service/auth/TokenProvider.java index ef1f6ad1f..73f9b0086 100644 --- a/src/main/java/roomescape/service/auth/TokenProvider.java +++ b/src/main/java/roomescape/service/auth/TokenProvider.java @@ -3,14 +3,13 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import jakarta.servlet.http.Cookie; +import java.security.Key; +import java.util.Arrays; import java.util.Date; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import roomescape.exception.AuthenticationException; -import java.security.Key; -import java.util.Arrays; - @Component public class TokenProvider { diff --git a/src/main/java/roomescape/service/dto/request/ReservationAdminSaveRequest.java b/src/main/java/roomescape/service/dto/request/ReservationAdminSaveRequest.java index b1f009fca..803198ed9 100644 --- a/src/main/java/roomescape/service/dto/request/ReservationAdminSaveRequest.java +++ b/src/main/java/roomescape/service/dto/request/ReservationAdminSaveRequest.java @@ -1,13 +1,12 @@ package roomescape.service.dto.request; import jakarta.validation.constraints.NotNull; +import java.time.LocalDate; import roomescape.domain.Member; import roomescape.domain.Reservation; import roomescape.domain.ReservationTime; import roomescape.domain.Theme; -import java.time.LocalDate; - public record ReservationAdminSaveRequest(@NotNull(message = "멤버를 입력해주세요") Long memberId, @NotNull(message = "예약 날짜를 입력해주세요.") LocalDate date, @NotNull(message = "예약 시간을 입력해주세요.") Long timeId, diff --git a/src/main/java/roomescape/service/dto/request/ReservationSaveRequest.java b/src/main/java/roomescape/service/dto/request/ReservationSaveRequest.java index 7f29e9351..6e816959b 100644 --- a/src/main/java/roomescape/service/dto/request/ReservationSaveRequest.java +++ b/src/main/java/roomescape/service/dto/request/ReservationSaveRequest.java @@ -1,13 +1,12 @@ package roomescape.service.dto.request; import jakarta.validation.constraints.NotNull; +import java.time.LocalDate; import roomescape.domain.Member; import roomescape.domain.Reservation; import roomescape.domain.ReservationTime; import roomescape.domain.Theme; -import java.time.LocalDate; - public record ReservationSaveRequest(@NotNull(message = "예약 날짜를 입력해주세요.") LocalDate date, @NotNull(message = "예약 시간을 입력해주세요.") Long timeId, @NotNull(message = "예약 테마를 입력해주세요.") Long themeId) { diff --git a/src/main/java/roomescape/service/dto/request/ReservationTimeSaveRequest.java b/src/main/java/roomescape/service/dto/request/ReservationTimeSaveRequest.java index 3cd620603..f93f81883 100644 --- a/src/main/java/roomescape/service/dto/request/ReservationTimeSaveRequest.java +++ b/src/main/java/roomescape/service/dto/request/ReservationTimeSaveRequest.java @@ -1,9 +1,8 @@ package roomescape.service.dto.request; import jakarta.validation.constraints.NotNull; -import roomescape.domain.ReservationTime; - import java.time.LocalTime; +import roomescape.domain.ReservationTime; public record ReservationTimeSaveRequest(@NotNull(message = "예약 시간을 입력해주세요.") LocalTime startAt) { diff --git a/src/main/java/roomescape/service/dto/response/ReservationResponse.java b/src/main/java/roomescape/service/dto/response/ReservationResponse.java index a28a5fd66..75579677b 100644 --- a/src/main/java/roomescape/service/dto/response/ReservationResponse.java +++ b/src/main/java/roomescape/service/dto/response/ReservationResponse.java @@ -1,10 +1,11 @@ package roomescape.service.dto.response; -import roomescape.domain.Reservation; - import java.time.LocalDate; +import roomescape.domain.Reservation; -public record ReservationResponse(Long id, MemberIdAndNameResponse member, LocalDate date, +public record ReservationResponse(Long id, + MemberIdAndNameResponse member, + LocalDate date, ReservationTimeResponse time, ThemeResponse theme) { diff --git a/src/main/java/roomescape/service/dto/response/ReservationStatusResponse.java b/src/main/java/roomescape/service/dto/response/ReservationStatusResponse.java index d8a9f9b87..ea27957db 100644 --- a/src/main/java/roomescape/service/dto/response/ReservationStatusResponse.java +++ b/src/main/java/roomescape/service/dto/response/ReservationStatusResponse.java @@ -1,8 +1,7 @@ package roomescape.service.dto.response; -import roomescape.domain.ReservationTime; - import java.time.LocalTime; +import roomescape.domain.ReservationTime; public record ReservationStatusResponse(LocalTime startAt, Long timeId, boolean alreadyBooked) { diff --git a/src/main/java/roomescape/service/dto/response/ReservationTimeResponse.java b/src/main/java/roomescape/service/dto/response/ReservationTimeResponse.java index 4d6d29469..12c773703 100644 --- a/src/main/java/roomescape/service/dto/response/ReservationTimeResponse.java +++ b/src/main/java/roomescape/service/dto/response/ReservationTimeResponse.java @@ -1,9 +1,8 @@ package roomescape.service.dto.response; import com.fasterxml.jackson.annotation.JsonFormat; -import roomescape.domain.ReservationTime; - import java.time.LocalTime; +import roomescape.domain.ReservationTime; public record ReservationTimeResponse(Long id, @JsonFormat(pattern = "HH:mm") LocalTime startAt) { diff --git a/src/main/java/roomescape/service/member/MemberService.java b/src/main/java/roomescape/service/member/MemberService.java index aa23ee6fd..3c4056ae4 100644 --- a/src/main/java/roomescape/service/member/MemberService.java +++ b/src/main/java/roomescape/service/member/MemberService.java @@ -1,11 +1,10 @@ package roomescape.service.member; +import java.util.List; import org.springframework.stereotype.Service; import roomescape.domain.Member; import roomescape.repository.MemberRepository; -import java.util.List; - @Service public class MemberService { diff --git a/src/main/java/roomescape/service/reservation/ReservationCreateValidator.java b/src/main/java/roomescape/service/reservation/ReservationCreateValidator.java index 624f0ec21..0f19f6e9a 100644 --- a/src/main/java/roomescape/service/reservation/ReservationCreateValidator.java +++ b/src/main/java/roomescape/service/reservation/ReservationCreateValidator.java @@ -1,5 +1,7 @@ package roomescape.service.reservation; +import java.time.LocalDate; +import java.time.LocalDateTime; import org.springframework.stereotype.Component; import roomescape.domain.Member; import roomescape.domain.ReservationTime; @@ -9,9 +11,6 @@ import roomescape.repository.ReservationTimeRepository; import roomescape.repository.ThemeRepository; -import java.time.LocalDate; -import java.time.LocalDateTime; - @Component public class ReservationCreateValidator { diff --git a/src/main/java/roomescape/service/theme/ThemeFindService.java b/src/main/java/roomescape/service/theme/ThemeFindService.java index f77b10fc2..0cbcfdce1 100644 --- a/src/main/java/roomescape/service/theme/ThemeFindService.java +++ b/src/main/java/roomescape/service/theme/ThemeFindService.java @@ -1,12 +1,11 @@ package roomescape.service.theme; +import java.time.LocalDate; +import java.util.List; import org.springframework.stereotype.Service; import roomescape.domain.Theme; import roomescape.repository.ThemeRepository; -import java.time.LocalDate; -import java.util.List; - @Service public class ThemeFindService { private static final int START_DAYS_SUBTRACT = 7; diff --git a/src/test/java/roomescape/JDBCTest.java b/src/test/java/roomescape/JDBCTest.java index 3dbc8b2fc..620090a4c 100644 --- a/src/test/java/roomescape/JDBCTest.java +++ b/src/test/java/roomescape/JDBCTest.java @@ -1,16 +1,15 @@ package roomescape; +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.Connection; +import java.sql.SQLException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.jdbc.core.JdbcTemplate; -import java.sql.Connection; -import java.sql.SQLException; - -import static org.assertj.core.api.Assertions.assertThat; - @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) public class JDBCTest { diff --git a/src/test/java/roomescape/controller/api/MemberApiControllerTest.java b/src/test/java/roomescape/controller/api/MemberApiControllerTest.java index eb8248359..e6f80b88d 100644 --- a/src/test/java/roomescape/controller/api/MemberApiControllerTest.java +++ b/src/test/java/roomescape/controller/api/MemberApiControllerTest.java @@ -1,13 +1,13 @@ package roomescape.controller.api; +import static org.hamcrest.Matchers.is; + import io.restassured.RestAssured; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import roomescape.util.TokenGenerator; -import static org.hamcrest.Matchers.is; - @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) class MemberApiControllerTest { diff --git a/src/test/java/roomescape/controller/api/ReservationApiControllerTest.java b/src/test/java/roomescape/controller/api/ReservationApiControllerTest.java index 8031bbdff..b88091780 100644 --- a/src/test/java/roomescape/controller/api/ReservationApiControllerTest.java +++ b/src/test/java/roomescape/controller/api/ReservationApiControllerTest.java @@ -1,7 +1,13 @@ package roomescape.controller.api; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.is; + import io.restassured.RestAssured; import io.restassured.http.ContentType; +import java.lang.reflect.Field; +import java.time.LocalDate; +import java.util.Map; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -10,13 +16,6 @@ import org.springframework.test.annotation.DirtiesContext; import roomescape.util.TokenGenerator; -import java.lang.reflect.Field; -import java.time.LocalDate; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.hamcrest.Matchers.is; - @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) public class ReservationApiControllerTest { @@ -81,7 +80,6 @@ void ReservationTime_CREATE_READ_Success() { .then().log().all() .statusCode(201); - RestAssured.given().log().all() .when().get("/reservations") .then().log().all() diff --git a/src/test/java/roomescape/controller/api/ReservationTimeApiControllerTest.java b/src/test/java/roomescape/controller/api/ReservationTimeApiControllerTest.java index aea42437f..d118fb944 100644 --- a/src/test/java/roomescape/controller/api/ReservationTimeApiControllerTest.java +++ b/src/test/java/roomescape/controller/api/ReservationTimeApiControllerTest.java @@ -1,16 +1,15 @@ package roomescape.controller.api; +import static org.hamcrest.Matchers.is; + import io.restassured.RestAssured; import io.restassured.http.ContentType; +import java.util.Map; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.DirtiesContext; -import java.util.Map; - -import static org.hamcrest.Matchers.is; - @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) public class ReservationTimeApiControllerTest { diff --git a/src/test/java/roomescape/controller/api/ThemeApiControllerTest.java b/src/test/java/roomescape/controller/api/ThemeApiControllerTest.java index 0f1e2f105..e922ac475 100644 --- a/src/test/java/roomescape/controller/api/ThemeApiControllerTest.java +++ b/src/test/java/roomescape/controller/api/ThemeApiControllerTest.java @@ -1,16 +1,15 @@ package roomescape.controller.api; +import static org.hamcrest.Matchers.is; + import io.restassured.RestAssured; import io.restassured.http.ContentType; +import java.util.Map; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.DirtiesContext; -import java.util.Map; - -import static org.hamcrest.Matchers.is; - @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) public class ThemeApiControllerTest { diff --git a/src/test/java/roomescape/domain/ReservationStatusTest.java b/src/test/java/roomescape/domain/ReservationStatusTest.java index 8297d444e..0e95d0848 100644 --- a/src/test/java/roomescape/domain/ReservationStatusTest.java +++ b/src/test/java/roomescape/domain/ReservationStatusTest.java @@ -1,13 +1,12 @@ package roomescape.domain; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; import java.time.LocalTime; import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; class ReservationStatusTest { diff --git a/src/test/java/roomescape/service/dto/ReservationSaveRequestTest.java b/src/test/java/roomescape/service/dto/ReservationSaveRequestTest.java index bc47df209..723e5e467 100644 --- a/src/test/java/roomescape/service/dto/ReservationSaveRequestTest.java +++ b/src/test/java/roomescape/service/dto/ReservationSaveRequestTest.java @@ -1,13 +1,12 @@ package roomescape.service.dto; +import static org.assertj.core.api.Assertions.assertThatCode; + +import java.time.LocalDate; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import roomescape.service.dto.request.ReservationSaveRequest; -import java.time.LocalDate; - -import static org.assertj.core.api.Assertions.assertThatCode; - class ReservationSaveRequestTest { @Test diff --git a/src/test/java/roomescape/service/dto/ThemeSaveRequestTest.java b/src/test/java/roomescape/service/dto/ThemeSaveRequestTest.java index 73f7719f5..5d9a71e1c 100644 --- a/src/test/java/roomescape/service/dto/ThemeSaveRequestTest.java +++ b/src/test/java/roomescape/service/dto/ThemeSaveRequestTest.java @@ -1,11 +1,11 @@ package roomescape.service.dto; +import static org.assertj.core.api.Assertions.assertThatCode; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import roomescape.service.dto.request.ThemeSaveRequest; -import static org.assertj.core.api.Assertions.assertThatCode; - public class ThemeSaveRequestTest { @Test diff --git a/src/test/java/roomescape/service/reservation/ReservationCreateServiceTest.java b/src/test/java/roomescape/service/reservation/ReservationCreateServiceTest.java index bc6b4a766..6398da40b 100644 --- a/src/test/java/roomescape/service/reservation/ReservationCreateServiceTest.java +++ b/src/test/java/roomescape/service/reservation/ReservationCreateServiceTest.java @@ -1,43 +1,23 @@ package roomescape.service.reservation; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.time.LocalDate; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.jdbc.core.JdbcTemplate; import roomescape.domain.Member; import roomescape.domain.Role; -import roomescape.repository.MemberRepository; -import roomescape.repository.ReservationRepository; -import roomescape.repository.ReservationTimeRepository; -import roomescape.repository.ThemeRepository; import roomescape.service.dto.request.ReservationSaveRequest; -import java.time.LocalDate; - -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) class ReservationCreateServiceTest { @Autowired private ReservationCreateService reservationCreateService; -// @Autowired -// public ReservationCreateServiceTest(JdbcTemplate jdbcTemplate) { -// reservationCreateService = new ReservationCreateService( -// new ReservationCreateValidator( -// new ReservationRepository(jdbcTemplate), -// new ReservationTimeRepository(jdbcTemplate), -// new ThemeRepository(jdbcTemplate), -// new MemberRepository(jdbcTemplate) -// ), -// new ReservationRepository(jdbcTemplate) -// ); -// } - @Test @DisplayName("예약 가능한 시간인 경우 성공한다.") void checkDuplicateReservationTime_Success() { diff --git a/src/test/java/roomescape/service/reservationtime/ReservationTimeCreateServiceTest.java b/src/test/java/roomescape/service/reservationtime/ReservationTimeCreateServiceTest.java index 32f624078..abb3ec26d 100644 --- a/src/test/java/roomescape/service/reservationtime/ReservationTimeCreateServiceTest.java +++ b/src/test/java/roomescape/service/reservationtime/ReservationTimeCreateServiceTest.java @@ -1,19 +1,15 @@ package roomescape.service.reservationtime; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.time.LocalTime; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.jdbc.core.JdbcTemplate; -import roomescape.repository.ReservationTimeRepository; import roomescape.service.dto.request.ReservationTimeSaveRequest; -import java.time.LocalTime; - -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) class ReservationTimeCreateServiceTest { @@ -21,7 +17,6 @@ class ReservationTimeCreateServiceTest { private ReservationTimeCreateService reservationTimeCreateService; - @Test @DisplayName("존재하지 않는 예약 시간인 경우 성공한다") void checkDuplicateTime_Success() { diff --git a/src/test/java/roomescape/service/reservationtime/ReservationTimeDeleteServiceTest.java b/src/test/java/roomescape/service/reservationtime/ReservationTimeDeleteServiceTest.java index 08352ce67..beec20140 100644 --- a/src/test/java/roomescape/service/reservationtime/ReservationTimeDeleteServiceTest.java +++ b/src/test/java/roomescape/service/reservationtime/ReservationTimeDeleteServiceTest.java @@ -1,17 +1,13 @@ package roomescape.service.reservationtime; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.annotation.DirtiesContext; -import roomescape.repository.ReservationRepository; -import roomescape.repository.ReservationTimeRepository; - -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) diff --git a/src/test/java/roomescape/service/reservationtime/ReservationTimeFindServiceTest.java b/src/test/java/roomescape/service/reservationtime/ReservationTimeFindServiceTest.java index ee9452699..794b8604d 100644 --- a/src/test/java/roomescape/service/reservationtime/ReservationTimeFindServiceTest.java +++ b/src/test/java/roomescape/service/reservationtime/ReservationTimeFindServiceTest.java @@ -1,21 +1,17 @@ package roomescape.service.reservationtime; +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Map; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.annotation.DirtiesContext; import roomescape.domain.ReservationStatus; import roomescape.domain.ReservationTime; -import roomescape.repository.ReservationTimeRepository; - -import java.time.LocalDate; -import java.time.LocalTime; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) From c224b2bc99eeb0072a39a6fe65566532ac4756df Mon Sep 17 00:00:00 2001 From: Minjoo Date: Wed, 15 May 2024 15:13:07 +0900 Subject: [PATCH 11/42] =?UTF-8?q?feat:=20Member=20equals,=20hashCode=20?= =?UTF-8?q?=EC=9E=AC=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: tkdgur0906 --- src/main/java/roomescape/domain/Member.java | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/main/java/roomescape/domain/Member.java b/src/main/java/roomescape/domain/Member.java index f62d1f9c9..b10280eb7 100644 --- a/src/main/java/roomescape/domain/Member.java +++ b/src/main/java/roomescape/domain/Member.java @@ -6,6 +6,7 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import java.util.Objects; @Entity public class Member { @@ -55,4 +56,23 @@ public String getPassword() { public Role getRole() { return role; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Member member = (Member) o; + return Objects.equals(id, member.id) && Objects.equals(name, member.name) + && Objects.equals(email, member.email) && Objects.equals(password, member.password) + && role == member.role; + } + + @Override + public int hashCode() { + return Objects.hash(id, name, email, password, role); + } } From 7f9ef6204f5ab4102c84932167b057d0bc76df92 Mon Sep 17 00:00:00 2001 From: Minjoo Date: Wed, 15 May 2024 15:20:26 +0900 Subject: [PATCH 12/42] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: tkdgur0906 --- build.gradle | 1 - src/test/java/roomescape/JDBCTest.java | 30 -------------------------- 2 files changed, 31 deletions(-) delete mode 100644 src/test/java/roomescape/JDBCTest.java diff --git a/build.gradle b/build.gradle index a04e9e271..377b055d6 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,6 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter' 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 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'io.jsonwebtoken:jjwt-api:0.12.5' diff --git a/src/test/java/roomescape/JDBCTest.java b/src/test/java/roomescape/JDBCTest.java deleted file mode 100644 index 620090a4c..000000000 --- a/src/test/java/roomescape/JDBCTest.java +++ /dev/null @@ -1,30 +0,0 @@ -package roomescape; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.sql.Connection; -import java.sql.SQLException; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.jdbc.core.JdbcTemplate; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) -public class JDBCTest { - - @Autowired - private JdbcTemplate jdbcTemplate; - - @Test - @DisplayName("예약 테이블이 데이터베이스에 정상적으로 생성된다.") - void makeReservationTable_Success() { - try (Connection connection = jdbcTemplate.getDataSource().getConnection()) { - assertThat(connection).isNotNull(); - assertThat(connection.getCatalog()).isEqualTo("DATABASE"); - assertThat(connection.getMetaData().getTables(null, null, "RESERVATION", null).next()).isTrue(); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } -} From 0e2303e45e5c3fa79bb43705d2b54387d795e594 Mon Sep 17 00:00:00 2001 From: Minjoo Date: Wed, 15 May 2024 15:54:02 +0900 Subject: [PATCH 13/42] =?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=EC=A0=9C=EA=B1=B0=20=EB=B0=8F?= =?UTF-8?q?=20=EA=B0=9C=ED=96=89=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/service/theme/ThemeFindService.java | 1 + .../reservationtime/ReservationTimeCreateServiceTest.java | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/roomescape/service/theme/ThemeFindService.java b/src/main/java/roomescape/service/theme/ThemeFindService.java index 0cbcfdce1..439a78ae3 100644 --- a/src/main/java/roomescape/service/theme/ThemeFindService.java +++ b/src/main/java/roomescape/service/theme/ThemeFindService.java @@ -8,6 +8,7 @@ @Service public class ThemeFindService { + private static final int START_DAYS_SUBTRACT = 7; private static final int END_DAYS_SUBTRACT = 1; private static final int RANK_COUNT = 7; diff --git a/src/test/java/roomescape/service/reservationtime/ReservationTimeCreateServiceTest.java b/src/test/java/roomescape/service/reservationtime/ReservationTimeCreateServiceTest.java index abb3ec26d..7e9c3a44b 100644 --- a/src/test/java/roomescape/service/reservationtime/ReservationTimeCreateServiceTest.java +++ b/src/test/java/roomescape/service/reservationtime/ReservationTimeCreateServiceTest.java @@ -16,7 +16,6 @@ class ReservationTimeCreateServiceTest { @Autowired private ReservationTimeCreateService reservationTimeCreateService; - @Test @DisplayName("존재하지 않는 예약 시간인 경우 성공한다") void checkDuplicateTime_Success() { From 2420b1009a2804ac3d6e4924d917bfffc2fc0ce3 Mon Sep 17 00:00:00 2001 From: Minjoo Date: Thu, 16 May 2024 11:06:49 +0900 Subject: [PATCH 14/42] =?UTF-8?q?refactor:=20SpringBootTest=20random=20por?= =?UTF-8?q?t=20=EC=82=AC=EC=9A=A9=20=EB=B0=8F=20=EC=B6=94=EC=83=81=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=EB=A1=9C=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AdminReservationCreateService.java | 2 +- .../controller/BaseControllerTest.java | 21 +++++++++++++++++++ .../controller/api/AuthApiControllerTest.java | 11 ++++++++-- .../api/MemberApiControllerTest.java | 11 ++++++++-- .../api/ReservationApiControllerTest.java | 12 ++++++++--- .../api/ReservationTimeApiControllerTest.java | 12 ++++++++--- .../api/ThemeApiControllerTest.java | 12 ++++++++--- .../controller/web/AdminControllerTest.java | 11 ++++++++-- .../roomescape/service/BaseServiceTest.java | 10 +++++++++ .../ReservationCreateServiceTest.java | 4 ++-- .../ReservationTimeCreateServiceTest.java | 4 ++-- .../ReservationTimeDeleteServiceTest.java | 5 ++--- .../ReservationTimeFindServiceTest.java | 5 ++--- .../service/theme/ThemeDeleteServiceTest.java | 5 ++--- 14 files changed, 96 insertions(+), 29 deletions(-) create mode 100644 src/test/java/roomescape/controller/BaseControllerTest.java create mode 100644 src/test/java/roomescape/service/BaseServiceTest.java diff --git a/src/main/java/roomescape/service/reservation/AdminReservationCreateService.java b/src/main/java/roomescape/service/reservation/AdminReservationCreateService.java index 835b3e72a..8d4c66bd5 100644 --- a/src/main/java/roomescape/service/reservation/AdminReservationCreateService.java +++ b/src/main/java/roomescape/service/reservation/AdminReservationCreateService.java @@ -11,8 +11,8 @@ @Service public class AdminReservationCreateService { - private final ReservationRepository reservationRepository; private final ReservationCreateValidator reservationCreateValidator; + private final ReservationRepository reservationRepository; public AdminReservationCreateService(ReservationRepository reservationRepository, ReservationCreateValidator reservationCreateValidator) { this.reservationRepository = reservationRepository; diff --git a/src/test/java/roomescape/controller/BaseControllerTest.java b/src/test/java/roomescape/controller/BaseControllerTest.java new file mode 100644 index 000000000..6921e5594 --- /dev/null +++ b/src/test/java/roomescape/controller/BaseControllerTest.java @@ -0,0 +1,21 @@ +package roomescape.controller; + +import io.restassured.RestAssured; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.annotation.DirtiesContext; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +public abstract class BaseControllerTest { + + @LocalServerPort + private int port; + + @BeforeEach + protected void setUp() { + RestAssured.port = port; + } +} diff --git a/src/test/java/roomescape/controller/api/AuthApiControllerTest.java b/src/test/java/roomescape/controller/api/AuthApiControllerTest.java index 15e8bd80a..d0bf8c4c8 100644 --- a/src/test/java/roomescape/controller/api/AuthApiControllerTest.java +++ b/src/test/java/roomescape/controller/api/AuthApiControllerTest.java @@ -2,16 +2,23 @@ import io.restassured.RestAssured; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; +import roomescape.controller.BaseControllerTest; import roomescape.service.dto.request.LoginRequest; import roomescape.service.dto.response.MemberIdAndNameResponse; import roomescape.util.TokenGenerator; -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) -class AuthApiControllerTest { +class AuthApiControllerTest extends BaseControllerTest { + + @Override + @BeforeEach + public void setUp() { + super.setUp(); + } @Test @DisplayName("올바른 로그인 정보를 입력할 시, 로그인에 성공한다.") diff --git a/src/test/java/roomescape/controller/api/MemberApiControllerTest.java b/src/test/java/roomescape/controller/api/MemberApiControllerTest.java index e6f80b88d..7153dee65 100644 --- a/src/test/java/roomescape/controller/api/MemberApiControllerTest.java +++ b/src/test/java/roomescape/controller/api/MemberApiControllerTest.java @@ -3,13 +3,20 @@ import static org.hamcrest.Matchers.is; import io.restassured.RestAssured; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import roomescape.controller.BaseControllerTest; import roomescape.util.TokenGenerator; -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) -class MemberApiControllerTest { +class MemberApiControllerTest extends BaseControllerTest { + + @Override + @BeforeEach + public void setUp() { + super.setUp(); + } @Test @DisplayName("유저 목록 조회 요청이 정상적으로 수행된다.") diff --git a/src/test/java/roomescape/controller/api/ReservationApiControllerTest.java b/src/test/java/roomescape/controller/api/ReservationApiControllerTest.java index b88091780..f3e3964ad 100644 --- a/src/test/java/roomescape/controller/api/ReservationApiControllerTest.java +++ b/src/test/java/roomescape/controller/api/ReservationApiControllerTest.java @@ -8,21 +8,27 @@ import java.lang.reflect.Field; import java.time.LocalDate; import java.util.Map; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.annotation.DirtiesContext; +import roomescape.controller.BaseControllerTest; import roomescape.util.TokenGenerator; -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) -@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) -public class ReservationApiControllerTest { +public class ReservationApiControllerTest extends BaseControllerTest { @Autowired private ReservationApiController reservationApiController; + @Override + @BeforeEach + public void setUp() { + super.setUp(); + } + @Test @DisplayName("관리자 예약 페이지 요청이 정상적으로 수행된다.") void moveToReservationPage_Success() { diff --git a/src/test/java/roomescape/controller/api/ReservationTimeApiControllerTest.java b/src/test/java/roomescape/controller/api/ReservationTimeApiControllerTest.java index d118fb944..a381bac39 100644 --- a/src/test/java/roomescape/controller/api/ReservationTimeApiControllerTest.java +++ b/src/test/java/roomescape/controller/api/ReservationTimeApiControllerTest.java @@ -5,14 +5,20 @@ import io.restassured.RestAssured; import io.restassured.http.ContentType; import java.util.Map; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.DirtiesContext; +import roomescape.controller.BaseControllerTest; -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) -@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) -public class ReservationTimeApiControllerTest { +public class ReservationTimeApiControllerTest extends BaseControllerTest { + + @Override + @BeforeEach + public void setUp() { + super.setUp(); + } @Test @DisplayName("예약 시간 추가, 조회, 삭제를 정상적으로 수행한다.") diff --git a/src/test/java/roomescape/controller/api/ThemeApiControllerTest.java b/src/test/java/roomescape/controller/api/ThemeApiControllerTest.java index e922ac475..f0009ae66 100644 --- a/src/test/java/roomescape/controller/api/ThemeApiControllerTest.java +++ b/src/test/java/roomescape/controller/api/ThemeApiControllerTest.java @@ -5,14 +5,20 @@ import io.restassured.RestAssured; import io.restassured.http.ContentType; import java.util.Map; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.DirtiesContext; +import roomescape.controller.BaseControllerTest; -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) -@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) -public class ThemeApiControllerTest { +public class ThemeApiControllerTest extends BaseControllerTest { + + @Override + @BeforeEach + public void setUp() { + super.setUp(); + } @Test @DisplayName("테마 조회를 정상적으로 수행한다.") diff --git a/src/test/java/roomescape/controller/web/AdminControllerTest.java b/src/test/java/roomescape/controller/web/AdminControllerTest.java index d589ef127..d5c8a9b08 100644 --- a/src/test/java/roomescape/controller/web/AdminControllerTest.java +++ b/src/test/java/roomescape/controller/web/AdminControllerTest.java @@ -1,14 +1,21 @@ package roomescape.controller.web; import io.restassured.RestAssured; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; +import roomescape.controller.BaseControllerTest; import roomescape.service.dto.request.LoginRequest; -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) -class AdminControllerTest { +class AdminControllerTest extends BaseControllerTest { + + @Override + @BeforeEach + public void setUp() { + super.setUp(); + } @Test @DisplayName("어드민 메인 페이지로 정상적으로 이동한다.") diff --git a/src/test/java/roomescape/service/BaseServiceTest.java b/src/test/java/roomescape/service/BaseServiceTest.java new file mode 100644 index 000000000..7a9ee5f41 --- /dev/null +++ b/src/test/java/roomescape/service/BaseServiceTest.java @@ -0,0 +1,10 @@ +package roomescape.service; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.annotation.DirtiesContext; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +public abstract class BaseServiceTest { +} diff --git a/src/test/java/roomescape/service/reservation/ReservationCreateServiceTest.java b/src/test/java/roomescape/service/reservation/ReservationCreateServiceTest.java index 6398da40b..5a161cac6 100644 --- a/src/test/java/roomescape/service/reservation/ReservationCreateServiceTest.java +++ b/src/test/java/roomescape/service/reservation/ReservationCreateServiceTest.java @@ -10,10 +10,10 @@ import org.springframework.boot.test.context.SpringBootTest; import roomescape.domain.Member; import roomescape.domain.Role; +import roomescape.service.BaseServiceTest; import roomescape.service.dto.request.ReservationSaveRequest; -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) -class ReservationCreateServiceTest { +class ReservationCreateServiceTest extends BaseServiceTest { @Autowired private ReservationCreateService reservationCreateService; diff --git a/src/test/java/roomescape/service/reservationtime/ReservationTimeCreateServiceTest.java b/src/test/java/roomescape/service/reservationtime/ReservationTimeCreateServiceTest.java index 7e9c3a44b..d3d663573 100644 --- a/src/test/java/roomescape/service/reservationtime/ReservationTimeCreateServiceTest.java +++ b/src/test/java/roomescape/service/reservationtime/ReservationTimeCreateServiceTest.java @@ -8,10 +8,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import roomescape.service.BaseServiceTest; import roomescape.service.dto.request.ReservationTimeSaveRequest; -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) -class ReservationTimeCreateServiceTest { +class ReservationTimeCreateServiceTest extends BaseServiceTest { @Autowired private ReservationTimeCreateService reservationTimeCreateService; diff --git a/src/test/java/roomescape/service/reservationtime/ReservationTimeDeleteServiceTest.java b/src/test/java/roomescape/service/reservationtime/ReservationTimeDeleteServiceTest.java index beec20140..6b6497f07 100644 --- a/src/test/java/roomescape/service/reservationtime/ReservationTimeDeleteServiceTest.java +++ b/src/test/java/roomescape/service/reservationtime/ReservationTimeDeleteServiceTest.java @@ -8,10 +8,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.DirtiesContext; +import roomescape.service.BaseServiceTest; -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) -@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) -class ReservationTimeDeleteServiceTest { +class ReservationTimeDeleteServiceTest extends BaseServiceTest { @Autowired private ReservationTimeDeleteService reservationTimeDeleteService; diff --git a/src/test/java/roomescape/service/reservationtime/ReservationTimeFindServiceTest.java b/src/test/java/roomescape/service/reservationtime/ReservationTimeFindServiceTest.java index 794b8604d..fc636ef47 100644 --- a/src/test/java/roomescape/service/reservationtime/ReservationTimeFindServiceTest.java +++ b/src/test/java/roomescape/service/reservationtime/ReservationTimeFindServiceTest.java @@ -12,10 +12,9 @@ import org.springframework.test.annotation.DirtiesContext; import roomescape.domain.ReservationStatus; import roomescape.domain.ReservationTime; +import roomescape.service.BaseServiceTest; -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) -@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) -class ReservationTimeFindServiceTest { +class ReservationTimeFindServiceTest extends BaseServiceTest { @Autowired private ReservationTimeFindService reservationTimeFindService; diff --git a/src/test/java/roomescape/service/theme/ThemeDeleteServiceTest.java b/src/test/java/roomescape/service/theme/ThemeDeleteServiceTest.java index 629594f3e..f965afac3 100644 --- a/src/test/java/roomescape/service/theme/ThemeDeleteServiceTest.java +++ b/src/test/java/roomescape/service/theme/ThemeDeleteServiceTest.java @@ -8,10 +8,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.DirtiesContext; +import roomescape.service.BaseServiceTest; -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) -@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) -class ThemeDeleteServiceTest { +class ThemeDeleteServiceTest extends BaseServiceTest { @Autowired private ThemeDeleteService themeDeleteService; From a0672677cd8ab0c67a2d6f910cf7b036eaadbe4f Mon Sep 17 00:00:00 2001 From: Minjoo Date: Thu, 16 May 2024 11:10:27 +0900 Subject: [PATCH 15/42] =?UTF-8?q?refactor:=20=EB=B3=80=EC=88=98=EB=AA=85?= =?UTF-8?q?=20=EB=AA=85=ED=99=95=ED=95=98=EA=B2=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roomescape/controller/api/ReservationApiController.java | 6 +++--- .../controller/api/ReservationTimeApiController.java | 6 +++--- .../java/roomescape/controller/api/ThemeApiController.java | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/java/roomescape/controller/api/ReservationApiController.java b/src/main/java/roomescape/controller/api/ReservationApiController.java index 32eee9703..6a775d962 100644 --- a/src/main/java/roomescape/controller/api/ReservationApiController.java +++ b/src/main/java/roomescape/controller/api/ReservationApiController.java @@ -94,10 +94,10 @@ public ResponseEntity addReservationByAdmin(@RequestBody @V .body(new ReservationResponse(newReservation)); } - @DeleteMapping("/reservations/{id}") + @DeleteMapping("/reservations/{reservationId}") public ResponseEntity deleteReservation(@PathVariable - @Positive(message = "1 이상의 값만 입력해주세요.") long id) { - reservationDeleteService.deleteReservation(id); + @Positive(message = "1 이상의 값만 입력해주세요.") long reservationId) { + reservationDeleteService.deleteReservation(reservationId); return ResponseEntity.noContent().build(); } } diff --git a/src/main/java/roomescape/controller/api/ReservationTimeApiController.java b/src/main/java/roomescape/controller/api/ReservationTimeApiController.java index b14d9a481..73e3320af 100644 --- a/src/main/java/roomescape/controller/api/ReservationTimeApiController.java +++ b/src/main/java/roomescape/controller/api/ReservationTimeApiController.java @@ -72,10 +72,10 @@ public ResponseEntity addReservationTime( .body(new ReservationTimeResponse(reservationTime)); } - @DeleteMapping("/times/{id}") + @DeleteMapping("/times/{timeId}") public ResponseEntity deleteReservationTime(@PathVariable - @Positive(message = "1 이상의 값만 입력해주세요.") long id) { - reservationTimeDeleteService.deleteReservationTime(id); + @Positive(message = "1 이상의 값만 입력해주세요.") long timeId) { + reservationTimeDeleteService.deleteReservationTime(timeId); return ResponseEntity.noContent().build(); } } diff --git a/src/main/java/roomescape/controller/api/ThemeApiController.java b/src/main/java/roomescape/controller/api/ThemeApiController.java index b61872bb6..b99148293 100644 --- a/src/main/java/roomescape/controller/api/ThemeApiController.java +++ b/src/main/java/roomescape/controller/api/ThemeApiController.java @@ -62,10 +62,10 @@ public ResponseEntity addTheme(@RequestBody @Valid ThemeSaveReque .body(new ThemeResponse(theme)); } - @DeleteMapping("/themes/{id}") + @DeleteMapping("/themes/{themeId}") public ResponseEntity deleteTheme(@PathVariable - @Positive(message = "1 이상의 값만 입력해주세요.") long id) { - themeDeleteService.deleteTheme(id); + @Positive(message = "1 이상의 값만 입력해주세요.") long themeId) { + themeDeleteService.deleteTheme(themeId); return ResponseEntity.noContent().build(); } } From f038f017988d8adcebc560eb3bd2c655e275baa2 Mon Sep 17 00:00:00 2001 From: Minjoo Date: Thu, 16 May 2024 12:14:54 +0900 Subject: [PATCH 16/42] =?UTF-8?q?refactor:=20Member=20=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=20List=EA=B0=80=20=EC=95=84=EB=8B=8C=20Object=20=ED=98=95?= =?UTF-8?q?=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EC=9D=91=EB=8B=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/api/AuthApiController.java | 2 +- .../controller/api/MemberApiController.java | 10 +++------- .../service/dto/response/ReservationResponse.java | 1 + .../{ => member}/MemberIdAndNameResponse.java | 2 +- .../response/member/MemberIdAndNameResponses.java | 14 ++++++++++++++ .../resources/static/js/reservation-with-member.js | 2 +- .../controller/api/AuthApiControllerTest.java | 3 +-- .../controller/api/MemberApiControllerTest.java | 2 +- 8 files changed, 23 insertions(+), 13 deletions(-) rename src/main/java/roomescape/service/dto/response/{ => member}/MemberIdAndNameResponse.java (57%) create mode 100644 src/main/java/roomescape/service/dto/response/member/MemberIdAndNameResponses.java diff --git a/src/main/java/roomescape/controller/api/AuthApiController.java b/src/main/java/roomescape/controller/api/AuthApiController.java index 1372a9b69..bbebb7777 100644 --- a/src/main/java/roomescape/controller/api/AuthApiController.java +++ b/src/main/java/roomescape/controller/api/AuthApiController.java @@ -12,7 +12,7 @@ import roomescape.domain.Member; import roomescape.service.auth.AuthService; import roomescape.service.dto.request.LoginRequest; -import roomescape.service.dto.response.MemberIdAndNameResponse; +import roomescape.service.dto.response.member.MemberIdAndNameResponse; @RestController public class AuthApiController { diff --git a/src/main/java/roomescape/controller/api/MemberApiController.java b/src/main/java/roomescape/controller/api/MemberApiController.java index aa9dbdce0..1b563e15d 100644 --- a/src/main/java/roomescape/controller/api/MemberApiController.java +++ b/src/main/java/roomescape/controller/api/MemberApiController.java @@ -5,7 +5,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import roomescape.domain.Member; -import roomescape.service.dto.response.MemberIdAndNameResponse; +import roomescape.service.dto.response.member.MemberIdAndNameResponses; import roomescape.service.member.MemberService; @RestController @@ -18,12 +18,8 @@ public MemberApiController(MemberService memberService) { } @GetMapping("/members") - public ResponseEntity> getMembers() { + public ResponseEntity getMembers() { List members = memberService.findMembers(); - return ResponseEntity.ok( - members.stream() - .map(member -> new MemberIdAndNameResponse(member.getId(), member.getName())) - .toList() - ); + return ResponseEntity.ok(MemberIdAndNameResponses.from(members)); } } diff --git a/src/main/java/roomescape/service/dto/response/ReservationResponse.java b/src/main/java/roomescape/service/dto/response/ReservationResponse.java index 75579677b..2c24b5719 100644 --- a/src/main/java/roomescape/service/dto/response/ReservationResponse.java +++ b/src/main/java/roomescape/service/dto/response/ReservationResponse.java @@ -2,6 +2,7 @@ import java.time.LocalDate; import roomescape.domain.Reservation; +import roomescape.service.dto.response.member.MemberIdAndNameResponse; public record ReservationResponse(Long id, MemberIdAndNameResponse member, diff --git a/src/main/java/roomescape/service/dto/response/MemberIdAndNameResponse.java b/src/main/java/roomescape/service/dto/response/member/MemberIdAndNameResponse.java similarity index 57% rename from src/main/java/roomescape/service/dto/response/MemberIdAndNameResponse.java rename to src/main/java/roomescape/service/dto/response/member/MemberIdAndNameResponse.java index a9f3db2e2..c825b9f3c 100644 --- a/src/main/java/roomescape/service/dto/response/MemberIdAndNameResponse.java +++ b/src/main/java/roomescape/service/dto/response/member/MemberIdAndNameResponse.java @@ -1,4 +1,4 @@ -package roomescape.service.dto.response; +package roomescape.service.dto.response.member; public record MemberIdAndNameResponse(Long id, String name) { } diff --git a/src/main/java/roomescape/service/dto/response/member/MemberIdAndNameResponses.java b/src/main/java/roomescape/service/dto/response/member/MemberIdAndNameResponses.java new file mode 100644 index 000000000..02ee30677 --- /dev/null +++ b/src/main/java/roomescape/service/dto/response/member/MemberIdAndNameResponses.java @@ -0,0 +1,14 @@ +package roomescape.service.dto.response.member; + +import java.util.List; +import roomescape.domain.Member; + +public record MemberIdAndNameResponses(List members) { + + public static MemberIdAndNameResponses from(List members) { + List responses = members.stream() + .map(member -> new MemberIdAndNameResponse(member.getId(), member.getName())) + .toList(); + return new MemberIdAndNameResponses(responses); + } +} diff --git a/src/main/resources/static/js/reservation-with-member.js b/src/main/resources/static/js/reservation-with-member.js index b2aa85c6e..bfcc8d20a 100644 --- a/src/main/resources/static/js/reservation-with-member.js +++ b/src/main/resources/static/js/reservation-with-member.js @@ -58,7 +58,7 @@ function fetchThemes() { function fetchMembers() { requestRead(MEMBER_API_ENDPOINT) .then(data => { - membersOptions.push(...data); + membersOptions.push(...data.members); populateSelect('member', membersOptions, 'name'); }) .catch(error => console.error('Error fetching member:', error)); diff --git a/src/test/java/roomescape/controller/api/AuthApiControllerTest.java b/src/test/java/roomescape/controller/api/AuthApiControllerTest.java index d0bf8c4c8..5e95e6253 100644 --- a/src/test/java/roomescape/controller/api/AuthApiControllerTest.java +++ b/src/test/java/roomescape/controller/api/AuthApiControllerTest.java @@ -5,11 +5,10 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import roomescape.controller.BaseControllerTest; import roomescape.service.dto.request.LoginRequest; -import roomescape.service.dto.response.MemberIdAndNameResponse; +import roomescape.service.dto.response.member.MemberIdAndNameResponse; import roomescape.util.TokenGenerator; class AuthApiControllerTest extends BaseControllerTest { diff --git a/src/test/java/roomescape/controller/api/MemberApiControllerTest.java b/src/test/java/roomescape/controller/api/MemberApiControllerTest.java index 7153dee65..52afd50eb 100644 --- a/src/test/java/roomescape/controller/api/MemberApiControllerTest.java +++ b/src/test/java/roomescape/controller/api/MemberApiControllerTest.java @@ -26,6 +26,6 @@ void selectMembers_Success() { .when().get("/members") .then().log().all() .statusCode(200) - .body("size()", is(2)); + .body("members.size()", is(2)); } } From d1a8b3c092b375cc654227321049b35f26e7dd83 Mon Sep 17 00:00:00 2001 From: Minjoo Date: Thu, 16 May 2024 12:24:11 +0900 Subject: [PATCH 17/42] =?UTF-8?q?refactor:=20Reservation=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20List=EA=B0=80=20=EC=95=84=EB=8B=8C=20Object=20?= =?UTF-8?q?=ED=98=95=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EC=9D=91=EB=8B=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/ReservationApiController.java | 19 ++++++------------- .../ReservationResponse.java | 4 +++- .../reservation/ReservationResponses.java | 14 ++++++++++++++ .../static/js/reservation-with-member.js | 2 +- .../api/ReservationApiControllerTest.java | 8 ++++---- 5 files changed, 28 insertions(+), 19 deletions(-) rename src/main/java/roomescape/service/dto/response/{ => reservation}/ReservationResponse.java (82%) create mode 100644 src/main/java/roomescape/service/dto/response/reservation/ReservationResponses.java diff --git a/src/main/java/roomescape/controller/api/ReservationApiController.java b/src/main/java/roomescape/controller/api/ReservationApiController.java index 6a775d962..1924bb1db 100644 --- a/src/main/java/roomescape/controller/api/ReservationApiController.java +++ b/src/main/java/roomescape/controller/api/ReservationApiController.java @@ -19,8 +19,9 @@ import roomescape.domain.Reservation; import roomescape.service.dto.request.ReservationAdminSaveRequest; import roomescape.service.dto.request.ReservationSaveRequest; -import roomescape.service.dto.response.ReservationResponse; +import roomescape.service.dto.response.reservation.ReservationResponse; import roomescape.service.dto.response.UserReservationResponse; +import roomescape.service.dto.response.reservation.ReservationResponses; import roomescape.service.reservation.AdminReservationCreateService; import roomescape.service.reservation.ReservationCreateService; import roomescape.service.reservation.ReservationDeleteService; @@ -46,27 +47,19 @@ public ReservationApiController(ReservationCreateService reservationCreateServic } @GetMapping("/reservations") - public ResponseEntity> getReservations() { + public ResponseEntity getReservations() { List reservations = reservationFindService.findReservations(); - return ResponseEntity.ok( - reservations.stream() - .map(ReservationResponse::new) - .toList() - ); + return ResponseEntity.ok(ReservationResponses.from(reservations)); } @GetMapping("/admin/reservations/search") - public ResponseEntity> getSearchingReservations(@RequestParam long memberId, + public ResponseEntity getSearchingReservations(@RequestParam long memberId, @RequestParam long themeId, @RequestParam LocalDate dateFrom, @RequestParam LocalDate dateTo ) { List reservations = reservationFindService.searchReservations(memberId, themeId, dateFrom, dateTo); - return ResponseEntity.ok( - reservations.stream() - .map(ReservationResponse::new) - .toList() - ); + return ResponseEntity.ok(ReservationResponses.from(reservations)); } @GetMapping("/reservations-mine") diff --git a/src/main/java/roomescape/service/dto/response/ReservationResponse.java b/src/main/java/roomescape/service/dto/response/reservation/ReservationResponse.java similarity index 82% rename from src/main/java/roomescape/service/dto/response/ReservationResponse.java rename to src/main/java/roomescape/service/dto/response/reservation/ReservationResponse.java index 2c24b5719..e0637618e 100644 --- a/src/main/java/roomescape/service/dto/response/ReservationResponse.java +++ b/src/main/java/roomescape/service/dto/response/reservation/ReservationResponse.java @@ -1,7 +1,9 @@ -package roomescape.service.dto.response; +package roomescape.service.dto.response.reservation; import java.time.LocalDate; import roomescape.domain.Reservation; +import roomescape.service.dto.response.ReservationTimeResponse; +import roomescape.service.dto.response.ThemeResponse; import roomescape.service.dto.response.member.MemberIdAndNameResponse; public record ReservationResponse(Long id, diff --git a/src/main/java/roomescape/service/dto/response/reservation/ReservationResponses.java b/src/main/java/roomescape/service/dto/response/reservation/ReservationResponses.java new file mode 100644 index 000000000..2a469e401 --- /dev/null +++ b/src/main/java/roomescape/service/dto/response/reservation/ReservationResponses.java @@ -0,0 +1,14 @@ +package roomescape.service.dto.response.reservation; + +import java.util.List; +import roomescape.domain.Reservation; + +public record ReservationResponses(List reservations) { + + public static ReservationResponses from(List reservations) { + List responses = reservations.stream() + .map(ReservationResponse::new) + .toList(); + return new ReservationResponses(responses); + } +} diff --git a/src/main/resources/static/js/reservation-with-member.js b/src/main/resources/static/js/reservation-with-member.js index bfcc8d20a..9f80e161c 100644 --- a/src/main/resources/static/js/reservation-with-member.js +++ b/src/main/resources/static/js/reservation-with-member.js @@ -24,7 +24,7 @@ function render(data) { const tableBody = document.getElementById('table-body'); tableBody.innerHTML = ''; - data.forEach(item => { + data.reservations.forEach(item => { const row = tableBody.insertRow(); row.insertCell(0).textContent = item.id; // 예약 id diff --git a/src/test/java/roomescape/controller/api/ReservationApiControllerTest.java b/src/test/java/roomescape/controller/api/ReservationApiControllerTest.java index f3e3964ad..22dc21c10 100644 --- a/src/test/java/roomescape/controller/api/ReservationApiControllerTest.java +++ b/src/test/java/roomescape/controller/api/ReservationApiControllerTest.java @@ -49,13 +49,13 @@ void moveToReservationPage_Failure() { } @Test - @DisplayName("예약 목록 조회 요청이 정상석으로 수행된다.") + @DisplayName("예약 목록 조회 요청이 정상적으로 수행된다.") void selectReservationListRequest_Success() { RestAssured.given().log().all() .when().get("/reservations") .then().log().all() .statusCode(200) - .body("size()", is(1)); + .body("reservations.size()", is(1)); } @Test @@ -90,7 +90,7 @@ void ReservationTime_CREATE_READ_Success() { .when().get("/reservations") .then().log().all() .statusCode(200) - .body("size()", is(2)); + .body("reservations.size()", is(2)); } @Test @@ -105,7 +105,7 @@ void deleteReservation_InDatabase_Success() { .when().get("/reservations") .then().log().all() .statusCode(200) - .body("size()", is(0)); + .body("reservations.size()", is(0)); } @Test From 9baf5b7a4a42283035186e1a4bafac69b2c4e0a5 Mon Sep 17 00:00:00 2001 From: Minjoo Date: Thu, 16 May 2024 12:28:17 +0900 Subject: [PATCH 18/42] =?UTF-8?q?refactor:=20user=20Reservation=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20List=EA=B0=80=20=EC=95=84=EB=8B=8C=20Objec?= =?UTF-8?q?t=20=ED=98=95=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EC=9D=91=EB=8B=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/api/ReservationApiController.java | 10 +++------- .../{ => reservation}/UserReservationResponse.java | 2 +- .../reservation/UserReservationResponses.java | 14 ++++++++++++++ src/main/resources/static/js/reservation-mine.js | 2 +- .../api/ReservationApiControllerTest.java | 2 +- 5 files changed, 20 insertions(+), 10 deletions(-) rename src/main/java/roomescape/service/dto/response/{ => reservation}/UserReservationResponse.java (93%) create mode 100644 src/main/java/roomescape/service/dto/response/reservation/UserReservationResponses.java diff --git a/src/main/java/roomescape/controller/api/ReservationApiController.java b/src/main/java/roomescape/controller/api/ReservationApiController.java index 1924bb1db..178d89d5e 100644 --- a/src/main/java/roomescape/controller/api/ReservationApiController.java +++ b/src/main/java/roomescape/controller/api/ReservationApiController.java @@ -20,8 +20,8 @@ import roomescape.service.dto.request.ReservationAdminSaveRequest; import roomescape.service.dto.request.ReservationSaveRequest; import roomescape.service.dto.response.reservation.ReservationResponse; -import roomescape.service.dto.response.UserReservationResponse; import roomescape.service.dto.response.reservation.ReservationResponses; +import roomescape.service.dto.response.reservation.UserReservationResponses; import roomescape.service.reservation.AdminReservationCreateService; import roomescape.service.reservation.ReservationCreateService; import roomescape.service.reservation.ReservationDeleteService; @@ -63,13 +63,9 @@ public ResponseEntity getSearchingReservations(@RequestPar } @GetMapping("/reservations-mine") - public ResponseEntity> getUserReservations(@AuthenticatedMember Member member) { + public ResponseEntity getUserReservations(@AuthenticatedMember Member member) { List userReservations = reservationFindService.findUserReservations(member.getId()); - return ResponseEntity.ok( - userReservations.stream() - .map(UserReservationResponse::new) - .toList() - ); + return ResponseEntity.ok(UserReservationResponses.from(userReservations)); } @PostMapping("/reservations") diff --git a/src/main/java/roomescape/service/dto/response/UserReservationResponse.java b/src/main/java/roomescape/service/dto/response/reservation/UserReservationResponse.java similarity index 93% rename from src/main/java/roomescape/service/dto/response/UserReservationResponse.java rename to src/main/java/roomescape/service/dto/response/reservation/UserReservationResponse.java index 137918170..245c105ca 100644 --- a/src/main/java/roomescape/service/dto/response/UserReservationResponse.java +++ b/src/main/java/roomescape/service/dto/response/reservation/UserReservationResponse.java @@ -1,4 +1,4 @@ -package roomescape.service.dto.response; +package roomescape.service.dto.response.reservation; import com.fasterxml.jackson.annotation.JsonFormat; import java.time.LocalDate; diff --git a/src/main/java/roomescape/service/dto/response/reservation/UserReservationResponses.java b/src/main/java/roomescape/service/dto/response/reservation/UserReservationResponses.java new file mode 100644 index 000000000..b997b4386 --- /dev/null +++ b/src/main/java/roomescape/service/dto/response/reservation/UserReservationResponses.java @@ -0,0 +1,14 @@ +package roomescape.service.dto.response.reservation; + +import java.util.List; +import roomescape.domain.Reservation; + +public record UserReservationResponses(List reservations) { + + public static UserReservationResponses from(List reservations) { + List responses = reservations.stream() + .map(UserReservationResponse::new) + .toList(); + return new UserReservationResponses(responses); + } +} diff --git a/src/main/resources/static/js/reservation-mine.js b/src/main/resources/static/js/reservation-mine.js index 96be821db..f998ac7bd 100644 --- a/src/main/resources/static/js/reservation-mine.js +++ b/src/main/resources/static/js/reservation-mine.js @@ -12,7 +12,7 @@ function render(data) { const tableBody = document.getElementById('table-body'); tableBody.innerHTML = ''; - data.forEach(item => { + data.reservations.forEach(item => { const row = tableBody.insertRow(); const theme = item.theme; diff --git a/src/test/java/roomescape/controller/api/ReservationApiControllerTest.java b/src/test/java/roomescape/controller/api/ReservationApiControllerTest.java index 22dc21c10..b13e7c30e 100644 --- a/src/test/java/roomescape/controller/api/ReservationApiControllerTest.java +++ b/src/test/java/roomescape/controller/api/ReservationApiControllerTest.java @@ -66,7 +66,7 @@ void selectUserReservationListRequest_Success() { .when().get("/reservations-mine") .then().log().all() .statusCode(200) - .body("size()", is(1)); + .body("reservations.size()", is(1)); } @Test From a4ed3745a6fbf82af3fc7352e1c6f93a1366de49 Mon Sep 17 00:00:00 2001 From: Minjoo Date: Thu, 16 May 2024 12:37:06 +0900 Subject: [PATCH 19/42] =?UTF-8?q?refactor:=20ReservationTime=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20List=EA=B0=80=20=EC=95=84=EB=8B=8C=20Object=20?= =?UTF-8?q?=ED=98=95=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EC=9D=91=EB=8B=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/ReservationTimeApiController.java | 11 ++++------- .../response/reservation/ReservationResponse.java | 2 +- .../ReservationTimeResponse.java | 2 +- .../reservationTime/ReservationTimeResponses.java | 14 ++++++++++++++ .../resources/static/js/reservation-with-member.js | 2 +- src/main/resources/static/js/time.js | 2 +- .../api/ReservationTimeApiControllerTest.java | 4 +--- 7 files changed, 23 insertions(+), 14 deletions(-) rename src/main/java/roomescape/service/dto/response/{ => reservationTime}/ReservationTimeResponse.java (86%) create mode 100644 src/main/java/roomescape/service/dto/response/reservationTime/ReservationTimeResponses.java diff --git a/src/main/java/roomescape/controller/api/ReservationTimeApiController.java b/src/main/java/roomescape/controller/api/ReservationTimeApiController.java index 73e3320af..cbfb7736d 100644 --- a/src/main/java/roomescape/controller/api/ReservationTimeApiController.java +++ b/src/main/java/roomescape/controller/api/ReservationTimeApiController.java @@ -18,7 +18,8 @@ import roomescape.domain.ReservationTime; import roomescape.service.dto.request.ReservationTimeSaveRequest; import roomescape.service.dto.response.ReservationStatusResponse; -import roomescape.service.dto.response.ReservationTimeResponse; +import roomescape.service.dto.response.reservationTime.ReservationTimeResponse; +import roomescape.service.dto.response.reservationTime.ReservationTimeResponses; import roomescape.service.reservationtime.ReservationTimeCreateService; import roomescape.service.reservationtime.ReservationTimeDeleteService; import roomescape.service.reservationtime.ReservationTimeFindService; @@ -40,13 +41,9 @@ public ReservationTimeApiController(ReservationTimeCreateService reservationTime } @GetMapping("/times") - public ResponseEntity> getReservationTimes() { + public ResponseEntity getReservationTimes() { List reservationTimes = reservationTimeFindService.findReservationTimes(); - return ResponseEntity.ok( - reservationTimes.stream() - .map(ReservationTimeResponse::new) - .toList() - ); + return ResponseEntity.ok(ReservationTimeResponses.from(reservationTimes)); } @GetMapping("/times/available") diff --git a/src/main/java/roomescape/service/dto/response/reservation/ReservationResponse.java b/src/main/java/roomescape/service/dto/response/reservation/ReservationResponse.java index e0637618e..e5aaf21e3 100644 --- a/src/main/java/roomescape/service/dto/response/reservation/ReservationResponse.java +++ b/src/main/java/roomescape/service/dto/response/reservation/ReservationResponse.java @@ -2,7 +2,7 @@ import java.time.LocalDate; import roomescape.domain.Reservation; -import roomescape.service.dto.response.ReservationTimeResponse; +import roomescape.service.dto.response.reservationTime.ReservationTimeResponse; import roomescape.service.dto.response.ThemeResponse; import roomescape.service.dto.response.member.MemberIdAndNameResponse; diff --git a/src/main/java/roomescape/service/dto/response/ReservationTimeResponse.java b/src/main/java/roomescape/service/dto/response/reservationTime/ReservationTimeResponse.java similarity index 86% rename from src/main/java/roomescape/service/dto/response/ReservationTimeResponse.java rename to src/main/java/roomescape/service/dto/response/reservationTime/ReservationTimeResponse.java index 12c773703..63df79904 100644 --- a/src/main/java/roomescape/service/dto/response/ReservationTimeResponse.java +++ b/src/main/java/roomescape/service/dto/response/reservationTime/ReservationTimeResponse.java @@ -1,4 +1,4 @@ -package roomescape.service.dto.response; +package roomescape.service.dto.response.reservationTime; import com.fasterxml.jackson.annotation.JsonFormat; import java.time.LocalTime; diff --git a/src/main/java/roomescape/service/dto/response/reservationTime/ReservationTimeResponses.java b/src/main/java/roomescape/service/dto/response/reservationTime/ReservationTimeResponses.java new file mode 100644 index 000000000..22cd1a790 --- /dev/null +++ b/src/main/java/roomescape/service/dto/response/reservationTime/ReservationTimeResponses.java @@ -0,0 +1,14 @@ +package roomescape.service.dto.response.reservationTime; + +import java.util.List; +import roomescape.domain.ReservationTime; + +public record ReservationTimeResponses(List reservationTimes) { + + public static ReservationTimeResponses from(List reservationTimes) { + List responses = reservationTimes.stream() + .map(ReservationTimeResponse::new) + .toList(); + return new ReservationTimeResponses(responses); + } +} diff --git a/src/main/resources/static/js/reservation-with-member.js b/src/main/resources/static/js/reservation-with-member.js index 9f80e161c..398945325 100644 --- a/src/main/resources/static/js/reservation-with-member.js +++ b/src/main/resources/static/js/reservation-with-member.js @@ -41,7 +41,7 @@ function render(data) { function fetchTimes() { requestRead(TIME_API_ENDPOINT) .then(data => { - timesOptions.push(...data); + timesOptions.push(...data.reservationTimes); }) .catch(error => console.error('Error fetching time:', error)); } diff --git a/src/main/resources/static/js/time.js b/src/main/resources/static/js/time.js index 641023a6d..f459000fd 100644 --- a/src/main/resources/static/js/time.js +++ b/src/main/resources/static/js/time.js @@ -20,7 +20,7 @@ function render(data) { const tableBody = document.getElementById('table-body'); tableBody.innerHTML = ''; - data.forEach(item => { + data.reservationTimes.forEach(item => { const row = tableBody.insertRow(); cellFields.forEach((field, index) => { diff --git a/src/test/java/roomescape/controller/api/ReservationTimeApiControllerTest.java b/src/test/java/roomescape/controller/api/ReservationTimeApiControllerTest.java index a381bac39..ebb75434b 100644 --- a/src/test/java/roomescape/controller/api/ReservationTimeApiControllerTest.java +++ b/src/test/java/roomescape/controller/api/ReservationTimeApiControllerTest.java @@ -8,8 +8,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.annotation.DirtiesContext; import roomescape.controller.BaseControllerTest; public class ReservationTimeApiControllerTest extends BaseControllerTest { @@ -38,7 +36,7 @@ void ReservationTime_CREATE_READ_DELETE_Success() { .when().get("/times") .then().log().all() .statusCode(200) - .body("size()", is(3)); + .body("reservationTimes.size()", is(3)); RestAssured.given().log().all() .when().delete("/times/3") From b18ba8924824e6ccea1a0ed4aceb80f7dfcc15b8 Mon Sep 17 00:00:00 2001 From: Minjoo Date: Thu, 16 May 2024 13:10:58 +0900 Subject: [PATCH 20/42] =?UTF-8?q?refactor:=20ReservationStatus=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20List=EA=B0=80=20=EC=95=84=EB=8B=8C=20Object=20?= =?UTF-8?q?=ED=98=95=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EC=9D=91=EB=8B=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/ReservationTimeApiController.java | 13 +++---------- .../ReservationStatusResponse.java | 2 +- .../ReservationStatusResponses.java | 18 ++++++++++++++++++ .../resources/static/js/user-reservation.js | 2 +- 4 files changed, 23 insertions(+), 12 deletions(-) rename src/main/java/roomescape/service/dto/response/{ => reservationTime}/ReservationStatusResponse.java (86%) create mode 100644 src/main/java/roomescape/service/dto/response/reservationTime/ReservationStatusResponses.java diff --git a/src/main/java/roomescape/controller/api/ReservationTimeApiController.java b/src/main/java/roomescape/controller/api/ReservationTimeApiController.java index cbfb7736d..bbdee0aae 100644 --- a/src/main/java/roomescape/controller/api/ReservationTimeApiController.java +++ b/src/main/java/roomescape/controller/api/ReservationTimeApiController.java @@ -17,7 +17,7 @@ import roomescape.domain.ReservationStatus; import roomescape.domain.ReservationTime; import roomescape.service.dto.request.ReservationTimeSaveRequest; -import roomescape.service.dto.response.ReservationStatusResponse; +import roomescape.service.dto.response.reservationTime.ReservationStatusResponses; import roomescape.service.dto.response.reservationTime.ReservationTimeResponse; import roomescape.service.dto.response.reservationTime.ReservationTimeResponses; import roomescape.service.reservationtime.ReservationTimeCreateService; @@ -47,18 +47,11 @@ public ResponseEntity getReservationTimes() { } @GetMapping("/times/available") - public ResponseEntity> getReservationTimesIsBooked( + public ResponseEntity getReservationTimesIsBooked( @RequestParam LocalDate date, @RequestParam @Positive(message = "1 이상의 값만 입력해주세요.") long themeId) { ReservationStatus reservationStatus = reservationTimeFindService.findIsBooked(date, themeId); - return ResponseEntity.ok( - reservationStatus.getReservationStatus() - .keySet() - .stream() - .map(reservationTime -> new ReservationStatusResponse( - reservationTime, - reservationStatus.findReservationStatusBy(reservationTime)) - ).toList()); + return ResponseEntity.ok(ReservationStatusResponses.from(reservationStatus)); } @PostMapping("/times") diff --git a/src/main/java/roomescape/service/dto/response/ReservationStatusResponse.java b/src/main/java/roomescape/service/dto/response/reservationTime/ReservationStatusResponse.java similarity index 86% rename from src/main/java/roomescape/service/dto/response/ReservationStatusResponse.java rename to src/main/java/roomescape/service/dto/response/reservationTime/ReservationStatusResponse.java index ea27957db..b1d5c863f 100644 --- a/src/main/java/roomescape/service/dto/response/ReservationStatusResponse.java +++ b/src/main/java/roomescape/service/dto/response/reservationTime/ReservationStatusResponse.java @@ -1,4 +1,4 @@ -package roomescape.service.dto.response; +package roomescape.service.dto.response.reservationTime; import java.time.LocalTime; import roomescape.domain.ReservationTime; diff --git a/src/main/java/roomescape/service/dto/response/reservationTime/ReservationStatusResponses.java b/src/main/java/roomescape/service/dto/response/reservationTime/ReservationStatusResponses.java new file mode 100644 index 000000000..dbf6010fc --- /dev/null +++ b/src/main/java/roomescape/service/dto/response/reservationTime/ReservationStatusResponses.java @@ -0,0 +1,18 @@ +package roomescape.service.dto.response.reservationTime; + +import java.util.List; +import roomescape.domain.ReservationStatus; + +public record ReservationStatusResponses(List reservationStatuses) { + + public static ReservationStatusResponses from(ReservationStatus reservationStatus) { + List responses = reservationStatus.getReservationStatus() + .keySet() + .stream() + .map(reservationTime -> new ReservationStatusResponse( + reservationTime, + reservationStatus.findReservationStatusBy(reservationTime)) + ).toList(); + return new ReservationStatusResponses(responses); + } +} diff --git a/src/main/resources/static/js/user-reservation.js b/src/main/resources/static/js/user-reservation.js index 1c324a2b8..403abdffb 100644 --- a/src/main/resources/static/js/user-reservation.js +++ b/src/main/resources/static/js/user-reservation.js @@ -106,7 +106,7 @@ function renderAvailableTimes(times) { timeSlots.innerHTML = '
선택할 수 있는 시간이 없습니다.
'; return; } - times.forEach(time => { + times.reservationStatuses.forEach(time => { const startAt = time.startAt; const timeId = time.timeId; const alreadyBooked = time.alreadyBooked; From 476c202907ca852b989fec7057f03692a9c3e780 Mon Sep 17 00:00:00 2001 From: Minjoo Date: Thu, 16 May 2024 13:22:51 +0900 Subject: [PATCH 21/42] =?UTF-8?q?refactor:=20Theme=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?List=EA=B0=80=20=EC=95=84=EB=8B=8C=20Object=20=ED=98=95?= =?UTF-8?q?=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EC=9D=91=EB=8B=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/api/ThemeApiController.java | 19 ++++++------------- .../reservation/ReservationResponse.java | 2 +- .../response/{ => theme}/ThemeResponse.java | 2 +- .../dto/response/theme/ThemeResponses.java | 14 ++++++++++++++ .../static/js/reservation-with-member.js | 2 +- src/main/resources/static/js/theme.js | 2 +- .../resources/static/js/user-reservation.js | 2 +- .../api/ThemeApiControllerTest.java | 6 +++--- 8 files changed, 28 insertions(+), 21 deletions(-) rename src/main/java/roomescape/service/dto/response/{ => theme}/ThemeResponse.java (84%) create mode 100644 src/main/java/roomescape/service/dto/response/theme/ThemeResponses.java diff --git a/src/main/java/roomescape/controller/api/ThemeApiController.java b/src/main/java/roomescape/controller/api/ThemeApiController.java index b99148293..5b58a314d 100644 --- a/src/main/java/roomescape/controller/api/ThemeApiController.java +++ b/src/main/java/roomescape/controller/api/ThemeApiController.java @@ -14,7 +14,8 @@ import org.springframework.web.bind.annotation.RestController; import roomescape.domain.Theme; import roomescape.service.dto.request.ThemeSaveRequest; -import roomescape.service.dto.response.ThemeResponse; +import roomescape.service.dto.response.theme.ThemeResponse; +import roomescape.service.dto.response.theme.ThemeResponses; import roomescape.service.theme.ThemeCreateService; import roomescape.service.theme.ThemeDeleteService; import roomescape.service.theme.ThemeFindService; @@ -36,23 +37,15 @@ public ThemeApiController(ThemeCreateService themeCreateService, } @GetMapping("/themes") - public ResponseEntity> getThemes() { + public ResponseEntity getThemes() { List themes = themeFindService.findThemes(); - return ResponseEntity.ok( - themes.stream() - .map(ThemeResponse::new) - .toList() - ); + return ResponseEntity.ok(ThemeResponses.from(themes)); } @GetMapping("/themes/ranks") - public ResponseEntity> getThemeRanks() { + public ResponseEntity getThemeRanks() { List themes = themeFindService.findThemeRanks(); - return ResponseEntity.ok( - themes.stream() - .map(ThemeResponse::new) - .toList() - ); + return ResponseEntity.ok(ThemeResponses.from(themes)); } @PostMapping("/themes") diff --git a/src/main/java/roomescape/service/dto/response/reservation/ReservationResponse.java b/src/main/java/roomescape/service/dto/response/reservation/ReservationResponse.java index e5aaf21e3..c4a4b0709 100644 --- a/src/main/java/roomescape/service/dto/response/reservation/ReservationResponse.java +++ b/src/main/java/roomescape/service/dto/response/reservation/ReservationResponse.java @@ -3,7 +3,7 @@ import java.time.LocalDate; import roomescape.domain.Reservation; import roomescape.service.dto.response.reservationTime.ReservationTimeResponse; -import roomescape.service.dto.response.ThemeResponse; +import roomescape.service.dto.response.theme.ThemeResponse; import roomescape.service.dto.response.member.MemberIdAndNameResponse; public record ReservationResponse(Long id, diff --git a/src/main/java/roomescape/service/dto/response/ThemeResponse.java b/src/main/java/roomescape/service/dto/response/theme/ThemeResponse.java similarity index 84% rename from src/main/java/roomescape/service/dto/response/ThemeResponse.java rename to src/main/java/roomescape/service/dto/response/theme/ThemeResponse.java index 35a7c2d9c..bc9ee9218 100644 --- a/src/main/java/roomescape/service/dto/response/ThemeResponse.java +++ b/src/main/java/roomescape/service/dto/response/theme/ThemeResponse.java @@ -1,4 +1,4 @@ -package roomescape.service.dto.response; +package roomescape.service.dto.response.theme; import roomescape.domain.Theme; diff --git a/src/main/java/roomescape/service/dto/response/theme/ThemeResponses.java b/src/main/java/roomescape/service/dto/response/theme/ThemeResponses.java new file mode 100644 index 000000000..7b2091149 --- /dev/null +++ b/src/main/java/roomescape/service/dto/response/theme/ThemeResponses.java @@ -0,0 +1,14 @@ +package roomescape.service.dto.response.theme; + +import java.util.List; +import roomescape.domain.Theme; + +public record ThemeResponses(List themes) { + + public static ThemeResponses from(List themes) { + List responses = themes.stream() + .map(ThemeResponse::new) + .toList(); + return new ThemeResponses(responses); + } +} diff --git a/src/main/resources/static/js/reservation-with-member.js b/src/main/resources/static/js/reservation-with-member.js index 398945325..64648eba7 100644 --- a/src/main/resources/static/js/reservation-with-member.js +++ b/src/main/resources/static/js/reservation-with-member.js @@ -49,7 +49,7 @@ function fetchTimes() { function fetchThemes() { requestRead(THEME_API_ENDPOINT) .then(data => { - themesOptions.push(...data); + themesOptions.push(...data.themes); populateSelect('theme', themesOptions, 'name'); }) .catch(error => console.error('Error fetching theme:', error)); diff --git a/src/main/resources/static/js/theme.js b/src/main/resources/static/js/theme.js index a2ddee91e..34a9c5c8a 100644 --- a/src/main/resources/static/js/theme.js +++ b/src/main/resources/static/js/theme.js @@ -22,7 +22,7 @@ function render(data) { const tableBody = document.getElementById('table-body'); tableBody.innerHTML = ''; - data.forEach(item => { + data.themes.forEach(item => { const row = tableBody.insertRow(); cellFields.forEach((field, index) => { diff --git a/src/main/resources/static/js/user-reservation.js b/src/main/resources/static/js/user-reservation.js index 403abdffb..4efabab35 100644 --- a/src/main/resources/static/js/user-reservation.js +++ b/src/main/resources/static/js/user-reservation.js @@ -67,7 +67,7 @@ function checkDate() { timeSlots.innerHTML = ''; requestRead(THEME_API_ENDPOINT) - .then(renderTheme) + .then(data => renderTheme(data.themes)) .catch(error => console.error('Error fetching times:', error)); } } diff --git a/src/test/java/roomescape/controller/api/ThemeApiControllerTest.java b/src/test/java/roomescape/controller/api/ThemeApiControllerTest.java index f0009ae66..21f9a767d 100644 --- a/src/test/java/roomescape/controller/api/ThemeApiControllerTest.java +++ b/src/test/java/roomescape/controller/api/ThemeApiControllerTest.java @@ -27,7 +27,7 @@ void findTheme_Success() { .when().get("/themes") .then().log().all() .statusCode(200) - .body("size()", is(2)); + .body("themes.size()", is(2)); } @Test @@ -50,7 +50,7 @@ void addTheme_Success() { .when().get("/themes") .then().log().all() .statusCode(200) - .body("size()", is(3)); + .body("themes.size()", is(3)); } @Test @@ -65,6 +65,6 @@ void deleteTheme_Success() { .when().get("/themes") .then().log().all() .statusCode(200) - .body("size()", is(1)); + .body("themes.size()", is(1)); } } From ecd03986ec8c16b243624021eb989b6ef2a8389f Mon Sep 17 00:00:00 2001 From: Minjoo Date: Thu, 16 May 2024 13:59:18 +0900 Subject: [PATCH 22/42] =?UTF-8?q?refactor:=20List?= =?UTF-8?q?=EB=A5=BC=20=EB=8B=B4=EB=8A=94=20=EC=9D=BC=EA=B8=89=20=EA=B0=9D?= =?UTF-8?q?=EC=B2=B4=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/ReservationTimeApiController.java | 4 +- .../roomescape/domain/ReservationStatus.java | 44 +++++++------------ .../domain/ReservationStatuses.java | 24 ++++++++++ .../ReservationStatusResponses.java | 13 +++--- .../ReservationTimeFindService.java | 6 +-- .../domain/ReservationStatusTest.java | 7 +-- .../ReservationTimeFindServiceTest.java | 20 ++++----- 7 files changed, 62 insertions(+), 56 deletions(-) create mode 100644 src/main/java/roomescape/domain/ReservationStatuses.java diff --git a/src/main/java/roomescape/controller/api/ReservationTimeApiController.java b/src/main/java/roomescape/controller/api/ReservationTimeApiController.java index bbdee0aae..a2524bf30 100644 --- a/src/main/java/roomescape/controller/api/ReservationTimeApiController.java +++ b/src/main/java/roomescape/controller/api/ReservationTimeApiController.java @@ -14,7 +14,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import roomescape.domain.ReservationStatus; +import roomescape.domain.ReservationStatuses; import roomescape.domain.ReservationTime; import roomescape.service.dto.request.ReservationTimeSaveRequest; import roomescape.service.dto.response.reservationTime.ReservationStatusResponses; @@ -50,7 +50,7 @@ public ResponseEntity getReservationTimes() { public ResponseEntity getReservationTimesIsBooked( @RequestParam LocalDate date, @RequestParam @Positive(message = "1 이상의 값만 입력해주세요.") long themeId) { - ReservationStatus reservationStatus = reservationTimeFindService.findIsBooked(date, themeId); + ReservationStatuses reservationStatus = reservationTimeFindService.findIsBooked(date, themeId); return ResponseEntity.ok(ReservationStatusResponses.from(reservationStatus)); } diff --git a/src/main/java/roomescape/domain/ReservationStatus.java b/src/main/java/roomescape/domain/ReservationStatus.java index 7aa414841..4003d108e 100644 --- a/src/main/java/roomescape/domain/ReservationStatus.java +++ b/src/main/java/roomescape/domain/ReservationStatus.java @@ -1,53 +1,39 @@ package roomescape.domain; -import java.util.HashMap; -import java.util.List; -import java.util.Map; import java.util.Objects; public class ReservationStatus { - private final Map reservationStatus; + private final ReservationTime time; + private final boolean isBooked; - private ReservationStatus(Map reservationStatus) { - this.reservationStatus = reservationStatus; + public ReservationStatus(ReservationTime time, boolean isBooked) { + this.time = time; + this.isBooked = isBooked; } - public static ReservationStatus of(List reservedTimes, List reservationTimes) { - Map reservationStatus = new HashMap<>(); - for (ReservationTime reservationTime : reservationTimes) { - reservationStatus.put(reservationTime, isReserved(reservedTimes, reservationTime)); - } - return new ReservationStatus(reservationStatus); - } - - private static boolean isReserved(List reservedTimes, ReservationTime reservationTime) { - return reservedTimes.stream() - .anyMatch(reservedTime -> reservedTime.equals(reservationTime)); - } - - public Boolean findReservationStatusBy(ReservationTime reservationTime) { - return reservationStatus.get(reservationTime); + public ReservationTime getTime() { + return time; } - public Map getReservationStatus() { - return reservationStatus; + public boolean isBooked() { + return isBooked; } @Override - public boolean equals(Object object) { - if (this == object) { + public boolean equals(Object o) { + if (this == o) { return true; } - if (object == null || getClass() != object.getClass()) { + if (o == null || getClass() != o.getClass()) { return false; } - ReservationStatus that = (ReservationStatus) object; - return Objects.equals(reservationStatus, that.reservationStatus); + ReservationStatus that = (ReservationStatus) o; + return isBooked == that.isBooked && Objects.equals(time, that.time); } @Override public int hashCode() { - return Objects.hash(reservationStatus); + return Objects.hash(time, isBooked); } } diff --git a/src/main/java/roomescape/domain/ReservationStatuses.java b/src/main/java/roomescape/domain/ReservationStatuses.java new file mode 100644 index 000000000..d17d3a496 --- /dev/null +++ b/src/main/java/roomescape/domain/ReservationStatuses.java @@ -0,0 +1,24 @@ +package roomescape.domain; + +import java.util.ArrayList; +import java.util.List; + +public class ReservationStatuses { + + private final List reservationStatuses; + + private ReservationStatuses(List reservationStatuses) { + this.reservationStatuses = reservationStatuses; + } + + public static ReservationStatuses of(List reservedTimes, List reservationTimes) { + List times = reservationTimes.stream() + .map(time -> new ReservationStatus(time, reservedTimes.contains(time))) + .toList(); + return new ReservationStatuses(times); + } + + public List getReservationStatuses() { + return new ArrayList<>(reservationStatuses); + } +} diff --git a/src/main/java/roomescape/service/dto/response/reservationTime/ReservationStatusResponses.java b/src/main/java/roomescape/service/dto/response/reservationTime/ReservationStatusResponses.java index dbf6010fc..3dfb575c6 100644 --- a/src/main/java/roomescape/service/dto/response/reservationTime/ReservationStatusResponses.java +++ b/src/main/java/roomescape/service/dto/response/reservationTime/ReservationStatusResponses.java @@ -1,18 +1,15 @@ package roomescape.service.dto.response.reservationTime; import java.util.List; -import roomescape.domain.ReservationStatus; +import roomescape.domain.ReservationStatuses; public record ReservationStatusResponses(List reservationStatuses) { - public static ReservationStatusResponses from(ReservationStatus reservationStatus) { - List responses = reservationStatus.getReservationStatus() - .keySet() + public static ReservationStatusResponses from(ReservationStatuses reservationStatuses) { + List responses = reservationStatuses.getReservationStatuses() .stream() - .map(reservationTime -> new ReservationStatusResponse( - reservationTime, - reservationStatus.findReservationStatusBy(reservationTime)) - ).toList(); + .map(status -> new ReservationStatusResponse(status.getTime(), status.isBooked())) + .toList(); return new ReservationStatusResponses(responses); } } diff --git a/src/main/java/roomescape/service/reservationtime/ReservationTimeFindService.java b/src/main/java/roomescape/service/reservationtime/ReservationTimeFindService.java index bc434b6ba..5f2f08643 100644 --- a/src/main/java/roomescape/service/reservationtime/ReservationTimeFindService.java +++ b/src/main/java/roomescape/service/reservationtime/ReservationTimeFindService.java @@ -3,7 +3,7 @@ import java.time.LocalDate; import java.util.List; import org.springframework.stereotype.Service; -import roomescape.domain.ReservationStatus; +import roomescape.domain.ReservationStatuses; import roomescape.domain.ReservationTime; import roomescape.repository.ReservationTimeRepository; @@ -20,9 +20,9 @@ public List findReservationTimes() { return reservationTimeRepository.findAll(); } - public ReservationStatus findIsBooked(LocalDate date, long themeId) { + public ReservationStatuses findIsBooked(LocalDate date, long themeId) { List reservedTimes = reservationTimeRepository.findReservationByThemeIdAndDate(date, themeId); List reservationTimes = reservationTimeRepository.findAll(); - return ReservationStatus.of(reservedTimes, reservationTimes); + return ReservationStatuses.of(reservedTimes, reservationTimes); } } diff --git a/src/test/java/roomescape/domain/ReservationStatusTest.java b/src/test/java/roomescape/domain/ReservationStatusTest.java index 0e95d0848..43f4cef67 100644 --- a/src/test/java/roomescape/domain/ReservationStatusTest.java +++ b/src/test/java/roomescape/domain/ReservationStatusTest.java @@ -20,11 +20,12 @@ void checkSameReservation_Success() { reservationTime1, reservationTime2 ); - ReservationStatus reservationStatus = ReservationStatus.of(reservedTimes, reservationTimes); + List reservationStatuses = ReservationStatuses.of(reservedTimes, reservationTimes) + .getReservationStatuses(); assertAll( - () -> assertThat(reservationStatus.findReservationStatusBy(reservationTime1)).isTrue(), - () -> assertThat(reservationStatus.findReservationStatusBy(reservationTime2)).isFalse() + () -> assertThat(reservationStatuses.get(0).isBooked()).isTrue(), + () -> assertThat(reservationStatuses.get(1).isBooked()).isFalse() ); } } diff --git a/src/test/java/roomescape/service/reservationtime/ReservationTimeFindServiceTest.java b/src/test/java/roomescape/service/reservationtime/ReservationTimeFindServiceTest.java index fc636ef47..0be63d2af 100644 --- a/src/test/java/roomescape/service/reservationtime/ReservationTimeFindServiceTest.java +++ b/src/test/java/roomescape/service/reservationtime/ReservationTimeFindServiceTest.java @@ -3,15 +3,12 @@ import static org.assertj.core.api.Assertions.assertThat; import java.time.LocalDate; -import java.time.LocalTime; -import java.util.Map; +import java.util.List; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.annotation.DirtiesContext; import roomescape.domain.ReservationStatus; -import roomescape.domain.ReservationTime; import roomescape.service.BaseServiceTest; class ReservationTimeFindServiceTest extends BaseServiceTest { @@ -23,11 +20,12 @@ class ReservationTimeFindServiceTest extends BaseServiceTest { @DisplayName("날짜와 테마가 주어지면 각 시간의 예약 여부를 구한다.") void findAvailabilityByDateAndTheme() { LocalDate date = LocalDate.now().plusDays(1L); - ReservationStatus reservationStatus = reservationTimeFindService.findIsBooked(date, 1L); - assertThat(reservationStatus.getReservationStatus()) - .isEqualTo(Map.of( - new ReservationTime(1L, LocalTime.of(10, 0)), true, - new ReservationTime(2L, LocalTime.of(11, 0)), false - )); + List reservationStatuses = reservationTimeFindService.findIsBooked(date, 1L) + .getReservationStatuses(); + Assertions.assertAll( + () -> assertThat(reservationStatuses.size()).isEqualTo(2), + () -> assertThat(reservationStatuses.get(0).isBooked()).isTrue(), + () -> assertThat(reservationStatuses.get(1).isBooked()).isFalse() + ); } } From 799af57cdfac99c3ea837d75cfcc90c770e4a1c3 Mon Sep 17 00:00:00 2001 From: Minjoo Date: Thu, 16 May 2024 15:04:46 +0900 Subject: [PATCH 23/42] =?UTF-8?q?refactor:=20=EC=98=88=EC=95=BD=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EA=B4=80=EB=A0=A8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=ED=95=98=EB=82=98=EB=A1=9C=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/ReservationApiController.java | 6 +- .../AdminReservationCreateService.java | 32 -------- .../reservation/ReservationCreateService.java | 74 ++++++++++++++++--- .../ReservationCreateValidator.java | 64 ---------------- 4 files changed, 65 insertions(+), 111 deletions(-) delete mode 100644 src/main/java/roomescape/service/reservation/AdminReservationCreateService.java delete mode 100644 src/main/java/roomescape/service/reservation/ReservationCreateValidator.java diff --git a/src/main/java/roomescape/controller/api/ReservationApiController.java b/src/main/java/roomescape/controller/api/ReservationApiController.java index 178d89d5e..e4f242624 100644 --- a/src/main/java/roomescape/controller/api/ReservationApiController.java +++ b/src/main/java/roomescape/controller/api/ReservationApiController.java @@ -22,7 +22,6 @@ import roomescape.service.dto.response.reservation.ReservationResponse; import roomescape.service.dto.response.reservation.ReservationResponses; import roomescape.service.dto.response.reservation.UserReservationResponses; -import roomescape.service.reservation.AdminReservationCreateService; import roomescape.service.reservation.ReservationCreateService; import roomescape.service.reservation.ReservationDeleteService; import roomescape.service.reservation.ReservationFindService; @@ -32,16 +31,13 @@ public class ReservationApiController { private final ReservationCreateService reservationCreateService; - private final AdminReservationCreateService adminReservationCreateService; private final ReservationFindService reservationFindService; private final ReservationDeleteService reservationDeleteService; public ReservationApiController(ReservationCreateService reservationCreateService, - AdminReservationCreateService adminReservationCreateService, ReservationFindService reservationFindService, ReservationDeleteService reservationDeleteService) { this.reservationCreateService = reservationCreateService; - this.adminReservationCreateService = adminReservationCreateService; this.reservationFindService = reservationFindService; this.reservationDeleteService = reservationDeleteService; } @@ -78,7 +74,7 @@ public ResponseEntity addReservationByUser(@RequestBody @Va @PostMapping("/admin/reservations") public ResponseEntity addReservationByAdmin(@RequestBody @Valid ReservationAdminSaveRequest request) { - Reservation newReservation = adminReservationCreateService.createReservation(request); + Reservation newReservation = reservationCreateService.createReservation(request); return ResponseEntity.created(URI.create("/admin/reservations/" + newReservation.getId())) .body(new ReservationResponse(newReservation)); } diff --git a/src/main/java/roomescape/service/reservation/AdminReservationCreateService.java b/src/main/java/roomescape/service/reservation/AdminReservationCreateService.java deleted file mode 100644 index 8d4c66bd5..000000000 --- a/src/main/java/roomescape/service/reservation/AdminReservationCreateService.java +++ /dev/null @@ -1,32 +0,0 @@ -package roomescape.service.reservation; - -import org.springframework.stereotype.Service; -import roomescape.domain.Member; -import roomescape.domain.Reservation; -import roomescape.domain.ReservationTime; -import roomescape.domain.Theme; -import roomescape.repository.ReservationRepository; -import roomescape.service.dto.request.ReservationAdminSaveRequest; - -@Service -public class AdminReservationCreateService { - - private final ReservationCreateValidator reservationCreateValidator; - private final ReservationRepository reservationRepository; - - public AdminReservationCreateService(ReservationRepository reservationRepository, ReservationCreateValidator reservationCreateValidator) { - this.reservationRepository = reservationRepository; - this.reservationCreateValidator = reservationCreateValidator; - } - - public Reservation createReservation(ReservationAdminSaveRequest request) { - ReservationTime reservationTime = reservationCreateValidator.getValidReservationTime(request.timeId()); - reservationCreateValidator.validateDateIsFuture(request.date(), reservationTime); - Theme theme = reservationCreateValidator.getValidTheme(request.themeId()); - reservationCreateValidator.validateAlreadyBooked(request.date(), request.timeId(), request.themeId()); - Member member = reservationCreateValidator.getValidMember(request.memberId()); - - Reservation reservation = request.toEntity(request, reservationTime, theme, member); - return reservationRepository.save(reservation); - } -} diff --git a/src/main/java/roomescape/service/reservation/ReservationCreateService.java b/src/main/java/roomescape/service/reservation/ReservationCreateService.java index 7aaec14ef..d3436c414 100644 --- a/src/main/java/roomescape/service/reservation/ReservationCreateService.java +++ b/src/main/java/roomescape/service/reservation/ReservationCreateService.java @@ -1,32 +1,86 @@ package roomescape.service.reservation; +import java.time.LocalDate; +import java.time.LocalDateTime; import org.springframework.stereotype.Service; import roomescape.domain.Member; import roomescape.domain.Reservation; import roomescape.domain.ReservationTime; import roomescape.domain.Theme; +import roomescape.repository.MemberRepository; import roomescape.repository.ReservationRepository; +import roomescape.repository.ReservationTimeRepository; +import roomescape.repository.ThemeRepository; +import roomescape.service.dto.request.ReservationAdminSaveRequest; import roomescape.service.dto.request.ReservationSaveRequest; @Service public class ReservationCreateService { - private final ReservationCreateValidator reservationCreateValidator; private final ReservationRepository reservationRepository; + private final ReservationTimeRepository reservationTimeRepository; + private final ThemeRepository themeRepository; + private final MemberRepository memberRepository; - public ReservationCreateService(ReservationCreateValidator reservationCreateValidator, - ReservationRepository reservationRepository) { - this.reservationCreateValidator = reservationCreateValidator; + public ReservationCreateService(ReservationRepository reservationRepository, + ReservationTimeRepository reservationTimeRepository, + ThemeRepository themeRepository, + MemberRepository memberRepository) { this.reservationRepository = reservationRepository; + this.reservationTimeRepository = reservationTimeRepository; + this.themeRepository = themeRepository; + this.memberRepository = memberRepository; + } + + public Reservation createReservation(ReservationAdminSaveRequest request) { + Reservation reservation = request.toEntity( + request, + getReservationTime(request.timeId()), + getTheme(request.themeId()), + getMember(request.memberId())); + return createReservation(reservation); } public Reservation createReservation(ReservationSaveRequest request, Member member) { - ReservationTime reservationTime = reservationCreateValidator.getValidReservationTime(request.timeId()); - reservationCreateValidator.validateDateIsFuture(request.date(), reservationTime); - Theme theme = reservationCreateValidator.getValidTheme(request.themeId()); - reservationCreateValidator.validateAlreadyBooked(request.date(), request.timeId(), request.themeId()); + Reservation reservation = request.toEntity( + request, + getReservationTime(request.timeId()), + getTheme(request.themeId()), + member); + return createReservation(reservation); + } + + private Reservation createReservation(Reservation request) { + validateDateIsFuture(request.getDate(), request.getReservationTime()); + validateAlreadyBooked(request.getDate(), request.getReservationTime().getId(), request.getTheme().getId()); + return reservationRepository.save(request); + } + + private ReservationTime getReservationTime(long reservationId) { + return reservationTimeRepository.findById(reservationId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 예약 시간 입니다.")); + } + + private Theme getTheme(long themeId) { + return themeRepository.findById(themeId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 테마 입니다.")); + } + + private Member getMember(long memberId) { + return memberRepository.findById(memberId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); + } + + public void validateAlreadyBooked(LocalDate date, long timeId, long themeId) { + if (reservationRepository.existsByDateAndTimeIdAndThemeId(date, timeId, themeId)) { + throw new IllegalArgumentException("해당 시간에 이미 예약된 테마입니다."); + } + } - Reservation reservation = request.toEntity(request, reservationTime, theme, member); - return reservationRepository.save(reservation); + public void validateDateIsFuture(LocalDate date, ReservationTime reservationTime) { + LocalDateTime localDateTime = LocalDateTime.of(date, reservationTime.getStartAt()); + if (localDateTime.isBefore(LocalDateTime.now())) { + throw new IllegalArgumentException("지나간 날짜와 시간에 대한 예약 생성은 불가능합니다."); + } } } diff --git a/src/main/java/roomescape/service/reservation/ReservationCreateValidator.java b/src/main/java/roomescape/service/reservation/ReservationCreateValidator.java deleted file mode 100644 index 0f19f6e9a..000000000 --- a/src/main/java/roomescape/service/reservation/ReservationCreateValidator.java +++ /dev/null @@ -1,64 +0,0 @@ -package roomescape.service.reservation; - -import java.time.LocalDate; -import java.time.LocalDateTime; -import org.springframework.stereotype.Component; -import roomescape.domain.Member; -import roomescape.domain.ReservationTime; -import roomescape.domain.Theme; -import roomescape.repository.MemberRepository; -import roomescape.repository.ReservationRepository; -import roomescape.repository.ReservationTimeRepository; -import roomescape.repository.ThemeRepository; - -@Component -public class ReservationCreateValidator { - - private final ReservationRepository reservationRepository; - private final ReservationTimeRepository reservationTimeRepository; - private final ThemeRepository themeRepository; - private final MemberRepository memberRepository; - - public ReservationCreateValidator(ReservationRepository reservationRepository, - ReservationTimeRepository reservationTimeRepository, - ThemeRepository themeRepository, - MemberRepository memberRepository) { - this.reservationRepository = reservationRepository; - this.reservationTimeRepository = reservationTimeRepository; - this.themeRepository = themeRepository; - this.memberRepository = memberRepository; - } - - - public ReservationTime getValidReservationTime(long reservationId) { - return reservationTimeRepository.findById(reservationId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 예약 시간 입니다.")); - } - - public Theme getValidTheme(long themeId) { - return themeRepository.findById(themeId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 테마 입니다.")); - } - - public void validateAlreadyBooked(LocalDate date, long timeId, long themeId) { - if (reservationRepository.existsByDateAndTimeIdAndThemeId(date, timeId, themeId)) { - throw new IllegalArgumentException("해당 시간에 이미 예약된 테마입니다."); - } - } - - public void validateDateIsFuture(LocalDate date, ReservationTime reservationTime) { - LocalDateTime localDateTime = toLocalDateTime(date, reservationTime); - if (localDateTime.isBefore(LocalDateTime.now())) { - throw new IllegalArgumentException("지나간 날짜와 시간에 대한 예약 생성은 불가능합니다."); - } - } - - private LocalDateTime toLocalDateTime(LocalDate date, ReservationTime reservationTime) { - return LocalDateTime.of(date, reservationTime.getStartAt()); - } - - public Member getValidMember(long memberId) { - return memberRepository.findById(memberId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다.")); - } -} From dddb6f42fb1328d4dd20085cd827126c502059d4 Mon Sep 17 00:00:00 2001 From: Minjoo Date: Thu, 16 May 2024 15:33:38 +0900 Subject: [PATCH 24/42] =?UTF-8?q?refactor:=20=EC=98=88=EC=95=BD=EB=90=9C?= =?UTF-8?q?=20=ED=85=8C=EB=A7=88=20=EC=8B=9C=EA=B0=84=20JPQL=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20-=20=ED=8A=B9=EC=A0=95=20=EB=82=A0?= =?UTF-8?q?=EC=A7=9C,=20=ED=8A=B9=EC=A0=95=20=ED=85=8C=EB=A7=88=EC=9D=98?= =?UTF-8?q?=20=EC=98=88=EC=95=BD=EC=9D=84=20=EB=AA=A8=EB=91=90=20=EB=B6=88?= =?UTF-8?q?=EB=9F=AC=EC=98=A8=20=EB=92=A4=20service=20=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=96=B4=EC=97=90=EC=84=9C=20reservationTime=20=EC=B0=BE?= =?UTF-8?q?=EB=8A=94=20=EB=A1=9C=EC=A7=81=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/ReservationTimeApiController.java | 2 +- .../repository/ReservationRepository.java | 5 +++-- .../repository/ReservationTimeRepository.java | 13 ------------- .../reservationtime/ReservationTimeFindService.java | 13 ++++++++++--- .../ReservationTimeFindServiceTest.java | 2 +- 5 files changed, 15 insertions(+), 20 deletions(-) diff --git a/src/main/java/roomescape/controller/api/ReservationTimeApiController.java b/src/main/java/roomescape/controller/api/ReservationTimeApiController.java index a2524bf30..6dfc81cc8 100644 --- a/src/main/java/roomescape/controller/api/ReservationTimeApiController.java +++ b/src/main/java/roomescape/controller/api/ReservationTimeApiController.java @@ -50,7 +50,7 @@ public ResponseEntity getReservationTimes() { public ResponseEntity getReservationTimesIsBooked( @RequestParam LocalDate date, @RequestParam @Positive(message = "1 이상의 값만 입력해주세요.") long themeId) { - ReservationStatuses reservationStatus = reservationTimeFindService.findIsBooked(date, themeId); + ReservationStatuses reservationStatus = reservationTimeFindService.findReservationStatuses(date, themeId); return ResponseEntity.ok(ReservationStatusResponses.from(reservationStatus)); } diff --git a/src/main/java/roomescape/repository/ReservationRepository.java b/src/main/java/roomescape/repository/ReservationRepository.java index 94c307994..9186827b5 100644 --- a/src/main/java/roomescape/repository/ReservationRepository.java +++ b/src/main/java/roomescape/repository/ReservationRepository.java @@ -10,11 +10,12 @@ public interface ReservationRepository extends JpaRepository { boolean existsByThemeId(long themeId); - List findByMemberIdAndThemeIdAndDateBetween(long memberId, long themeId, LocalDate dateFrom, - LocalDate dateTo); + List findByMemberIdAndThemeIdAndDateBetween(long memberId, long themeId, LocalDate dateFrom, LocalDate dateTo); List findByMemberId(long memberId); + List findByDateAndThemeId(LocalDate date, long themeId); + boolean existsByDateAndTimeIdAndThemeId(LocalDate date, long timeId, long themeId); boolean existsByTimeId(long timeId); diff --git a/src/main/java/roomescape/repository/ReservationTimeRepository.java b/src/main/java/roomescape/repository/ReservationTimeRepository.java index b4c7f474b..8d0ff2b32 100644 --- a/src/main/java/roomescape/repository/ReservationTimeRepository.java +++ b/src/main/java/roomescape/repository/ReservationTimeRepository.java @@ -1,12 +1,8 @@ package roomescape.repository; -import java.time.LocalDate; import java.time.LocalTime; -import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import roomescape.domain.ReservationTime; @@ -14,13 +10,4 @@ public interface ReservationTimeRepository extends JpaRepository { Optional findByStartAt(LocalTime startAt); - - @Query("SELECT rt " - + "FROM ReservationTime rt " - + "INNER JOIN Reservation r " - + "ON rt.id = r.time.id " - + "WHERE r.date = :date " - + "AND r.theme.id = :themeId") - List findReservationByThemeIdAndDate(@Param("date") LocalDate date, - @Param("themeId") long themeId); } diff --git a/src/main/java/roomescape/service/reservationtime/ReservationTimeFindService.java b/src/main/java/roomescape/service/reservationtime/ReservationTimeFindService.java index 5f2f08643..48be5237f 100644 --- a/src/main/java/roomescape/service/reservationtime/ReservationTimeFindService.java +++ b/src/main/java/roomescape/service/reservationtime/ReservationTimeFindService.java @@ -3,16 +3,20 @@ import java.time.LocalDate; import java.util.List; import org.springframework.stereotype.Service; +import roomescape.domain.Reservation; import roomescape.domain.ReservationStatuses; import roomescape.domain.ReservationTime; +import roomescape.repository.ReservationRepository; import roomescape.repository.ReservationTimeRepository; @Service public class ReservationTimeFindService { + private final ReservationRepository reservationRepository; private final ReservationTimeRepository reservationTimeRepository; - public ReservationTimeFindService(ReservationTimeRepository reservationTimeRepository) { + public ReservationTimeFindService(ReservationRepository reservationRepository, ReservationTimeRepository reservationTimeRepository) { + this.reservationRepository = reservationRepository; this.reservationTimeRepository = reservationTimeRepository; } @@ -20,8 +24,11 @@ public List findReservationTimes() { return reservationTimeRepository.findAll(); } - public ReservationStatuses findIsBooked(LocalDate date, long themeId) { - List reservedTimes = reservationTimeRepository.findReservationByThemeIdAndDate(date, themeId); + public ReservationStatuses findReservationStatuses(LocalDate date, long themeId) { + List reservations = reservationRepository.findByDateAndThemeId(date, themeId); + List reservedTimes = reservations.stream() + .map(Reservation::getReservationTime) + .toList(); List reservationTimes = reservationTimeRepository.findAll(); return ReservationStatuses.of(reservedTimes, reservationTimes); } diff --git a/src/test/java/roomescape/service/reservationtime/ReservationTimeFindServiceTest.java b/src/test/java/roomescape/service/reservationtime/ReservationTimeFindServiceTest.java index 0be63d2af..fde9c74ac 100644 --- a/src/test/java/roomescape/service/reservationtime/ReservationTimeFindServiceTest.java +++ b/src/test/java/roomescape/service/reservationtime/ReservationTimeFindServiceTest.java @@ -20,7 +20,7 @@ class ReservationTimeFindServiceTest extends BaseServiceTest { @DisplayName("날짜와 테마가 주어지면 각 시간의 예약 여부를 구한다.") void findAvailabilityByDateAndTheme() { LocalDate date = LocalDate.now().plusDays(1L); - List reservationStatuses = reservationTimeFindService.findIsBooked(date, 1L) + List reservationStatuses = reservationTimeFindService.findReservationStatuses(date, 1L) .getReservationStatuses(); Assertions.assertAll( () -> assertThat(reservationStatuses.size()).isEqualTo(2), From 59be832c2e6a8befb41a5adcd470331bcd1b826c Mon Sep 17 00:00:00 2001 From: Minjoo Date: Thu, 16 May 2024 15:50:55 +0900 Subject: [PATCH 25/42] =?UTF-8?q?refactor:=20=EC=98=88=EC=95=BD=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EA=B4=80=EB=A6=AC=EC=9E=90=20API=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/ReservationApiController.java | 39 ++-------- .../admin/AdminReservationApiController.java | 55 +++++++++++++ .../static/js/reservation-with-member.js | 2 +- .../api/ReservationApiControllerTest.java | 46 +++-------- .../AdminReservationApiControllerTest.java | 78 +++++++++++++++++++ 5 files changed, 149 insertions(+), 71 deletions(-) create mode 100644 src/main/java/roomescape/controller/api/admin/AdminReservationApiController.java create mode 100644 src/test/java/roomescape/controller/api/admin/AdminReservationApiControllerTest.java diff --git a/src/main/java/roomescape/controller/api/ReservationApiController.java b/src/main/java/roomescape/controller/api/ReservationApiController.java index e4f242624..57e916f6e 100644 --- a/src/main/java/roomescape/controller/api/ReservationApiController.java +++ b/src/main/java/roomescape/controller/api/ReservationApiController.java @@ -3,7 +3,6 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.Positive; import java.net.URI; -import java.time.LocalDate; import java.util.List; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; @@ -12,15 +11,12 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import roomescape.auth.AuthenticatedMember; import roomescape.domain.Member; import roomescape.domain.Reservation; -import roomescape.service.dto.request.ReservationAdminSaveRequest; import roomescape.service.dto.request.ReservationSaveRequest; import roomescape.service.dto.response.reservation.ReservationResponse; -import roomescape.service.dto.response.reservation.ReservationResponses; import roomescape.service.dto.response.reservation.UserReservationResponses; import roomescape.service.reservation.ReservationCreateService; import roomescape.service.reservation.ReservationDeleteService; @@ -30,34 +26,18 @@ @RestController public class ReservationApiController { - private final ReservationCreateService reservationCreateService; private final ReservationFindService reservationFindService; + private final ReservationCreateService reservationCreateService; private final ReservationDeleteService reservationDeleteService; - public ReservationApiController(ReservationCreateService reservationCreateService, - ReservationFindService reservationFindService, + public ReservationApiController(ReservationFindService reservationFindService, + ReservationCreateService reservationCreateService, ReservationDeleteService reservationDeleteService) { - this.reservationCreateService = reservationCreateService; this.reservationFindService = reservationFindService; + this.reservationCreateService = reservationCreateService; this.reservationDeleteService = reservationDeleteService; } - @GetMapping("/reservations") - public ResponseEntity getReservations() { - List reservations = reservationFindService.findReservations(); - return ResponseEntity.ok(ReservationResponses.from(reservations)); - } - - @GetMapping("/admin/reservations/search") - public ResponseEntity getSearchingReservations(@RequestParam long memberId, - @RequestParam long themeId, - @RequestParam LocalDate dateFrom, - @RequestParam LocalDate dateTo - ) { - List reservations = reservationFindService.searchReservations(memberId, themeId, dateFrom, dateTo); - return ResponseEntity.ok(ReservationResponses.from(reservations)); - } - @GetMapping("/reservations-mine") public ResponseEntity getUserReservations(@AuthenticatedMember Member member) { List userReservations = reservationFindService.findUserReservations(member.getId()); @@ -65,20 +45,13 @@ public ResponseEntity getUserReservations(@Authenticat } @PostMapping("/reservations") - public ResponseEntity addReservationByUser(@RequestBody @Valid ReservationSaveRequest request, - @AuthenticatedMember Member member) { + public ResponseEntity addReservation(@RequestBody @Valid ReservationSaveRequest request, + @AuthenticatedMember Member member) { Reservation newReservation = reservationCreateService.createReservation(request, member); return ResponseEntity.created(URI.create("/reservations/" + newReservation.getId())) .body(new ReservationResponse(newReservation)); } - @PostMapping("/admin/reservations") - public ResponseEntity addReservationByAdmin(@RequestBody @Valid ReservationAdminSaveRequest request) { - Reservation newReservation = reservationCreateService.createReservation(request); - return ResponseEntity.created(URI.create("/admin/reservations/" + newReservation.getId())) - .body(new ReservationResponse(newReservation)); - } - @DeleteMapping("/reservations/{reservationId}") public ResponseEntity deleteReservation(@PathVariable @Positive(message = "1 이상의 값만 입력해주세요.") long reservationId) { diff --git a/src/main/java/roomescape/controller/api/admin/AdminReservationApiController.java b/src/main/java/roomescape/controller/api/admin/AdminReservationApiController.java new file mode 100644 index 000000000..7b0160c50 --- /dev/null +++ b/src/main/java/roomescape/controller/api/admin/AdminReservationApiController.java @@ -0,0 +1,55 @@ +package roomescape.controller.api.admin; + +import jakarta.validation.Valid; +import java.net.URI; +import java.time.LocalDate; +import java.util.List; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import roomescape.domain.Reservation; +import roomescape.service.dto.request.ReservationAdminSaveRequest; +import roomescape.service.dto.response.reservation.ReservationResponse; +import roomescape.service.dto.response.reservation.ReservationResponses; +import roomescape.service.reservation.ReservationCreateService; +import roomescape.service.reservation.ReservationFindService; + +@RestController +public class AdminReservationApiController { + + private final ReservationFindService reservationFindService; + private final ReservationCreateService reservationCreateService; + + public AdminReservationApiController(ReservationFindService reservationFindService, + ReservationCreateService reservationCreateService) { + this.reservationFindService = reservationFindService; + this.reservationCreateService = reservationCreateService; + } + + @GetMapping("/admin/reservations") + public ResponseEntity getReservations() { + List reservations = reservationFindService.findReservations(); + return ResponseEntity.ok(ReservationResponses.from(reservations)); + } + + @GetMapping("/admin/reservations/search") + public ResponseEntity getSearchingReservations(@RequestParam long memberId, + @RequestParam long themeId, + @RequestParam LocalDate dateFrom, + @RequestParam LocalDate dateTo + ) { + List reservations = reservationFindService.searchReservations(memberId, themeId, dateFrom, dateTo); + return ResponseEntity.ok(ReservationResponses.from(reservations)); + } + + @PostMapping("/admin/reservations") + public ResponseEntity addReservation( + @RequestBody @Valid ReservationAdminSaveRequest request) { + Reservation newReservation = reservationCreateService.createReservation(request); + return ResponseEntity.created(URI.create("/admin/reservations/" + newReservation.getId())) + .body(new ReservationResponse(newReservation)); + } +} diff --git a/src/main/resources/static/js/reservation-with-member.js b/src/main/resources/static/js/reservation-with-member.js index 64648eba7..7b7dd1a26 100644 --- a/src/main/resources/static/js/reservation-with-member.js +++ b/src/main/resources/static/js/reservation-with-member.js @@ -11,7 +11,7 @@ document.addEventListener('DOMContentLoaded', () => { document.getElementById('add-button').addEventListener('click', addInputRow); document.getElementById('filter-form').addEventListener('submit', applyFilter); - requestRead(RESERVATION_API_ENDPOINT) + requestRead(`/admin${RESERVATION_API_ENDPOINT}`) .then(render) .catch(error => console.error('Error fetching reservations:', error)); diff --git a/src/test/java/roomescape/controller/api/ReservationApiControllerTest.java b/src/test/java/roomescape/controller/api/ReservationApiControllerTest.java index b13e7c30e..7fde26c03 100644 --- a/src/test/java/roomescape/controller/api/ReservationApiControllerTest.java +++ b/src/test/java/roomescape/controller/api/ReservationApiControllerTest.java @@ -12,9 +12,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.test.annotation.DirtiesContext; import roomescape.controller.BaseControllerTest; import roomescape.util.TokenGenerator; @@ -30,36 +28,7 @@ public void setUp() { } @Test - @DisplayName("관리자 예약 페이지 요청이 정상적으로 수행된다.") - void moveToReservationPage_Success() { - RestAssured.given().log().all() - .cookie("token", TokenGenerator.makeAdminToken()) - .when().get("/admin/reservation") - .then().log().all() - .statusCode(200); - } - - @Test - @DisplayName("관리자 예약 페이지에 권한이 없는 유저는 401을 받는다.") - void moveToReservationPage_Failure() { - RestAssured.given().log().all() - .when().get("/admin/reservation") - .then().log().all() - .statusCode(401); - } - - @Test - @DisplayName("예약 목록 조회 요청이 정상적으로 수행된다.") - void selectReservationListRequest_Success() { - RestAssured.given().log().all() - .when().get("/reservations") - .then().log().all() - .statusCode(200) - .body("reservations.size()", is(1)); - } - - @Test - @DisplayName("유저 예약 목록 조회를 정상적으로 수행한다.") + @DisplayName("특정 유저 예약 목록 조회를 정상적으로 수행한다.") void selectUserReservationListRequest_Success() { RestAssured.given().log().all() .cookie("token", TokenGenerator.makeUserToken()) @@ -70,9 +39,9 @@ void selectUserReservationListRequest_Success() { } @Test - @DisplayName("예약 추가, 조회를 정상적으로 수행한다.") - void ReservationTime_CREATE_READ_Success() { - Map reservation = Map.of("name", "브라운", + @DisplayName("유저가 예약 추가, 조회를 정상적으로 수행한다.") + void Reservation_CREATE_READ_Success() { + Map reservation = Map.of( "date", LocalDate.now().plusDays(2L).toString(), "timeId", 1, "themeId", 1 @@ -87,7 +56,8 @@ void ReservationTime_CREATE_READ_Success() { .statusCode(201); RestAssured.given().log().all() - .when().get("/reservations") + .cookie("token", TokenGenerator.makeAdminToken()) + .when().get("/admin/reservations") .then().log().all() .statusCode(200) .body("reservations.size()", is(2)); @@ -102,12 +72,14 @@ void deleteReservation_InDatabase_Success() { .statusCode(204); RestAssured.given().log().all() - .when().get("/reservations") + .cookie("token", TokenGenerator.makeAdminToken()) + .when().get("/admin/reservations") .then().log().all() .statusCode(200) .body("reservations.size()", is(0)); } + // TODO: 불필요한 테스트 삭제 @Test @DisplayName("데이터베이스 관련 로직을 컨트롤러에서 분리하였다.") void layerRefactoring() { diff --git a/src/test/java/roomescape/controller/api/admin/AdminReservationApiControllerTest.java b/src/test/java/roomescape/controller/api/admin/AdminReservationApiControllerTest.java new file mode 100644 index 000000000..db08c0f5d --- /dev/null +++ b/src/test/java/roomescape/controller/api/admin/AdminReservationApiControllerTest.java @@ -0,0 +1,78 @@ +package roomescape.controller.api.admin; + +import static org.hamcrest.Matchers.is; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import java.time.LocalDate; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import roomescape.controller.BaseControllerTest; +import roomescape.util.TokenGenerator; + +class AdminReservationApiControllerTest extends BaseControllerTest { + + @Override + @BeforeEach + public void setUp() { + super.setUp(); + } + + @Test + @DisplayName("관리자 예약 페이지 요청이 정상적으로 수행된다.") + void moveToReservationPage_Success() { + RestAssured.given().log().all() + .cookie("token", TokenGenerator.makeAdminToken()) + .when().get("/admin/reservation") + .then().log().all() + .statusCode(200); + } + + @Test + @DisplayName("관리자 예약 페이지에 권한이 없는 유저는 401을 받는다.") + void moveToReservationPage_Failure() { + RestAssured.given().log().all() + .when().get("/admin/reservation") + .then().log().all() + .statusCode(401); + } + + @Test + @DisplayName("전체 예약 목록 조회 요청이 정상적으로 수행된다.") + void selectReservationListRequest_Success() { + RestAssured.given().log().all() + .cookie("token", TokenGenerator.makeAdminToken()) + .when().get("/admin/reservations") + .then().log().all() + .statusCode(200) + .body("reservations.size()", is(1)); + } + + @Test + @DisplayName("관리자가 예약 추가, 조회를 정상적으로 수행한다.") + void Reservation_CREATE_READ_Success() { + Map reservation = Map.of( + "memberId", 1, + "date", LocalDate.now().plusDays(2L).toString(), + "timeId", 1, + "themeId", 1 + ); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .cookie("token", TokenGenerator.makeAdminToken()) + .body(reservation) + .when().post("/admin/reservations") + .then().log().all() + .statusCode(201); + + RestAssured.given().log().all() + .cookie("token", TokenGenerator.makeAdminToken()) + .when().get("/admin/reservations") + .then().log().all() + .statusCode(200) + .body("reservations.size()", is(2)); + } +} From 9f2d268d3f9df02ac7cd4bc56298da33b74b9075 Mon Sep 17 00:00:00 2001 From: Minjoo Date: Thu, 16 May 2024 15:55:00 +0900 Subject: [PATCH 26/42] =?UTF-8?q?refactor:=20=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EA=B4=80=EB=A0=A8=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=EB=A7=8C=20=EC=A0=91=EA=B7=BC=20=EA=B0=80=EB=8A=A5?= =?UTF-8?q?=ED=95=9C=20API=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 --- .../AdminMemberApiController.java} | 8 ++++---- src/main/resources/static/js/reservation-with-member.js | 2 +- .../AdminMemberApiControllerTest.java} | 7 +++---- 3 files changed, 8 insertions(+), 9 deletions(-) rename src/main/java/roomescape/controller/api/{MemberApiController.java => admin/AdminMemberApiController.java} (78%) rename src/test/java/roomescape/controller/api/{MemberApiControllerTest.java => admin/AdminMemberApiControllerTest.java} (78%) diff --git a/src/main/java/roomescape/controller/api/MemberApiController.java b/src/main/java/roomescape/controller/api/admin/AdminMemberApiController.java similarity index 78% rename from src/main/java/roomescape/controller/api/MemberApiController.java rename to src/main/java/roomescape/controller/api/admin/AdminMemberApiController.java index 1b563e15d..53dec5ec9 100644 --- a/src/main/java/roomescape/controller/api/MemberApiController.java +++ b/src/main/java/roomescape/controller/api/admin/AdminMemberApiController.java @@ -1,4 +1,4 @@ -package roomescape.controller.api; +package roomescape.controller.api.admin; import java.util.List; import org.springframework.http.ResponseEntity; @@ -9,15 +9,15 @@ import roomescape.service.member.MemberService; @RestController -public class MemberApiController { +public class AdminMemberApiController { private final MemberService memberService; - public MemberApiController(MemberService memberService) { + public AdminMemberApiController(MemberService memberService) { this.memberService = memberService; } - @GetMapping("/members") + @GetMapping("/admin/members") public ResponseEntity getMembers() { List members = memberService.findMembers(); return ResponseEntity.ok(MemberIdAndNameResponses.from(members)); diff --git a/src/main/resources/static/js/reservation-with-member.js b/src/main/resources/static/js/reservation-with-member.js index 7b7dd1a26..7a797e275 100644 --- a/src/main/resources/static/js/reservation-with-member.js +++ b/src/main/resources/static/js/reservation-with-member.js @@ -56,7 +56,7 @@ function fetchThemes() { } function fetchMembers() { - requestRead(MEMBER_API_ENDPOINT) + requestRead(`/admin${MEMBER_API_ENDPOINT}`) .then(data => { membersOptions.push(...data.members); populateSelect('member', membersOptions, 'name'); diff --git a/src/test/java/roomescape/controller/api/MemberApiControllerTest.java b/src/test/java/roomescape/controller/api/admin/AdminMemberApiControllerTest.java similarity index 78% rename from src/test/java/roomescape/controller/api/MemberApiControllerTest.java rename to src/test/java/roomescape/controller/api/admin/AdminMemberApiControllerTest.java index 52afd50eb..7ad9602d6 100644 --- a/src/test/java/roomescape/controller/api/MemberApiControllerTest.java +++ b/src/test/java/roomescape/controller/api/admin/AdminMemberApiControllerTest.java @@ -1,4 +1,4 @@ -package roomescape.controller.api; +package roomescape.controller.api.admin; import static org.hamcrest.Matchers.is; @@ -6,11 +6,10 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; import roomescape.controller.BaseControllerTest; import roomescape.util.TokenGenerator; -class MemberApiControllerTest extends BaseControllerTest { +class AdminMemberApiControllerTest extends BaseControllerTest { @Override @BeforeEach @@ -23,7 +22,7 @@ public void setUp() { void selectMembers_Success() { RestAssured.given().log().all() .cookie("token", TokenGenerator.makeAdminToken()) - .when().get("/members") + .when().get("/admin/members") .then().log().all() .statusCode(200) .body("members.size()", is(2)); From 7b94d74ec83a08a54bef1c448fd904ca8bdc335e Mon Sep 17 00:00:00 2001 From: Minjoo Date: Thu, 16 May 2024 15:55:48 +0900 Subject: [PATCH 27/42] =?UTF-8?q?test:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/ReservationApiControllerTest.java | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/src/test/java/roomescape/controller/api/ReservationApiControllerTest.java b/src/test/java/roomescape/controller/api/ReservationApiControllerTest.java index 7fde26c03..e29dda04d 100644 --- a/src/test/java/roomescape/controller/api/ReservationApiControllerTest.java +++ b/src/test/java/roomescape/controller/api/ReservationApiControllerTest.java @@ -1,26 +1,19 @@ package roomescape.controller.api; -import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.is; import io.restassured.RestAssured; import io.restassured.http.ContentType; -import java.lang.reflect.Field; import java.time.LocalDate; import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.jdbc.core.JdbcTemplate; import roomescape.controller.BaseControllerTest; import roomescape.util.TokenGenerator; public class ReservationApiControllerTest extends BaseControllerTest { - @Autowired - private ReservationApiController reservationApiController; - @Override @BeforeEach public void setUp() { @@ -78,20 +71,4 @@ void deleteReservation_InDatabase_Success() { .statusCode(200) .body("reservations.size()", is(0)); } - - // TODO: 불필요한 테스트 삭제 - @Test - @DisplayName("데이터베이스 관련 로직을 컨트롤러에서 분리하였다.") - void layerRefactoring() { - boolean isJdbcTemplateInjected = false; - - for (Field field : reservationApiController.getClass().getDeclaredFields()) { - if (field.getType().equals(JdbcTemplate.class)) { - isJdbcTemplateInjected = true; - break; - } - } - - assertThat(isJdbcTemplateInjected).isFalse(); - } } From edf9e2fbf9234a098309553a2a2b63b564928102 Mon Sep 17 00:00:00 2001 From: Minjoo Date: Thu, 16 May 2024 16:02:51 +0900 Subject: [PATCH 28/42] =?UTF-8?q?refactor:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=86=8D=EB=8F=84=EB=A5=BC=20=EC=9C=84=ED=95=B4=20@DirtiesC?= =?UTF-8?q?ontext=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/BaseControllerTest.java | 3 ++- src/test/resources/clean_data.sql | 17 +++++++++++++++++ src/test/resources/test_data.sql | 16 ++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 src/test/resources/clean_data.sql create mode 100644 src/test/resources/test_data.sql diff --git a/src/test/java/roomescape/controller/BaseControllerTest.java b/src/test/java/roomescape/controller/BaseControllerTest.java index 6921e5594..e1d1ccdf9 100644 --- a/src/test/java/roomescape/controller/BaseControllerTest.java +++ b/src/test/java/roomescape/controller/BaseControllerTest.java @@ -6,9 +6,10 @@ import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.jdbc.Sql; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +@Sql(value = {"classpath:clean_data.sql", "classpath:test_data.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) public abstract class BaseControllerTest { @LocalServerPort diff --git a/src/test/resources/clean_data.sql b/src/test/resources/clean_data.sql new file mode 100644 index 000000000..ac4430a88 --- /dev/null +++ b/src/test/resources/clean_data.sql @@ -0,0 +1,17 @@ +DELETE +FROM reservation; +DELETE +FROM reservation_time; +DELETE +FROM theme; +DELETE +FROM member; + +ALTER TABLE reservation + ALTER COLUMN id RESTART WITH 1; +ALTER TABLE reservation_time + ALTER COLUMN id RESTART WITH 1; +ALTER TABLE theme + ALTER COLUMN id RESTART WITH 1; +ALTER TABLE member + ALTER COLUMN id RESTART WITH 1; diff --git a/src/test/resources/test_data.sql b/src/test/resources/test_data.sql new file mode 100644 index 000000000..f8166d62e --- /dev/null +++ b/src/test/resources/test_data.sql @@ -0,0 +1,16 @@ +INSERT INTO theme (name, description, thumbnail) +VALUES ('theme1', 'description1', 'thumbnail1'); +INSERT INTO theme (name, description, thumbnail) +VALUES ('theme2', 'description2', 'thumbnail2'); + +INSERT INTO reservation_time (start_at) +VALUES ('10:00'); +INSERT INTO reservation_time (start_at) +VALUES ('11:00'); + +INSERT INTO member (name, email, password, `role`) +VALUES ('testUser', 'user@naver.com', '1234', 'USER'); +INSERT INTO member (name, email, password, `role`) +VALUES ('testAdmin', 'admin@naver.com', '1234', 'ADMIN'); +INSERT INTO reservation (member_id, date, time_id, theme_id) +VALUES (1, CURRENT_DATE + INTERVAL '1' DAY, 1, 1); From 37065859bdfa7bcf36d01f431e451e3c4cc1f56c Mon Sep 17 00:00:00 2001 From: Minjoo Date: Thu, 16 May 2024 16:03:54 +0900 Subject: [PATCH 29/42] =?UTF-8?q?test:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/ReservationSaveRequestTest.java | 18 ------------------ .../service/dto/ThemeSaveRequestTest.java | 17 ----------------- 2 files changed, 35 deletions(-) delete mode 100644 src/test/java/roomescape/service/dto/ReservationSaveRequestTest.java delete mode 100644 src/test/java/roomescape/service/dto/ThemeSaveRequestTest.java diff --git a/src/test/java/roomescape/service/dto/ReservationSaveRequestTest.java b/src/test/java/roomescape/service/dto/ReservationSaveRequestTest.java deleted file mode 100644 index 723e5e467..000000000 --- a/src/test/java/roomescape/service/dto/ReservationSaveRequestTest.java +++ /dev/null @@ -1,18 +0,0 @@ -package roomescape.service.dto; - -import static org.assertj.core.api.Assertions.assertThatCode; - -import java.time.LocalDate; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import roomescape.service.dto.request.ReservationSaveRequest; - -class ReservationSaveRequestTest { - - @Test - @DisplayName("이름이 정상 입력될 경우 성공한다.") - void checkNameBlank_Success() { - assertThatCode(() -> new ReservationSaveRequest(LocalDate.now(), 1L, 1L)) - .doesNotThrowAnyException(); - } -} diff --git a/src/test/java/roomescape/service/dto/ThemeSaveRequestTest.java b/src/test/java/roomescape/service/dto/ThemeSaveRequestTest.java deleted file mode 100644 index 5d9a71e1c..000000000 --- a/src/test/java/roomescape/service/dto/ThemeSaveRequestTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package roomescape.service.dto; - -import static org.assertj.core.api.Assertions.assertThatCode; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import roomescape.service.dto.request.ThemeSaveRequest; - -public class ThemeSaveRequestTest { - - @Test - @DisplayName("테마 이름이 정상 입력될 경우 성공한다.") - void checkThemeNameBlank_Success() { - assertThatCode(() -> new ThemeSaveRequest("capy", "description", "thumbnail")) - .doesNotThrowAnyException(); - } -} From 20351f7023e9e2ca53f5485eaa54eb908c18a1b0 Mon Sep 17 00:00:00 2001 From: Minjoo Date: Thu, 16 May 2024 16:16:39 +0900 Subject: [PATCH 30/42] =?UTF-8?q?style:=20=EA=B0=9C=ED=96=89=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/java/roomescape/repository/ReservationRepository.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/roomescape/repository/ReservationRepository.java b/src/main/java/roomescape/repository/ReservationRepository.java index 9186827b5..55f73ddd5 100644 --- a/src/main/java/roomescape/repository/ReservationRepository.java +++ b/src/main/java/roomescape/repository/ReservationRepository.java @@ -8,6 +8,7 @@ @Repository public interface ReservationRepository extends JpaRepository { + boolean existsByThemeId(long themeId); List findByMemberIdAndThemeIdAndDateBetween(long memberId, long themeId, LocalDate dateFrom, LocalDate dateTo); From 69c992942ec7e32673fffc42344c00f98c1f399e Mon Sep 17 00:00:00 2001 From: Minjoo Date: Thu, 16 May 2024 17:01:22 +0900 Subject: [PATCH 31/42] =?UTF-8?q?refactor:=20createReservation=20->=20crea?= =?UTF-8?q?te=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=AA=85=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/api/ReservationApiController.java | 2 +- .../api/admin/AdminReservationApiController.java | 2 +- .../service/reservation/ReservationCreateService.java | 10 +++++----- .../reservation/ReservationCreateServiceTest.java | 7 +++---- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/main/java/roomescape/controller/api/ReservationApiController.java b/src/main/java/roomescape/controller/api/ReservationApiController.java index 57e916f6e..f3044f253 100644 --- a/src/main/java/roomescape/controller/api/ReservationApiController.java +++ b/src/main/java/roomescape/controller/api/ReservationApiController.java @@ -47,7 +47,7 @@ public ResponseEntity getUserReservations(@Authenticat @PostMapping("/reservations") public ResponseEntity addReservation(@RequestBody @Valid ReservationSaveRequest request, @AuthenticatedMember Member member) { - Reservation newReservation = reservationCreateService.createReservation(request, member); + Reservation newReservation = reservationCreateService.create(request, member); return ResponseEntity.created(URI.create("/reservations/" + newReservation.getId())) .body(new ReservationResponse(newReservation)); } diff --git a/src/main/java/roomescape/controller/api/admin/AdminReservationApiController.java b/src/main/java/roomescape/controller/api/admin/AdminReservationApiController.java index 7b0160c50..03d40712b 100644 --- a/src/main/java/roomescape/controller/api/admin/AdminReservationApiController.java +++ b/src/main/java/roomescape/controller/api/admin/AdminReservationApiController.java @@ -48,7 +48,7 @@ public ResponseEntity getSearchingReservations(@RequestPar @PostMapping("/admin/reservations") public ResponseEntity addReservation( @RequestBody @Valid ReservationAdminSaveRequest request) { - Reservation newReservation = reservationCreateService.createReservation(request); + Reservation newReservation = reservationCreateService.create(request); return ResponseEntity.created(URI.create("/admin/reservations/" + newReservation.getId())) .body(new ReservationResponse(newReservation)); } diff --git a/src/main/java/roomescape/service/reservation/ReservationCreateService.java b/src/main/java/roomescape/service/reservation/ReservationCreateService.java index d3436c414..0a9717287 100644 --- a/src/main/java/roomescape/service/reservation/ReservationCreateService.java +++ b/src/main/java/roomescape/service/reservation/ReservationCreateService.java @@ -32,25 +32,25 @@ public ReservationCreateService(ReservationRepository reservationRepository, this.memberRepository = memberRepository; } - public Reservation createReservation(ReservationAdminSaveRequest request) { + public Reservation create(ReservationAdminSaveRequest request) { Reservation reservation = request.toEntity( request, getReservationTime(request.timeId()), getTheme(request.themeId()), getMember(request.memberId())); - return createReservation(reservation); + return create(reservation); } - public Reservation createReservation(ReservationSaveRequest request, Member member) { + public Reservation create(ReservationSaveRequest request, Member member) { Reservation reservation = request.toEntity( request, getReservationTime(request.timeId()), getTheme(request.themeId()), member); - return createReservation(reservation); + return create(reservation); } - private Reservation createReservation(Reservation request) { + private Reservation create(Reservation request) { validateDateIsFuture(request.getDate(), request.getReservationTime()); validateAlreadyBooked(request.getDate(), request.getReservationTime().getId(), request.getTheme().getId()); return reservationRepository.save(request); diff --git a/src/test/java/roomescape/service/reservation/ReservationCreateServiceTest.java b/src/test/java/roomescape/service/reservation/ReservationCreateServiceTest.java index 5a161cac6..2bc647980 100644 --- a/src/test/java/roomescape/service/reservation/ReservationCreateServiceTest.java +++ b/src/test/java/roomescape/service/reservation/ReservationCreateServiceTest.java @@ -7,7 +7,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; import roomescape.domain.Member; import roomescape.domain.Role; import roomescape.service.BaseServiceTest; @@ -25,7 +24,7 @@ void checkDuplicateReservationTime_Success() { LocalDate.now().plusDays(1L), 2L, 2L); Member member = new Member(1L, "capy", "test@naver.com", "1234", Role.USER); - assertThatCode(() -> reservationCreateService.createReservation(request, member)) + assertThatCode(() -> reservationCreateService.create(request, member)) .doesNotThrowAnyException(); } @@ -36,7 +35,7 @@ void checkDuplicateReservationTime_Failure() { LocalDate.now().plusDays(1L), 1L, 1L); Member member = new Member("capy", "abc@gmail.com", "1234", Role.USER); - assertThatThrownBy(() -> reservationCreateService.createReservation(request, member)) + assertThatThrownBy(() -> reservationCreateService.create(request, member)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("해당 시간에 이미 예약된 테마입니다."); } @@ -48,7 +47,7 @@ void checkReservationDateTimeIsFuture_Failure() { LocalDate.now().minusDays(1L), 2L, 2L); Member member = new Member("capy", "abc@gmail.com", "1234", Role.USER); - assertThatThrownBy(() -> reservationCreateService.createReservation(request, member)) + assertThatThrownBy(() -> reservationCreateService.create(request, member)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("지나간 날짜와 시간에 대한 예약 생성은 불가능합니다."); } From a06f5afd30cd70ca670c83793dbcdd2c6add5bb8 Mon Sep 17 00:00:00 2001 From: Minjoo Date: Fri, 17 May 2024 13:47:28 +0900 Subject: [PATCH 32/42] =?UTF-8?q?refactor:=20Member=EC=9D=98=20name,=20ema?= =?UTF-8?q?il,=20password=20=ED=8F=AC=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/api/AuthApiController.java | 2 +- src/main/java/roomescape/domain/Member.java | 26 ++++++++---- .../roomescape/domain/member/MemberEmail.java | 41 +++++++++++++++++++ .../roomescape/domain/member/MemberName.java | 29 +++++++++++++ .../domain/member/MemberPassword.java | 29 +++++++++++++ .../repository/MemberRepository.java | 4 +- .../roomescape/service/auth/AuthService.java | 4 +- .../member/MemberIdAndNameResponses.java | 2 +- .../reservation/ReservationResponse.java | 2 +- .../ReservationCreateServiceTest.java | 19 +++++++-- 10 files changed, 142 insertions(+), 16 deletions(-) create mode 100644 src/main/java/roomescape/domain/member/MemberEmail.java create mode 100644 src/main/java/roomescape/domain/member/MemberName.java create mode 100644 src/main/java/roomescape/domain/member/MemberPassword.java diff --git a/src/main/java/roomescape/controller/api/AuthApiController.java b/src/main/java/roomescape/controller/api/AuthApiController.java index bbebb7777..8b9c6cc40 100644 --- a/src/main/java/roomescape/controller/api/AuthApiController.java +++ b/src/main/java/roomescape/controller/api/AuthApiController.java @@ -25,7 +25,7 @@ public AuthApiController(AuthService authService) { @GetMapping("/login/check") public ResponseEntity getMemberLoginInfo(@AuthenticatedMember Member member) { - return ResponseEntity.ok(new MemberIdAndNameResponse(member.getId(), member.getName())); + return ResponseEntity.ok(new MemberIdAndNameResponse(member.getId(), member.getName().getValue())); } @PostMapping("/login") diff --git a/src/main/java/roomescape/domain/Member.java b/src/main/java/roomescape/domain/Member.java index b10280eb7..47af882d3 100644 --- a/src/main/java/roomescape/domain/Member.java +++ b/src/main/java/roomescape/domain/Member.java @@ -1,5 +1,6 @@ package roomescape.domain; +import jakarta.persistence.Embedded; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; @@ -7,6 +8,9 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import java.util.Objects; +import roomescape.domain.member.MemberEmail; +import roomescape.domain.member.MemberName; +import roomescape.domain.member.MemberPassword; @Entity public class Member { @@ -14,14 +18,20 @@ public class Member { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - private String name; - private String email; - private String password; + + @Embedded + private MemberName name; + + @Embedded + private MemberEmail email; + + @Embedded + private MemberPassword password; @Enumerated(value = EnumType.STRING) private Role role; - public Member(Long id, String name, String email, String password, Role role) { + public Member(Long id, MemberName name, MemberEmail email, MemberPassword password, Role role) { this.id = id; this.name = name; this.email = email; @@ -29,7 +39,7 @@ public Member(Long id, String name, String email, String password, Role role) { this.role = role; } - public Member(String name, String email, String password, Role role) { + public Member(MemberName name, MemberEmail email, MemberPassword password, Role role) { this(null, name, email, password, role); } @@ -41,15 +51,15 @@ public Long getId() { return id; } - public String getName() { + public MemberName getName() { return name; } - public String getEmail() { + public MemberEmail getEmail() { return email; } - public String getPassword() { + public MemberPassword getPassword() { return password; } diff --git a/src/main/java/roomescape/domain/member/MemberEmail.java b/src/main/java/roomescape/domain/member/MemberEmail.java new file mode 100644 index 000000000..029af79a7 --- /dev/null +++ b/src/main/java/roomescape/domain/member/MemberEmail.java @@ -0,0 +1,41 @@ +package roomescape.domain.member; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import java.util.regex.Pattern; + +@Embeddable +public class MemberEmail { + + private static final String EMAIL_REGEX = + "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}$"; + private static final Pattern EMAIL_PATTERN = Pattern.compile(EMAIL_REGEX); + + @Column(name = "email") + private String value; + + public MemberEmail() { + } + + public MemberEmail(String value) { + validateNullOrBlank(value); + validateEmailPattern(value); + this.value = value; + } + + private void validateNullOrBlank(String value) { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException("이메일을 입력해주세요."); + } + } + + private void validateEmailPattern(String value) { + if (!EMAIL_PATTERN.matcher(value).matches()) { + throw new IllegalArgumentException("올바른 이메일 형식이 아닙니다."); + } + } + + public String getValue() { + return value; + } +} diff --git a/src/main/java/roomescape/domain/member/MemberName.java b/src/main/java/roomescape/domain/member/MemberName.java new file mode 100644 index 000000000..3af3b43a6 --- /dev/null +++ b/src/main/java/roomescape/domain/member/MemberName.java @@ -0,0 +1,29 @@ +package roomescape.domain.member; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; + +@Embeddable +public class MemberName { + + @Column(name = "name") + private String value; + + public MemberName() { + } + + public MemberName(String value) { + validateNullOrBlank(value); + this.value = value; + } + + private void validateNullOrBlank(String value) { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException("이름을 입력해주세요."); + } + } + + public String getValue() { + return value; + } +} diff --git a/src/main/java/roomescape/domain/member/MemberPassword.java b/src/main/java/roomescape/domain/member/MemberPassword.java new file mode 100644 index 000000000..d1bc19d01 --- /dev/null +++ b/src/main/java/roomescape/domain/member/MemberPassword.java @@ -0,0 +1,29 @@ +package roomescape.domain.member; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; + +@Embeddable +public class MemberPassword { + + @Column(name = "password") + private String value; + + public MemberPassword() { + } + + public MemberPassword(String value) { + validateNullOrBlank(value); + this.value = value; + } + + public static void validateNullOrBlank(String value) { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException("비밀번호를 입력해주세요."); + } + } + + public String getValue() { + return value; + } +} diff --git a/src/main/java/roomescape/repository/MemberRepository.java b/src/main/java/roomescape/repository/MemberRepository.java index 9de7aa59d..14d938e22 100644 --- a/src/main/java/roomescape/repository/MemberRepository.java +++ b/src/main/java/roomescape/repository/MemberRepository.java @@ -4,9 +4,11 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import roomescape.domain.Member; +import roomescape.domain.member.MemberEmail; +import roomescape.domain.member.MemberPassword; @Repository public interface MemberRepository extends JpaRepository { - Optional findByEmailAndPassword(String email, String password); + Optional findByEmailAndPassword(MemberEmail email, MemberPassword password); } diff --git a/src/main/java/roomescape/service/auth/AuthService.java b/src/main/java/roomescape/service/auth/AuthService.java index e26ac56e3..275565ddd 100644 --- a/src/main/java/roomescape/service/auth/AuthService.java +++ b/src/main/java/roomescape/service/auth/AuthService.java @@ -2,6 +2,8 @@ import org.springframework.stereotype.Service; import roomescape.domain.Member; +import roomescape.domain.member.MemberEmail; +import roomescape.domain.member.MemberPassword; import roomescape.exception.AuthenticationException; import roomescape.repository.MemberRepository; import roomescape.service.dto.request.LoginRequest; @@ -24,7 +26,7 @@ public Member findMemberByToken(String token) { } public String login(LoginRequest request) { - Member member = memberRepository.findByEmailAndPassword(request.email(), request.password()) + Member member = memberRepository.findByEmailAndPassword(new MemberEmail(request.email()), new MemberPassword(request.password())) .orElseThrow(() -> new AuthenticationException("잘못된 로그인 정보입니다.")); return tokenProvider.generateAccessToken(member.getId()); diff --git a/src/main/java/roomescape/service/dto/response/member/MemberIdAndNameResponses.java b/src/main/java/roomescape/service/dto/response/member/MemberIdAndNameResponses.java index 02ee30677..d3f8e6814 100644 --- a/src/main/java/roomescape/service/dto/response/member/MemberIdAndNameResponses.java +++ b/src/main/java/roomescape/service/dto/response/member/MemberIdAndNameResponses.java @@ -7,7 +7,7 @@ public record MemberIdAndNameResponses(List members) { public static MemberIdAndNameResponses from(List members) { List responses = members.stream() - .map(member -> new MemberIdAndNameResponse(member.getId(), member.getName())) + .map(member -> new MemberIdAndNameResponse(member.getId(), member.getName().getValue())) .toList(); return new MemberIdAndNameResponses(responses); } diff --git a/src/main/java/roomescape/service/dto/response/reservation/ReservationResponse.java b/src/main/java/roomescape/service/dto/response/reservation/ReservationResponse.java index c4a4b0709..3527582ca 100644 --- a/src/main/java/roomescape/service/dto/response/reservation/ReservationResponse.java +++ b/src/main/java/roomescape/service/dto/response/reservation/ReservationResponse.java @@ -14,7 +14,7 @@ public record ReservationResponse(Long id, public ReservationResponse(Reservation reservation) { this(reservation.getId(), - new MemberIdAndNameResponse(reservation.getMember().getId(), reservation.getMember().getName()), + new MemberIdAndNameResponse(reservation.getMember().getId(), reservation.getMember().getName().getValue()), reservation.getDate(), new ReservationTimeResponse(reservation.getReservationTime()), new ThemeResponse(reservation.getTheme())); diff --git a/src/test/java/roomescape/service/reservation/ReservationCreateServiceTest.java b/src/test/java/roomescape/service/reservation/ReservationCreateServiceTest.java index 2bc647980..be9753fe7 100644 --- a/src/test/java/roomescape/service/reservation/ReservationCreateServiceTest.java +++ b/src/test/java/roomescape/service/reservation/ReservationCreateServiceTest.java @@ -8,6 +8,9 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import roomescape.domain.Member; +import roomescape.domain.member.MemberEmail; +import roomescape.domain.member.MemberName; +import roomescape.domain.member.MemberPassword; import roomescape.domain.Role; import roomescape.service.BaseServiceTest; import roomescape.service.dto.request.ReservationSaveRequest; @@ -22,7 +25,11 @@ class ReservationCreateServiceTest extends BaseServiceTest { void checkDuplicateReservationTime_Success() { ReservationSaveRequest request = new ReservationSaveRequest( LocalDate.now().plusDays(1L), 2L, 2L); - Member member = new Member(1L, "capy", "test@naver.com", "1234", Role.USER); + Member member = new Member( + 1L, + new MemberName("capy"), + new MemberEmail("test@naver.com"), + new MemberPassword("1234"), Role.USER); assertThatCode(() -> reservationCreateService.create(request, member)) .doesNotThrowAnyException(); @@ -33,7 +40,10 @@ void checkDuplicateReservationTime_Success() { void checkDuplicateReservationTime_Failure() { ReservationSaveRequest request = new ReservationSaveRequest( LocalDate.now().plusDays(1L), 1L, 1L); - Member member = new Member("capy", "abc@gmail.com", "1234", Role.USER); + Member member = new Member( + new MemberName("capy"), + new MemberEmail("test@naver.com"), + new MemberPassword("1234"), Role.USER); assertThatThrownBy(() -> reservationCreateService.create(request, member)) .isInstanceOf(IllegalArgumentException.class) @@ -45,7 +55,10 @@ void checkDuplicateReservationTime_Failure() { void checkReservationDateTimeIsFuture_Failure() { ReservationSaveRequest request = new ReservationSaveRequest( LocalDate.now().minusDays(1L), 2L, 2L); - Member member = new Member("capy", "abc@gmail.com", "1234", Role.USER); + Member member = new Member( + new MemberName("capy"), + new MemberEmail("test@naver.com"), + new MemberPassword("1234"), Role.USER); assertThatThrownBy(() -> reservationCreateService.create(request, member)) .isInstanceOf(IllegalArgumentException.class) From 9a5323af48eb393cbad1d89ba3123000e4ede21c Mon Sep 17 00:00:00 2001 From: Minjoo Date: Fri, 17 May 2024 13:47:58 +0900 Subject: [PATCH 33/42] =?UTF-8?q?refactor:=20Member,=20Role=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/roomescape/auth/AdminAuthHandlerInterceptor.java | 4 ++-- .../roomescape/auth/AuthenticatedMemberArgumentResolver.java | 2 +- .../java/roomescape/controller/api/AuthApiController.java | 2 +- .../roomescape/controller/api/ReservationApiController.java | 2 +- .../controller/api/admin/AdminMemberApiController.java | 2 +- src/main/java/roomescape/domain/Reservation.java | 1 + src/main/java/roomescape/domain/{ => member}/Member.java | 5 +---- src/main/java/roomescape/domain/{ => member}/Role.java | 2 +- src/main/java/roomescape/repository/MemberRepository.java | 2 +- src/main/java/roomescape/service/auth/AuthService.java | 2 +- .../service/dto/request/ReservationAdminSaveRequest.java | 2 +- .../service/dto/request/ReservationSaveRequest.java | 2 +- .../dto/response/member/MemberIdAndNameResponses.java | 2 +- src/main/java/roomescape/service/member/MemberService.java | 2 +- .../service/reservation/ReservationCreateService.java | 2 +- .../service/reservation/ReservationCreateServiceTest.java | 4 ++-- 16 files changed, 18 insertions(+), 20 deletions(-) rename src/main/java/roomescape/domain/{ => member}/Member.java (92%) rename src/main/java/roomescape/domain/{ => member}/Role.java (59%) diff --git a/src/main/java/roomescape/auth/AdminAuthHandlerInterceptor.java b/src/main/java/roomescape/auth/AdminAuthHandlerInterceptor.java index 1d0237a5d..69a11758a 100644 --- a/src/main/java/roomescape/auth/AdminAuthHandlerInterceptor.java +++ b/src/main/java/roomescape/auth/AdminAuthHandlerInterceptor.java @@ -5,8 +5,8 @@ import jakarta.servlet.http.HttpServletResponse; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; -import roomescape.domain.Member; -import roomescape.domain.Role; +import roomescape.domain.member.Member; +import roomescape.domain.member.Role; import roomescape.exception.AuthenticationException; import roomescape.service.auth.AuthService; import roomescape.service.auth.TokenProvider; diff --git a/src/main/java/roomescape/auth/AuthenticatedMemberArgumentResolver.java b/src/main/java/roomescape/auth/AuthenticatedMemberArgumentResolver.java index 46cf57cda..93a8ee021 100644 --- a/src/main/java/roomescape/auth/AuthenticatedMemberArgumentResolver.java +++ b/src/main/java/roomescape/auth/AuthenticatedMemberArgumentResolver.java @@ -8,7 +8,7 @@ import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; -import roomescape.domain.Member; +import roomescape.domain.member.Member; import roomescape.exception.AuthenticationException; import roomescape.service.auth.AuthService; import roomescape.service.auth.TokenProvider; diff --git a/src/main/java/roomescape/controller/api/AuthApiController.java b/src/main/java/roomescape/controller/api/AuthApiController.java index 8b9c6cc40..9167023f1 100644 --- a/src/main/java/roomescape/controller/api/AuthApiController.java +++ b/src/main/java/roomescape/controller/api/AuthApiController.java @@ -9,7 +9,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import roomescape.auth.AuthenticatedMember; -import roomescape.domain.Member; +import roomescape.domain.member.Member; import roomescape.service.auth.AuthService; import roomescape.service.dto.request.LoginRequest; import roomescape.service.dto.response.member.MemberIdAndNameResponse; diff --git a/src/main/java/roomescape/controller/api/ReservationApiController.java b/src/main/java/roomescape/controller/api/ReservationApiController.java index f3044f253..5c638286e 100644 --- a/src/main/java/roomescape/controller/api/ReservationApiController.java +++ b/src/main/java/roomescape/controller/api/ReservationApiController.java @@ -13,7 +13,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import roomescape.auth.AuthenticatedMember; -import roomescape.domain.Member; +import roomescape.domain.member.Member; import roomescape.domain.Reservation; import roomescape.service.dto.request.ReservationSaveRequest; import roomescape.service.dto.response.reservation.ReservationResponse; diff --git a/src/main/java/roomescape/controller/api/admin/AdminMemberApiController.java b/src/main/java/roomescape/controller/api/admin/AdminMemberApiController.java index 53dec5ec9..2c038b5c2 100644 --- a/src/main/java/roomescape/controller/api/admin/AdminMemberApiController.java +++ b/src/main/java/roomescape/controller/api/admin/AdminMemberApiController.java @@ -4,7 +4,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; -import roomescape.domain.Member; +import roomescape.domain.member.Member; import roomescape.service.dto.response.member.MemberIdAndNameResponses; import roomescape.service.member.MemberService; diff --git a/src/main/java/roomescape/domain/Reservation.java b/src/main/java/roomescape/domain/Reservation.java index 1f17f4685..536bf73c1 100644 --- a/src/main/java/roomescape/domain/Reservation.java +++ b/src/main/java/roomescape/domain/Reservation.java @@ -7,6 +7,7 @@ import jakarta.persistence.ManyToOne; import java.time.LocalDate; import java.util.Objects; +import roomescape.domain.member.Member; @Entity public class Reservation { diff --git a/src/main/java/roomescape/domain/Member.java b/src/main/java/roomescape/domain/member/Member.java similarity index 92% rename from src/main/java/roomescape/domain/Member.java rename to src/main/java/roomescape/domain/member/Member.java index 47af882d3..a467c5a03 100644 --- a/src/main/java/roomescape/domain/Member.java +++ b/src/main/java/roomescape/domain/member/Member.java @@ -1,4 +1,4 @@ -package roomescape.domain; +package roomescape.domain.member; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; @@ -8,9 +8,6 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import java.util.Objects; -import roomescape.domain.member.MemberEmail; -import roomescape.domain.member.MemberName; -import roomescape.domain.member.MemberPassword; @Entity public class Member { diff --git a/src/main/java/roomescape/domain/Role.java b/src/main/java/roomescape/domain/member/Role.java similarity index 59% rename from src/main/java/roomescape/domain/Role.java rename to src/main/java/roomescape/domain/member/Role.java index e54a3097c..8df97e61e 100644 --- a/src/main/java/roomescape/domain/Role.java +++ b/src/main/java/roomescape/domain/member/Role.java @@ -1,4 +1,4 @@ -package roomescape.domain; +package roomescape.domain.member; public enum Role { diff --git a/src/main/java/roomescape/repository/MemberRepository.java b/src/main/java/roomescape/repository/MemberRepository.java index 14d938e22..701f1567d 100644 --- a/src/main/java/roomescape/repository/MemberRepository.java +++ b/src/main/java/roomescape/repository/MemberRepository.java @@ -3,7 +3,7 @@ import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -import roomescape.domain.Member; +import roomescape.domain.member.Member; import roomescape.domain.member.MemberEmail; import roomescape.domain.member.MemberPassword; diff --git a/src/main/java/roomescape/service/auth/AuthService.java b/src/main/java/roomescape/service/auth/AuthService.java index 275565ddd..e7868d3f6 100644 --- a/src/main/java/roomescape/service/auth/AuthService.java +++ b/src/main/java/roomescape/service/auth/AuthService.java @@ -1,7 +1,7 @@ package roomescape.service.auth; import org.springframework.stereotype.Service; -import roomescape.domain.Member; +import roomescape.domain.member.Member; import roomescape.domain.member.MemberEmail; import roomescape.domain.member.MemberPassword; import roomescape.exception.AuthenticationException; diff --git a/src/main/java/roomescape/service/dto/request/ReservationAdminSaveRequest.java b/src/main/java/roomescape/service/dto/request/ReservationAdminSaveRequest.java index 803198ed9..441ad16c0 100644 --- a/src/main/java/roomescape/service/dto/request/ReservationAdminSaveRequest.java +++ b/src/main/java/roomescape/service/dto/request/ReservationAdminSaveRequest.java @@ -2,7 +2,7 @@ import jakarta.validation.constraints.NotNull; import java.time.LocalDate; -import roomescape.domain.Member; +import roomescape.domain.member.Member; import roomescape.domain.Reservation; import roomescape.domain.ReservationTime; import roomescape.domain.Theme; diff --git a/src/main/java/roomescape/service/dto/request/ReservationSaveRequest.java b/src/main/java/roomescape/service/dto/request/ReservationSaveRequest.java index 6e816959b..281d8ae0b 100644 --- a/src/main/java/roomescape/service/dto/request/ReservationSaveRequest.java +++ b/src/main/java/roomescape/service/dto/request/ReservationSaveRequest.java @@ -2,7 +2,7 @@ import jakarta.validation.constraints.NotNull; import java.time.LocalDate; -import roomescape.domain.Member; +import roomescape.domain.member.Member; import roomescape.domain.Reservation; import roomescape.domain.ReservationTime; import roomescape.domain.Theme; diff --git a/src/main/java/roomescape/service/dto/response/member/MemberIdAndNameResponses.java b/src/main/java/roomescape/service/dto/response/member/MemberIdAndNameResponses.java index d3f8e6814..679c12008 100644 --- a/src/main/java/roomescape/service/dto/response/member/MemberIdAndNameResponses.java +++ b/src/main/java/roomescape/service/dto/response/member/MemberIdAndNameResponses.java @@ -1,7 +1,7 @@ package roomescape.service.dto.response.member; import java.util.List; -import roomescape.domain.Member; +import roomescape.domain.member.Member; public record MemberIdAndNameResponses(List members) { diff --git a/src/main/java/roomescape/service/member/MemberService.java b/src/main/java/roomescape/service/member/MemberService.java index 3c4056ae4..167e9e11a 100644 --- a/src/main/java/roomescape/service/member/MemberService.java +++ b/src/main/java/roomescape/service/member/MemberService.java @@ -2,7 +2,7 @@ import java.util.List; import org.springframework.stereotype.Service; -import roomescape.domain.Member; +import roomescape.domain.member.Member; import roomescape.repository.MemberRepository; @Service diff --git a/src/main/java/roomescape/service/reservation/ReservationCreateService.java b/src/main/java/roomescape/service/reservation/ReservationCreateService.java index 0a9717287..995a40a9b 100644 --- a/src/main/java/roomescape/service/reservation/ReservationCreateService.java +++ b/src/main/java/roomescape/service/reservation/ReservationCreateService.java @@ -3,7 +3,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import org.springframework.stereotype.Service; -import roomescape.domain.Member; +import roomescape.domain.member.Member; import roomescape.domain.Reservation; import roomescape.domain.ReservationTime; import roomescape.domain.Theme; diff --git a/src/test/java/roomescape/service/reservation/ReservationCreateServiceTest.java b/src/test/java/roomescape/service/reservation/ReservationCreateServiceTest.java index be9753fe7..48045325a 100644 --- a/src/test/java/roomescape/service/reservation/ReservationCreateServiceTest.java +++ b/src/test/java/roomescape/service/reservation/ReservationCreateServiceTest.java @@ -7,11 +7,11 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import roomescape.domain.Member; +import roomescape.domain.member.Member; import roomescape.domain.member.MemberEmail; import roomescape.domain.member.MemberName; import roomescape.domain.member.MemberPassword; -import roomescape.domain.Role; +import roomescape.domain.member.Role; import roomescape.service.BaseServiceTest; import roomescape.service.dto.request.ReservationSaveRequest; From bfcac76f420c338587e240e8ef31af65c4165f8c Mon Sep 17 00:00:00 2001 From: Minjoo Date: Fri, 17 May 2024 15:18:41 +0900 Subject: [PATCH 34/42] =?UTF-8?q?refactor:=20vo=20equals=20&=20hashCode=20?= =?UTF-8?q?=EC=9E=AC=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roomescape/domain/member/MemberEmail.java | 18 ++++++++++++++++++ .../roomescape/domain/member/MemberName.java | 18 ++++++++++++++++++ .../domain/member/MemberPassword.java | 18 ++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/src/main/java/roomescape/domain/member/MemberEmail.java b/src/main/java/roomescape/domain/member/MemberEmail.java index 029af79a7..bef511529 100644 --- a/src/main/java/roomescape/domain/member/MemberEmail.java +++ b/src/main/java/roomescape/domain/member/MemberEmail.java @@ -2,6 +2,7 @@ import jakarta.persistence.Column; import jakarta.persistence.Embeddable; +import java.util.Objects; import java.util.regex.Pattern; @Embeddable @@ -38,4 +39,21 @@ private void validateEmailPattern(String value) { public String getValue() { return value; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MemberEmail that = (MemberEmail) o; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } } diff --git a/src/main/java/roomescape/domain/member/MemberName.java b/src/main/java/roomescape/domain/member/MemberName.java index 3af3b43a6..4ee309061 100644 --- a/src/main/java/roomescape/domain/member/MemberName.java +++ b/src/main/java/roomescape/domain/member/MemberName.java @@ -2,6 +2,7 @@ import jakarta.persistence.Column; import jakarta.persistence.Embeddable; +import java.util.Objects; @Embeddable public class MemberName { @@ -26,4 +27,21 @@ private void validateNullOrBlank(String value) { public String getValue() { return value; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MemberName that = (MemberName) o; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } } diff --git a/src/main/java/roomescape/domain/member/MemberPassword.java b/src/main/java/roomescape/domain/member/MemberPassword.java index d1bc19d01..0effd0717 100644 --- a/src/main/java/roomescape/domain/member/MemberPassword.java +++ b/src/main/java/roomescape/domain/member/MemberPassword.java @@ -2,6 +2,7 @@ import jakarta.persistence.Column; import jakarta.persistence.Embeddable; +import java.util.Objects; @Embeddable public class MemberPassword { @@ -26,4 +27,21 @@ public static void validateNullOrBlank(String value) { public String getValue() { return value; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MemberPassword that = (MemberPassword) o; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } } From a59e0a4d64f16db341b6284560420aa0880ed593 Mon Sep 17 00:00:00 2001 From: Minjoo Date: Sat, 18 May 2024 21:16:02 +0900 Subject: [PATCH 35/42] =?UTF-8?q?refactor:=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EA=B0=9D=EC=B2=B4=20=EC=82=AC=EC=9A=A9=20?= =?UTF-8?q?=EB=B0=8F=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=EC=95=A0=ED=94=8C?= =?UTF-8?q?=EB=A6=AC=EC=BC=80=EC=9D=B4=EC=85=98=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EC=B6=94=EC=83=81=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/ApiExceptionResponse.java | 6 +++ .../exception/AuthenticationException.java | 6 ++- .../exception/GlobalExceptionHandler.java | 41 ++++++++++++------- .../exception/RoomescapeException.java | 17 ++++++++ 4 files changed, 54 insertions(+), 16 deletions(-) create mode 100644 src/main/java/roomescape/exception/ApiExceptionResponse.java create mode 100644 src/main/java/roomescape/exception/RoomescapeException.java diff --git a/src/main/java/roomescape/exception/ApiExceptionResponse.java b/src/main/java/roomescape/exception/ApiExceptionResponse.java new file mode 100644 index 000000000..08f75e426 --- /dev/null +++ b/src/main/java/roomescape/exception/ApiExceptionResponse.java @@ -0,0 +1,6 @@ +package roomescape.exception; + +import org.springframework.http.HttpStatus; + +public record ApiExceptionResponse(HttpStatus status, String message) { +} diff --git a/src/main/java/roomescape/exception/AuthenticationException.java b/src/main/java/roomescape/exception/AuthenticationException.java index d7cc72c17..3a5493df7 100644 --- a/src/main/java/roomescape/exception/AuthenticationException.java +++ b/src/main/java/roomescape/exception/AuthenticationException.java @@ -1,8 +1,10 @@ package roomescape.exception; -public class AuthenticationException extends RuntimeException { +import org.springframework.http.HttpStatus; + +public class AuthenticationException extends RoomescapeException { public AuthenticationException(String message) { - super(message); + super(message, HttpStatus.UNAUTHORIZED); } } diff --git a/src/main/java/roomescape/exception/GlobalExceptionHandler.java b/src/main/java/roomescape/exception/GlobalExceptionHandler.java index fd9e8f509..705baed19 100644 --- a/src/main/java/roomescape/exception/GlobalExceptionHandler.java +++ b/src/main/java/roomescape/exception/GlobalExceptionHandler.java @@ -1,9 +1,12 @@ package roomescape.exception; import jakarta.validation.ConstraintViolationException; +import java.util.HashMap; +import java.util.Map; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -11,34 +14,44 @@ @RestControllerAdvice public class GlobalExceptionHandler { - @ExceptionHandler(value = IllegalArgumentException.class) - ResponseEntity handleIllegalArgumentException(IllegalArgumentException ex) { - return ResponseEntity.badRequest().body(ex.getMessage()); + @ExceptionHandler(IllegalArgumentException.class) + ResponseEntity handleIllegalArgumentException(IllegalArgumentException ex) { + return ResponseEntity.badRequest() + .body(new ApiExceptionResponse(HttpStatus.BAD_REQUEST, ex.getMessage())); } @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) { - return new ResponseEntity<>( - ex.getBindingResult().getFieldErrors().get(0).getDefaultMessage(), - HttpStatus.BAD_REQUEST); + public ResponseEntity> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) { + Map errors = new HashMap<>(); + ex.getBindingResult().getAllErrors() + .forEach((error) -> { + String fieldName = ((FieldError) error).getField(); + String errorMessage = error.getDefaultMessage(); + errors.put(fieldName, errorMessage); + }); + return ResponseEntity.badRequest() + .body(errors); } @ExceptionHandler(ConstraintViolationException.class) - public ResponseEntity handleMethodConstraintViolationException(ConstraintViolationException ex) { - return ResponseEntity.badRequest().body(ex.getMessage()); + public ResponseEntity handleMethodConstraintViolationException(ConstraintViolationException ex) { + return ResponseEntity.badRequest() + .body(new ApiExceptionResponse(HttpStatus.BAD_REQUEST, ex.getMessage())); } @ExceptionHandler(HttpMessageNotReadableException.class) - public ResponseEntity handleHttpMessageNotReadableException(HttpMessageNotReadableException ex) { - return ResponseEntity.badRequest().body(ex.getMessage()); + public ResponseEntity handleHttpMessageNotReadableException(HttpMessageNotReadableException ex) { + return ResponseEntity.badRequest() + .body(new ApiExceptionResponse(HttpStatus.BAD_REQUEST, ex.getMessage())); } @ExceptionHandler(AuthenticationException.class) - public ResponseEntity handleAuthenticationException(AuthenticationException ex) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ex.getMessage()); + public ResponseEntity handleAuthenticationException(AuthenticationException ex) { + return ResponseEntity.status(ex.getHttpStatus()) + .body(new ApiExceptionResponse(ex.getHttpStatus(), ex.getMessage())); } - @ExceptionHandler(value = RuntimeException.class) + @ExceptionHandler(RuntimeException.class) ResponseEntity handleRuntimeException() { return ResponseEntity.internalServerError() .body("서버에서 예기치 못한 오류가 발생했습니다. 문제가 지속되는 경우 관리자에게 문의해주세요."); diff --git a/src/main/java/roomescape/exception/RoomescapeException.java b/src/main/java/roomescape/exception/RoomescapeException.java new file mode 100644 index 000000000..3c88cea60 --- /dev/null +++ b/src/main/java/roomescape/exception/RoomescapeException.java @@ -0,0 +1,17 @@ +package roomescape.exception; + +import org.springframework.http.HttpStatus; + +public abstract class RoomescapeException extends RuntimeException { + + private final HttpStatus httpStatus; + + protected RoomescapeException(String message, HttpStatus httpStatus) { + super(message); + this.httpStatus = httpStatus; + } + + public HttpStatus getHttpStatus() { + return httpStatus; + } +} From 92b5148d61bc9bb3699d0926d247ddb16dad86df Mon Sep 17 00:00:00 2001 From: Minjoo Date: Sat, 18 May 2024 21:24:09 +0900 Subject: [PATCH 36/42] =?UTF-8?q?refactor:=20IllegalArgumentException=20->?= =?UTF-8?q?=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=EC=98=88=EC=99=B8=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/roomescape/domain/member/MemberEmail.java | 5 +++-- .../java/roomescape/domain/member/MemberName.java | 3 ++- .../roomescape/domain/member/MemberPassword.java | 3 ++- .../exception/GlobalExceptionHandler.java | 8 ++++---- .../exception/InvalidRequestException.java | 10 ++++++++++ .../reservation/ReservationCreateService.java | 13 +++++++------ .../reservation/ReservationDeleteService.java | 3 ++- .../ReservationTimeCreateService.java | 3 ++- .../ReservationTimeDeleteService.java | 5 +++-- .../service/theme/ThemeDeleteService.java | 5 +++-- .../reservation/ReservationCreateServiceTest.java | 5 +++-- .../ReservationTimeCreateServiceTest.java | 3 ++- .../ReservationTimeDeleteServiceTest.java | 3 ++- .../service/theme/ThemeDeleteServiceTest.java | 3 ++- 14 files changed, 47 insertions(+), 25 deletions(-) create mode 100644 src/main/java/roomescape/exception/InvalidRequestException.java diff --git a/src/main/java/roomescape/domain/member/MemberEmail.java b/src/main/java/roomescape/domain/member/MemberEmail.java index bef511529..03c343d6b 100644 --- a/src/main/java/roomescape/domain/member/MemberEmail.java +++ b/src/main/java/roomescape/domain/member/MemberEmail.java @@ -4,6 +4,7 @@ import jakarta.persistence.Embeddable; import java.util.Objects; import java.util.regex.Pattern; +import roomescape.exception.InvalidRequestException; @Embeddable public class MemberEmail { @@ -26,13 +27,13 @@ public MemberEmail(String value) { private void validateNullOrBlank(String value) { if (value == null || value.isBlank()) { - throw new IllegalArgumentException("이메일을 입력해주세요."); + throw new InvalidRequestException("이메일을 입력해주세요."); } } private void validateEmailPattern(String value) { if (!EMAIL_PATTERN.matcher(value).matches()) { - throw new IllegalArgumentException("올바른 이메일 형식이 아닙니다."); + throw new InvalidRequestException("올바른 이메일 형식이 아닙니다."); } } diff --git a/src/main/java/roomescape/domain/member/MemberName.java b/src/main/java/roomescape/domain/member/MemberName.java index 4ee309061..c40887dea 100644 --- a/src/main/java/roomescape/domain/member/MemberName.java +++ b/src/main/java/roomescape/domain/member/MemberName.java @@ -3,6 +3,7 @@ import jakarta.persistence.Column; import jakarta.persistence.Embeddable; import java.util.Objects; +import roomescape.exception.InvalidRequestException; @Embeddable public class MemberName { @@ -20,7 +21,7 @@ public MemberName(String value) { private void validateNullOrBlank(String value) { if (value == null || value.isBlank()) { - throw new IllegalArgumentException("이름을 입력해주세요."); + throw new InvalidRequestException("이름을 입력해주세요."); } } diff --git a/src/main/java/roomescape/domain/member/MemberPassword.java b/src/main/java/roomescape/domain/member/MemberPassword.java index 0effd0717..8d6f76d84 100644 --- a/src/main/java/roomescape/domain/member/MemberPassword.java +++ b/src/main/java/roomescape/domain/member/MemberPassword.java @@ -3,6 +3,7 @@ import jakarta.persistence.Column; import jakarta.persistence.Embeddable; import java.util.Objects; +import roomescape.exception.InvalidRequestException; @Embeddable public class MemberPassword { @@ -20,7 +21,7 @@ public MemberPassword(String value) { public static void validateNullOrBlank(String value) { if (value == null || value.isBlank()) { - throw new IllegalArgumentException("비밀번호를 입력해주세요."); + throw new InvalidRequestException("비밀번호를 입력해주세요."); } } diff --git a/src/main/java/roomescape/exception/GlobalExceptionHandler.java b/src/main/java/roomescape/exception/GlobalExceptionHandler.java index 705baed19..1cae9b35b 100644 --- a/src/main/java/roomescape/exception/GlobalExceptionHandler.java +++ b/src/main/java/roomescape/exception/GlobalExceptionHandler.java @@ -14,10 +14,10 @@ @RestControllerAdvice public class GlobalExceptionHandler { - @ExceptionHandler(IllegalArgumentException.class) - ResponseEntity handleIllegalArgumentException(IllegalArgumentException ex) { - return ResponseEntity.badRequest() - .body(new ApiExceptionResponse(HttpStatus.BAD_REQUEST, ex.getMessage())); + @ExceptionHandler(RoomescapeException.class) + ResponseEntity handleIllegalArgumentException(RoomescapeException ex) { + return ResponseEntity.status(ex.getHttpStatus()) + .body(new ApiExceptionResponse(ex.getHttpStatus(), ex.getMessage())); } @ExceptionHandler(MethodArgumentNotValidException.class) diff --git a/src/main/java/roomescape/exception/InvalidRequestException.java b/src/main/java/roomescape/exception/InvalidRequestException.java new file mode 100644 index 000000000..9999241f6 --- /dev/null +++ b/src/main/java/roomescape/exception/InvalidRequestException.java @@ -0,0 +1,10 @@ +package roomescape.exception; + +import org.springframework.http.HttpStatus; + +public class InvalidRequestException extends RoomescapeException { + + public InvalidRequestException(String message) { + super(message, HttpStatus.BAD_REQUEST); + } +} diff --git a/src/main/java/roomescape/service/reservation/ReservationCreateService.java b/src/main/java/roomescape/service/reservation/ReservationCreateService.java index 995a40a9b..51f9384d0 100644 --- a/src/main/java/roomescape/service/reservation/ReservationCreateService.java +++ b/src/main/java/roomescape/service/reservation/ReservationCreateService.java @@ -3,10 +3,11 @@ import java.time.LocalDate; import java.time.LocalDateTime; import org.springframework.stereotype.Service; -import roomescape.domain.member.Member; import roomescape.domain.Reservation; import roomescape.domain.ReservationTime; import roomescape.domain.Theme; +import roomescape.domain.member.Member; +import roomescape.exception.InvalidRequestException; import roomescape.repository.MemberRepository; import roomescape.repository.ReservationRepository; import roomescape.repository.ReservationTimeRepository; @@ -58,29 +59,29 @@ private Reservation create(Reservation request) { private ReservationTime getReservationTime(long reservationId) { return reservationTimeRepository.findById(reservationId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 예약 시간 입니다.")); + .orElseThrow(() -> new InvalidRequestException("존재하지 않는 예약 시간 입니다.")); } private Theme getTheme(long themeId) { return themeRepository.findById(themeId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 테마 입니다.")); + .orElseThrow(() -> new InvalidRequestException("존재하지 않는 테마 입니다.")); } private Member getMember(long memberId) { return memberRepository.findById(memberId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); + .orElseThrow(() -> new InvalidRequestException("존재하지 않는 사용자입니다.")); } public void validateAlreadyBooked(LocalDate date, long timeId, long themeId) { if (reservationRepository.existsByDateAndTimeIdAndThemeId(date, timeId, themeId)) { - throw new IllegalArgumentException("해당 시간에 이미 예약된 테마입니다."); + throw new InvalidRequestException("해당 시간에 이미 예약된 테마입니다."); } } public void validateDateIsFuture(LocalDate date, ReservationTime reservationTime) { LocalDateTime localDateTime = LocalDateTime.of(date, reservationTime.getStartAt()); if (localDateTime.isBefore(LocalDateTime.now())) { - throw new IllegalArgumentException("지나간 날짜와 시간에 대한 예약 생성은 불가능합니다."); + throw new InvalidRequestException("지나간 날짜와 시간에 대한 예약 생성은 불가능합니다."); } } } diff --git a/src/main/java/roomescape/service/reservation/ReservationDeleteService.java b/src/main/java/roomescape/service/reservation/ReservationDeleteService.java index ffbf79b5c..a4ff371aa 100644 --- a/src/main/java/roomescape/service/reservation/ReservationDeleteService.java +++ b/src/main/java/roomescape/service/reservation/ReservationDeleteService.java @@ -1,6 +1,7 @@ package roomescape.service.reservation; import org.springframework.stereotype.Service; +import roomescape.exception.InvalidRequestException; import roomescape.repository.ReservationRepository; @Service @@ -14,7 +15,7 @@ public ReservationDeleteService(ReservationRepository reservationRepository) { public void deleteReservation(long id) { reservationRepository.findById(id) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 예약 아이디 입니다.")); + .orElseThrow(() -> new InvalidRequestException("존재하지 않는 예약 아이디 입니다.")); reservationRepository.deleteById(id); } } diff --git a/src/main/java/roomescape/service/reservationtime/ReservationTimeCreateService.java b/src/main/java/roomescape/service/reservationtime/ReservationTimeCreateService.java index 7335f3868..681a4d03c 100644 --- a/src/main/java/roomescape/service/reservationtime/ReservationTimeCreateService.java +++ b/src/main/java/roomescape/service/reservationtime/ReservationTimeCreateService.java @@ -2,6 +2,7 @@ import org.springframework.stereotype.Service; import roomescape.domain.ReservationTime; +import roomescape.exception.InvalidRequestException; import roomescape.repository.ReservationTimeRepository; import roomescape.service.dto.request.ReservationTimeSaveRequest; @@ -16,7 +17,7 @@ public ReservationTimeCreateService(ReservationTimeRepository reservationTimeRep public ReservationTime createReservationTime(ReservationTimeSaveRequest request) { if (reservationTimeRepository.findByStartAt(request.startAt()).isPresent()) { - throw new IllegalArgumentException("이미 존재하는 예약 시간입니다."); + throw new InvalidRequestException("이미 존재하는 예약 시간입니다."); } ReservationTime newReservationTime = request.toEntity(request); diff --git a/src/main/java/roomescape/service/reservationtime/ReservationTimeDeleteService.java b/src/main/java/roomescape/service/reservationtime/ReservationTimeDeleteService.java index 65a7e7462..ea17557a2 100644 --- a/src/main/java/roomescape/service/reservationtime/ReservationTimeDeleteService.java +++ b/src/main/java/roomescape/service/reservationtime/ReservationTimeDeleteService.java @@ -1,6 +1,7 @@ package roomescape.service.reservationtime; import org.springframework.stereotype.Service; +import roomescape.exception.InvalidRequestException; import roomescape.repository.ReservationRepository; import roomescape.repository.ReservationTimeRepository; @@ -18,10 +19,10 @@ public ReservationTimeDeleteService(ReservationTimeRepository reservationTimeRep public void deleteReservationTime(long id) { reservationTimeRepository.findById(id) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 예약 시간 아이디 입니다.")); + .orElseThrow(() -> new InvalidRequestException("존재하지 않는 예약 시간 아이디 입니다.")); if (reservationRepository.existsByTimeId(id)) { - throw new IllegalArgumentException("이미 예약중인 시간은 삭제할 수 없습니다."); + throw new InvalidRequestException("이미 예약중인 시간은 삭제할 수 없습니다."); } reservationTimeRepository.deleteById(id); } diff --git a/src/main/java/roomescape/service/theme/ThemeDeleteService.java b/src/main/java/roomescape/service/theme/ThemeDeleteService.java index 7760886d8..3112abcee 100644 --- a/src/main/java/roomescape/service/theme/ThemeDeleteService.java +++ b/src/main/java/roomescape/service/theme/ThemeDeleteService.java @@ -1,6 +1,7 @@ package roomescape.service.theme; import org.springframework.stereotype.Service; +import roomescape.exception.InvalidRequestException; import roomescape.repository.ReservationRepository; import roomescape.repository.ThemeRepository; @@ -18,10 +19,10 @@ public ThemeDeleteService(ThemeRepository themeRepository, public void deleteTheme(long id) { themeRepository.findById(id) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 테마 아이디 입니다.")); + .orElseThrow(() -> new InvalidRequestException("존재하지 않는 테마 아이디 입니다.")); if (reservationRepository.existsByThemeId(id)) { - throw new IllegalArgumentException("이미 예약중인 테마는 삭제할 수 없습니다."); + throw new InvalidRequestException("이미 예약중인 테마는 삭제할 수 없습니다."); } themeRepository.deleteById(id); diff --git a/src/test/java/roomescape/service/reservation/ReservationCreateServiceTest.java b/src/test/java/roomescape/service/reservation/ReservationCreateServiceTest.java index 48045325a..fec7a13a8 100644 --- a/src/test/java/roomescape/service/reservation/ReservationCreateServiceTest.java +++ b/src/test/java/roomescape/service/reservation/ReservationCreateServiceTest.java @@ -12,6 +12,7 @@ import roomescape.domain.member.MemberName; import roomescape.domain.member.MemberPassword; import roomescape.domain.member.Role; +import roomescape.exception.InvalidRequestException; import roomescape.service.BaseServiceTest; import roomescape.service.dto.request.ReservationSaveRequest; @@ -46,7 +47,7 @@ void checkDuplicateReservationTime_Failure() { new MemberPassword("1234"), Role.USER); assertThatThrownBy(() -> reservationCreateService.create(request, member)) - .isInstanceOf(IllegalArgumentException.class) + .isInstanceOf(InvalidRequestException.class) .hasMessage("해당 시간에 이미 예약된 테마입니다."); } @@ -61,7 +62,7 @@ void checkReservationDateTimeIsFuture_Failure() { new MemberPassword("1234"), Role.USER); assertThatThrownBy(() -> reservationCreateService.create(request, member)) - .isInstanceOf(IllegalArgumentException.class) + .isInstanceOf(InvalidRequestException.class) .hasMessage("지나간 날짜와 시간에 대한 예약 생성은 불가능합니다."); } } diff --git a/src/test/java/roomescape/service/reservationtime/ReservationTimeCreateServiceTest.java b/src/test/java/roomescape/service/reservationtime/ReservationTimeCreateServiceTest.java index d3d663573..8040a4ebe 100644 --- a/src/test/java/roomescape/service/reservationtime/ReservationTimeCreateServiceTest.java +++ b/src/test/java/roomescape/service/reservationtime/ReservationTimeCreateServiceTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import roomescape.exception.InvalidRequestException; import roomescape.service.BaseServiceTest; import roomescape.service.dto.request.ReservationTimeSaveRequest; @@ -31,7 +32,7 @@ void checkDuplicateTime_Failure() { ReservationTimeSaveRequest request = new ReservationTimeSaveRequest(LocalTime.of(11, 0)); assertThatThrownBy(() -> reservationTimeCreateService.createReservationTime(request)) - .isInstanceOf(IllegalArgumentException.class) + .isInstanceOf(InvalidRequestException.class) .hasMessage("이미 존재하는 예약 시간입니다."); } } diff --git a/src/test/java/roomescape/service/reservationtime/ReservationTimeDeleteServiceTest.java b/src/test/java/roomescape/service/reservationtime/ReservationTimeDeleteServiceTest.java index 6b6497f07..2a427ed03 100644 --- a/src/test/java/roomescape/service/reservationtime/ReservationTimeDeleteServiceTest.java +++ b/src/test/java/roomescape/service/reservationtime/ReservationTimeDeleteServiceTest.java @@ -8,6 +8,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.DirtiesContext; +import roomescape.exception.InvalidRequestException; import roomescape.service.BaseServiceTest; class ReservationTimeDeleteServiceTest extends BaseServiceTest { @@ -26,7 +27,7 @@ void deleteNotReservedTime_Success() { @DisplayName("이미 예약 중인 시간을 삭제할 시 예외가 발생한다.") void deleteReservedTime_Failure() { assertThatThrownBy(() -> reservationTimeDeleteService.deleteReservationTime(1L)) - .isInstanceOf(IllegalArgumentException.class) + .isInstanceOf(InvalidRequestException.class) .hasMessage("이미 예약중인 시간은 삭제할 수 없습니다."); } } diff --git a/src/test/java/roomescape/service/theme/ThemeDeleteServiceTest.java b/src/test/java/roomescape/service/theme/ThemeDeleteServiceTest.java index f965afac3..ce527fc0f 100644 --- a/src/test/java/roomescape/service/theme/ThemeDeleteServiceTest.java +++ b/src/test/java/roomescape/service/theme/ThemeDeleteServiceTest.java @@ -8,6 +8,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.DirtiesContext; +import roomescape.exception.InvalidRequestException; import roomescape.service.BaseServiceTest; class ThemeDeleteServiceTest extends BaseServiceTest { @@ -26,7 +27,7 @@ void deleteNotReservedTime_Success() { @DisplayName("이미 예약 중인 테마를 삭제할 시 예외가 발생한다.") void deleteReservedTime_Failure() { assertThatThrownBy(() -> themeDeleteService.deleteTheme(1L)) - .isInstanceOf(IllegalArgumentException.class) + .isInstanceOf(InvalidRequestException.class) .hasMessage("이미 예약중인 테마는 삭제할 수 없습니다."); } } From 5b3d5d1b6e56e7144e43939d2fccb1ba627e4004 Mon Sep 17 00:00:00 2001 From: Minjoo Date: Sat, 18 May 2024 21:40:47 +0900 Subject: [PATCH 37/42] =?UTF-8?q?refactor:=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EA=B0=9D=EC=B2=B4=EA=B0=80=20=EB=8B=A4=EC=96=91=ED=95=9C=20?= =?UTF-8?q?=EC=9E=90=EB=A3=8C=ED=98=95=EC=9D=84=20=EB=B0=9B=EC=9D=84=20?= =?UTF-8?q?=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/ApiExceptionResponse.java | 2 +- .../exception/GlobalExceptionHandler.java | 24 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main/java/roomescape/exception/ApiExceptionResponse.java b/src/main/java/roomescape/exception/ApiExceptionResponse.java index 08f75e426..f514dc209 100644 --- a/src/main/java/roomescape/exception/ApiExceptionResponse.java +++ b/src/main/java/roomescape/exception/ApiExceptionResponse.java @@ -2,5 +2,5 @@ import org.springframework.http.HttpStatus; -public record ApiExceptionResponse(HttpStatus status, String message) { +public record ApiExceptionResponse(HttpStatus status, T message) { } diff --git a/src/main/java/roomescape/exception/GlobalExceptionHandler.java b/src/main/java/roomescape/exception/GlobalExceptionHandler.java index 1cae9b35b..920d8f5e6 100644 --- a/src/main/java/roomescape/exception/GlobalExceptionHandler.java +++ b/src/main/java/roomescape/exception/GlobalExceptionHandler.java @@ -15,13 +15,13 @@ public class GlobalExceptionHandler { @ExceptionHandler(RoomescapeException.class) - ResponseEntity handleIllegalArgumentException(RoomescapeException ex) { + ResponseEntity> handleIllegalArgumentException(RoomescapeException ex) { return ResponseEntity.status(ex.getHttpStatus()) - .body(new ApiExceptionResponse(ex.getHttpStatus(), ex.getMessage())); + .body(new ApiExceptionResponse<>(ex.getHttpStatus(), ex.getMessage())); } @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) { + public ResponseEntity>> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) { Map errors = new HashMap<>(); ex.getBindingResult().getAllErrors() .forEach((error) -> { @@ -30,30 +30,30 @@ public ResponseEntity> handleMethodArgumentNotValidException errors.put(fieldName, errorMessage); }); return ResponseEntity.badRequest() - .body(errors); + .body(new ApiExceptionResponse<>(HttpStatus.BAD_REQUEST, errors)); } @ExceptionHandler(ConstraintViolationException.class) - public ResponseEntity handleMethodConstraintViolationException(ConstraintViolationException ex) { + public ResponseEntity> handleMethodConstraintViolationException(ConstraintViolationException ex) { return ResponseEntity.badRequest() - .body(new ApiExceptionResponse(HttpStatus.BAD_REQUEST, ex.getMessage())); + .body(new ApiExceptionResponse<>(HttpStatus.BAD_REQUEST, ex.getMessage())); } @ExceptionHandler(HttpMessageNotReadableException.class) - public ResponseEntity handleHttpMessageNotReadableException(HttpMessageNotReadableException ex) { + public ResponseEntity> handleHttpMessageNotReadableException(HttpMessageNotReadableException ex) { return ResponseEntity.badRequest() - .body(new ApiExceptionResponse(HttpStatus.BAD_REQUEST, ex.getMessage())); + .body(new ApiExceptionResponse<>(HttpStatus.BAD_REQUEST, ex.getMessage())); } @ExceptionHandler(AuthenticationException.class) - public ResponseEntity handleAuthenticationException(AuthenticationException ex) { + public ResponseEntity> handleAuthenticationException(AuthenticationException ex) { return ResponseEntity.status(ex.getHttpStatus()) - .body(new ApiExceptionResponse(ex.getHttpStatus(), ex.getMessage())); + .body(new ApiExceptionResponse<>(ex.getHttpStatus(), ex.getMessage())); } @ExceptionHandler(RuntimeException.class) - ResponseEntity handleRuntimeException() { + ResponseEntity> handleRuntimeException() { return ResponseEntity.internalServerError() - .body("서버에서 예기치 못한 오류가 발생했습니다. 문제가 지속되는 경우 관리자에게 문의해주세요."); + .body(new ApiExceptionResponse<>(HttpStatus.INTERNAL_SERVER_ERROR, "서버에서 예기치 못한 오류가 발생했습니다. 문제가 지속되는 경우 관리자에게 문의해주세요.")); } } From 92fbca085125b49fb3aa0c2ad18537a81ce24ce7 Mon Sep 17 00:00:00 2001 From: Minjoo Date: Sat, 18 May 2024 22:04:06 +0900 Subject: [PATCH 38/42] =?UTF-8?q?refactor:=20id=20=EC=9C=A0=ED=9A=A8?= =?UTF-8?q?=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EC=BB=A4=EC=8A=A4=ED=85=80=20?= =?UTF-8?q?=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/ReservationApiController.java | 6 ++--- .../api/ReservationTimeApiController.java | 3 ++- .../controller/api/ThemeApiController.java | 4 ++-- .../controller/api/validator/IdPositive.java | 24 +++++++++++++++++++ 4 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 src/main/java/roomescape/controller/api/validator/IdPositive.java diff --git a/src/main/java/roomescape/controller/api/ReservationApiController.java b/src/main/java/roomescape/controller/api/ReservationApiController.java index 5c638286e..f116bb7c4 100644 --- a/src/main/java/roomescape/controller/api/ReservationApiController.java +++ b/src/main/java/roomescape/controller/api/ReservationApiController.java @@ -1,7 +1,6 @@ package roomescape.controller.api; import jakarta.validation.Valid; -import jakarta.validation.constraints.Positive; import java.net.URI; import java.util.List; import org.springframework.http.ResponseEntity; @@ -13,8 +12,9 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import roomescape.auth.AuthenticatedMember; -import roomescape.domain.member.Member; +import roomescape.controller.api.validator.IdPositive; import roomescape.domain.Reservation; +import roomescape.domain.member.Member; import roomescape.service.dto.request.ReservationSaveRequest; import roomescape.service.dto.response.reservation.ReservationResponse; import roomescape.service.dto.response.reservation.UserReservationResponses; @@ -54,7 +54,7 @@ public ResponseEntity addReservation(@RequestBody @Valid Re @DeleteMapping("/reservations/{reservationId}") public ResponseEntity deleteReservation(@PathVariable - @Positive(message = "1 이상의 값만 입력해주세요.") long reservationId) { + @IdPositive long reservationId) { reservationDeleteService.deleteReservation(reservationId); return ResponseEntity.noContent().build(); } diff --git a/src/main/java/roomescape/controller/api/ReservationTimeApiController.java b/src/main/java/roomescape/controller/api/ReservationTimeApiController.java index 6dfc81cc8..55d7bee55 100644 --- a/src/main/java/roomescape/controller/api/ReservationTimeApiController.java +++ b/src/main/java/roomescape/controller/api/ReservationTimeApiController.java @@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import roomescape.controller.api.validator.IdPositive; import roomescape.domain.ReservationStatuses; import roomescape.domain.ReservationTime; import roomescape.service.dto.request.ReservationTimeSaveRequest; @@ -64,7 +65,7 @@ public ResponseEntity addReservationTime( @DeleteMapping("/times/{timeId}") public ResponseEntity deleteReservationTime(@PathVariable - @Positive(message = "1 이상의 값만 입력해주세요.") long timeId) { + @IdPositive long timeId) { reservationTimeDeleteService.deleteReservationTime(timeId); return ResponseEntity.noContent().build(); } diff --git a/src/main/java/roomescape/controller/api/ThemeApiController.java b/src/main/java/roomescape/controller/api/ThemeApiController.java index 5b58a314d..c830942ed 100644 --- a/src/main/java/roomescape/controller/api/ThemeApiController.java +++ b/src/main/java/roomescape/controller/api/ThemeApiController.java @@ -1,7 +1,6 @@ package roomescape.controller.api; import jakarta.validation.Valid; -import jakarta.validation.constraints.Positive; import java.net.URI; import java.util.List; import org.springframework.http.ResponseEntity; @@ -12,6 +11,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +import roomescape.controller.api.validator.IdPositive; import roomescape.domain.Theme; import roomescape.service.dto.request.ThemeSaveRequest; import roomescape.service.dto.response.theme.ThemeResponse; @@ -57,7 +57,7 @@ public ResponseEntity addTheme(@RequestBody @Valid ThemeSaveReque @DeleteMapping("/themes/{themeId}") public ResponseEntity deleteTheme(@PathVariable - @Positive(message = "1 이상의 값만 입력해주세요.") long themeId) { + @IdPositive long themeId) { themeDeleteService.deleteTheme(themeId); return ResponseEntity.noContent().build(); } diff --git a/src/main/java/roomescape/controller/api/validator/IdPositive.java b/src/main/java/roomescape/controller/api/validator/IdPositive.java new file mode 100644 index 000000000..809b84f2d --- /dev/null +++ b/src/main/java/roomescape/controller/api/validator/IdPositive.java @@ -0,0 +1,24 @@ +package roomescape.controller.api.validator; + +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import jakarta.validation.constraints.Positive; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Positive(message = "1 이상의 값만 입력해주세요.") +@Constraint(validatedBy = {}) +@Documented +@Target({PARAMETER}) +@Retention(RUNTIME) +public @interface IdPositive { + String message() default ""; + + Class[] groups() default {}; + + Class[] payload() default {}; +} From c6c744e8a424ca365e95fe860e40f6473a5c83a3 Mon Sep 17 00:00:00 2001 From: Minjoo Date: Sat, 18 May 2024 22:05:28 +0900 Subject: [PATCH 39/42] =?UTF-8?q?refactor:=20=EC=9C=A0=ED=9A=A8=EC=84=B1?= =?UTF-8?q?=20=EA=B2=80=EC=82=AC=20=EC=A0=91=EA=B7=BC=20=EC=A0=9C=ED=95=9C?= =?UTF-8?q?=EC=9E=90=20private=EC=9C=BC=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 --- .../service/reservation/ReservationCreateService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/roomescape/service/reservation/ReservationCreateService.java b/src/main/java/roomescape/service/reservation/ReservationCreateService.java index 51f9384d0..65efd0fff 100644 --- a/src/main/java/roomescape/service/reservation/ReservationCreateService.java +++ b/src/main/java/roomescape/service/reservation/ReservationCreateService.java @@ -72,13 +72,13 @@ private Member getMember(long memberId) { .orElseThrow(() -> new InvalidRequestException("존재하지 않는 사용자입니다.")); } - public void validateAlreadyBooked(LocalDate date, long timeId, long themeId) { + private void validateAlreadyBooked(LocalDate date, long timeId, long themeId) { if (reservationRepository.existsByDateAndTimeIdAndThemeId(date, timeId, themeId)) { throw new InvalidRequestException("해당 시간에 이미 예약된 테마입니다."); } } - public void validateDateIsFuture(LocalDate date, ReservationTime reservationTime) { + private void validateDateIsFuture(LocalDate date, ReservationTime reservationTime) { LocalDateTime localDateTime = LocalDateTime.of(date, reservationTime.getStartAt()); if (localDateTime.isBefore(LocalDateTime.now())) { throw new InvalidRequestException("지나간 날짜와 시간에 대한 예약 생성은 불가능합니다."); From 29eee7f2c2c22a5cbda2c4e508fa13569d1c3d42 Mon Sep 17 00:00:00 2001 From: Minjoo Date: Sat, 18 May 2024 22:08:30 +0900 Subject: [PATCH 40/42] =?UTF-8?q?refactor:=20=EC=98=88=EC=95=BD=20?= =?UTF-8?q?=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EB=B0=8F=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/reservation/ReservationCreateService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/roomescape/service/reservation/ReservationCreateService.java b/src/main/java/roomescape/service/reservation/ReservationCreateService.java index 65efd0fff..9f7253697 100644 --- a/src/main/java/roomescape/service/reservation/ReservationCreateService.java +++ b/src/main/java/roomescape/service/reservation/ReservationCreateService.java @@ -39,7 +39,7 @@ public Reservation create(ReservationAdminSaveRequest request) { getReservationTime(request.timeId()), getTheme(request.themeId()), getMember(request.memberId())); - return create(reservation); + return saveReservation(reservation); } public Reservation create(ReservationSaveRequest request, Member member) { @@ -48,10 +48,10 @@ public Reservation create(ReservationSaveRequest request, Member member) { getReservationTime(request.timeId()), getTheme(request.themeId()), member); - return create(reservation); + return saveReservation(reservation); } - private Reservation create(Reservation request) { + private Reservation saveReservation(Reservation request) { validateDateIsFuture(request.getDate(), request.getReservationTime()); validateAlreadyBooked(request.getDate(), request.getReservationTime().getId(), request.getTheme().getId()); return reservationRepository.save(request); From 230f5e3cd9ba1a37eb93ac8e874c3c038d0edab6 Mon Sep 17 00:00:00 2001 From: Minjoo Date: Sun, 19 May 2024 19:31:24 +0900 Subject: [PATCH 41/42] =?UTF-8?q?refactor:=20@Transactional=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/reservation/ReservationCreateService.java | 3 +++ .../service/reservation/ReservationDeleteService.java | 2 ++ .../service/reservationtime/ReservationTimeCreateService.java | 2 ++ .../service/reservationtime/ReservationTimeDeleteService.java | 2 ++ src/main/java/roomescape/service/theme/ThemeCreateService.java | 2 ++ src/main/java/roomescape/service/theme/ThemeDeleteService.java | 2 ++ 6 files changed, 13 insertions(+) diff --git a/src/main/java/roomescape/service/reservation/ReservationCreateService.java b/src/main/java/roomescape/service/reservation/ReservationCreateService.java index 9f7253697..65b028ad7 100644 --- a/src/main/java/roomescape/service/reservation/ReservationCreateService.java +++ b/src/main/java/roomescape/service/reservation/ReservationCreateService.java @@ -3,6 +3,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import roomescape.domain.Reservation; import roomescape.domain.ReservationTime; import roomescape.domain.Theme; @@ -33,6 +34,7 @@ public ReservationCreateService(ReservationRepository reservationRepository, this.memberRepository = memberRepository; } + @Transactional public Reservation create(ReservationAdminSaveRequest request) { Reservation reservation = request.toEntity( request, @@ -42,6 +44,7 @@ public Reservation create(ReservationAdminSaveRequest request) { return saveReservation(reservation); } + @Transactional public Reservation create(ReservationSaveRequest request, Member member) { Reservation reservation = request.toEntity( request, diff --git a/src/main/java/roomescape/service/reservation/ReservationDeleteService.java b/src/main/java/roomescape/service/reservation/ReservationDeleteService.java index a4ff371aa..d10de8206 100644 --- a/src/main/java/roomescape/service/reservation/ReservationDeleteService.java +++ b/src/main/java/roomescape/service/reservation/ReservationDeleteService.java @@ -1,6 +1,7 @@ package roomescape.service.reservation; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import roomescape.exception.InvalidRequestException; import roomescape.repository.ReservationRepository; @@ -13,6 +14,7 @@ public ReservationDeleteService(ReservationRepository reservationRepository) { this.reservationRepository = reservationRepository; } + @Transactional public void deleteReservation(long id) { reservationRepository.findById(id) .orElseThrow(() -> new InvalidRequestException("존재하지 않는 예약 아이디 입니다.")); diff --git a/src/main/java/roomescape/service/reservationtime/ReservationTimeCreateService.java b/src/main/java/roomescape/service/reservationtime/ReservationTimeCreateService.java index 681a4d03c..3e6df554e 100644 --- a/src/main/java/roomescape/service/reservationtime/ReservationTimeCreateService.java +++ b/src/main/java/roomescape/service/reservationtime/ReservationTimeCreateService.java @@ -1,6 +1,7 @@ package roomescape.service.reservationtime; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import roomescape.domain.ReservationTime; import roomescape.exception.InvalidRequestException; import roomescape.repository.ReservationTimeRepository; @@ -15,6 +16,7 @@ public ReservationTimeCreateService(ReservationTimeRepository reservationTimeRep this.reservationTimeRepository = reservationTimeRepository; } + @Transactional public ReservationTime createReservationTime(ReservationTimeSaveRequest request) { if (reservationTimeRepository.findByStartAt(request.startAt()).isPresent()) { throw new InvalidRequestException("이미 존재하는 예약 시간입니다."); diff --git a/src/main/java/roomescape/service/reservationtime/ReservationTimeDeleteService.java b/src/main/java/roomescape/service/reservationtime/ReservationTimeDeleteService.java index ea17557a2..c465223fc 100644 --- a/src/main/java/roomescape/service/reservationtime/ReservationTimeDeleteService.java +++ b/src/main/java/roomescape/service/reservationtime/ReservationTimeDeleteService.java @@ -1,6 +1,7 @@ package roomescape.service.reservationtime; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import roomescape.exception.InvalidRequestException; import roomescape.repository.ReservationRepository; import roomescape.repository.ReservationTimeRepository; @@ -17,6 +18,7 @@ public ReservationTimeDeleteService(ReservationTimeRepository reservationTimeRep this.reservationRepository = reservationRepository; } + @Transactional public void deleteReservationTime(long id) { reservationTimeRepository.findById(id) .orElseThrow(() -> new InvalidRequestException("존재하지 않는 예약 시간 아이디 입니다.")); diff --git a/src/main/java/roomescape/service/theme/ThemeCreateService.java b/src/main/java/roomescape/service/theme/ThemeCreateService.java index abe30291f..da55921e2 100644 --- a/src/main/java/roomescape/service/theme/ThemeCreateService.java +++ b/src/main/java/roomescape/service/theme/ThemeCreateService.java @@ -1,6 +1,7 @@ package roomescape.service.theme; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import roomescape.domain.Theme; import roomescape.repository.ThemeRepository; import roomescape.service.dto.request.ThemeSaveRequest; @@ -14,6 +15,7 @@ public ThemeCreateService(ThemeRepository themeRepository) { this.themeRepository = themeRepository; } + @Transactional public Theme createTheme(ThemeSaveRequest request) { Theme theme = request.toEntity(request); return themeRepository.save(theme); diff --git a/src/main/java/roomescape/service/theme/ThemeDeleteService.java b/src/main/java/roomescape/service/theme/ThemeDeleteService.java index 3112abcee..da413cddc 100644 --- a/src/main/java/roomescape/service/theme/ThemeDeleteService.java +++ b/src/main/java/roomescape/service/theme/ThemeDeleteService.java @@ -1,6 +1,7 @@ package roomescape.service.theme; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import roomescape.exception.InvalidRequestException; import roomescape.repository.ReservationRepository; import roomescape.repository.ThemeRepository; @@ -17,6 +18,7 @@ public ThemeDeleteService(ThemeRepository themeRepository, this.reservationRepository = reservationRepository; } + @Transactional public void deleteTheme(long id) { themeRepository.findById(id) .orElseThrow(() -> new InvalidRequestException("존재하지 않는 테마 아이디 입니다.")); From cd71c48d5a429adb4a2565a86e538e7819160946 Mon Sep 17 00:00:00 2001 From: Minjoo Date: Sun, 19 May 2024 19:44:34 +0900 Subject: [PATCH 42/42] =?UTF-8?q?refactor:=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=20=EC=9D=B4=EB=A6=84=20=EC=97=AD=ED=95=A0=EC=97=90=20=EB=A7=9E?= =?UTF-8?q?=EA=B2=8C=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/exception/GlobalExceptionHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/roomescape/exception/GlobalExceptionHandler.java b/src/main/java/roomescape/exception/GlobalExceptionHandler.java index 920d8f5e6..e5fdc5bc2 100644 --- a/src/main/java/roomescape/exception/GlobalExceptionHandler.java +++ b/src/main/java/roomescape/exception/GlobalExceptionHandler.java @@ -15,7 +15,7 @@ public class GlobalExceptionHandler { @ExceptionHandler(RoomescapeException.class) - ResponseEntity> handleIllegalArgumentException(RoomescapeException ex) { + ResponseEntity> handleRoomescapeException(RoomescapeException ex) { return ResponseEntity.status(ex.getHttpStatus()) .body(new ApiExceptionResponse<>(ex.getHttpStatus(), ex.getMessage())); }