Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[1 - 2단계 방탈출 예약 대기] 페드로(류형욱) 미션 제출합니다. #78

Merged
merged 45 commits into from
May 18, 2024
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
8eaeba6
feat: 기본 코드 준비
hw0603 May 14, 2024
b56a6ea
docs(README): API 명세와 기능 명세 분리
hw0603 May 14, 2024
a344eda
chore: 데이터베이스 의존성 변경
hw0603 May 14, 2024
7940c1e
chore: JPA 관련 설정 추가
hw0603 May 14, 2024
5eeba23
feat(ReservationTime): JPA 엔티티로 변경
hw0603 May 14, 2024
20ba370
feat(Theme): JPA 엔티티로 변경
hw0603 May 14, 2024
4dfa9ae
feat(Member): JPA 엔티티로 변경
hw0603 May 14, 2024
a3ac3de
style: import문 최적화
hw0603 May 14, 2024
27a1a17
feat(Schedule): JPA 엔티티로 변경
hw0603 May 14, 2024
345a008
remove: JDBC 테스트 관련 파일 제거
hw0603 May 14, 2024
d36f8ad
remove: JdbcRepository 구현체 제거
hw0603 May 15, 2024
8845394
feat(ReservationTime): Jpa Repository 구현
hw0603 May 15, 2024
e293a2b
fix(ReservationTimeService): JPA Repository에 맞게 예약 가능 시간 조회 로직 수정
hw0603 May 15, 2024
a4347df
refactor(/auth/dto): 요청DTO에서 래핑된 객체를 받도록 변경
hw0603 May 15, 2024
b6f5ba1
remove(MemberJdbcRepository): JdbcRepository 구현체 제거
hw0603 May 15, 2024
6b0607e
feat(MemberRepository): JPA Repository 구현
hw0603 May 15, 2024
3db3aa1
remove(ThemeJdbcRepository): JdbcRepository 구현체 제거
hw0603 May 15, 2024
d4ff45a
feat(ThemeRepository): JPA Repository 구현
hw0603 May 15, 2024
f3b4629
feat(ReservationRepository): JPA Repository 구현
hw0603 May 15, 2024
50847ac
refactor: 요청 DTO의 날짜/시간 타입 변
hw0603 May 15, 2024
b69a90e
docs(README): 내 예약 조회 API, 요구사항 작성
hw0603 May 15, 2024
0e46807
feat(MemberPageController): 내 예약 페이지 추가
hw0603 May 15, 2024
74aa65d
feat(MemberController): 사용자 예약 조회 기능 구현
hw0603 May 15, 2024
a5ac413
fix: Reservation 버튼 링크 수정
hw0603 May 15, 2024
619f8f5
test: 기능 변경에 따른 test 수정
hw0603 May 15, 2024
4751384
refactor(ScheduleRepository): Cascade 제거 후 JPA Repository 구현
hw0603 May 15, 2024
af2fefb
refactor(Repository): 인터페이스 default 메서드 삭제
hw0603 May 15, 2024
d527966
test(ThemeRepositoryTest): 인기 테마 네이티브 쿼리 테스트 작성
hw0603 May 15, 2024
6e835cb
test(ReservationRepositoryTest): 예약 검색 필터링 테스트 작성
hw0603 May 16, 2024
b586232
test(MemberPageControllerTest): 사용자별 예약 페이지 접근 테스트 작성
hw0603 May 16, 2024
67392f2
refactor(/domain): no-args 생성자의 접근 제어자를 protected로 변경
hw0603 May 16, 2024
00f3571
fix(Description): 누락된 @Embeddable 어노테이션 추가
hw0603 May 16, 2024
b6d8997
refactor(/domain): 미사용 생성자 제거
hw0603 May 16, 2024
dccdb1f
refactor(/domain): 도메인 ID를 wrapper type 으로 변경
hw0603 May 16, 2024
921f7bd
feat(ReservationStatus): 예약 상태 enum 추가
hw0603 May 16, 2024
49a586f
refactor(ReservationFilterRequest): 예약 목록 필터링 dto 이름 변경
hw0603 May 16, 2024
3ee0c25
refactor: 패키지 구조 변경
hw0603 May 16, 2024
4424c24
refactor(/test): @Sql 어노테이션 인자를 일관성 있게 수정
hw0603 May 16, 2024
e56b2f1
refactor(Schedule): schedule을 entity에서 embeddable로 변경
hw0603 May 16, 2024
36b8f99
style: import문 최적화
hw0603 May 16, 2024
7902c72
chore: 초기 데이터 설정 파일 추가
hw0603 May 16, 2024
c720efa
remove: .gitmessage.txt 삭제
hw0603 May 16, 2024
e89d1e4
comment(ThemeRepository): 미 사용 주석 삭제
hw0603 May 17, 2024
719d51c
chore: 트랜잭션 로깅 추가
hw0603 May 17, 2024
fe542f4
remove: 미 사용 테스트 삭제
hw0603 May 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 43 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@
- uri: /login
- file path: templates/login.html

### 내 예약 페이지 접근
- http method: GET
- uri: /member/reservation
- file path: templates/reservation-mine.html


### 모든 예약 조회
- http method: GET
- uri: /reservations
Expand Down Expand Up @@ -635,30 +641,42 @@
}
```

## 기능 명세서

### 예약
- [x] 예약 정보는 식별자, 이름, 일정으로 이뤄져있다.
- [x] 이름은 1자 이상, 20자 이하여야 한다.

### 일정
- [x] 일정은 날짜, 예약 시간으로 이뤄져있다.
- [x] 일정은 현재 이후여야 한다.
- [x] 날짜는 올바른 형식으로 주어져야 한다.
- [x] 예약 시간은 이미 존재하는 시간들 중 하나이어야 한다.
- [x] 테마는 이미 존재하는 테마들 중 하나이어야 한다.

### 시간
- [x] 시간 정보는 식별자, 시작하는 시간으로 이뤄져있다.
### 사용자 예약 조회
- http method: GET
- uri: /reservations-mine
- request
```
GET /members/reservations HTTP/1.1
cookie: token={token}
host: localhost:8080
```

### 테마
- [x] 테마는 식별자, 이름, 설명, 썸네일로 이뤄져있다.
- [x] 이름은 중복될 수 없다.
- [x] 이름은 1자 이상, 20자 이하여야 한다.
- [x] 설명은 100자를 초과할 수 없다.
- response
```
HTTP/1.1 200
Content-Type: application/json

### 사용자
- [x] 사용자는 식별자, 이름, 이메일, 비밀번호, 역할로 이뤄져있다.
- [x] 이름은 1자 이상, 20자 이하여야 한다.
- [x] 이메일은 중복될 수 없다.
- [x] 비밀번호는 6~12자리 이내이어야 한다.
[
{
"reservationId": 1,
"theme": "테마1",
"date": "2024-03-01",
"time": "10:00",
"status": "예약"
},
{
"reservationId": 2,
"theme": "테마2",
"date": "2024-03-01",
"time": "12:00",
"status": "예약"
},
{
"reservationId": 3,
"theme": "테마3",
"date": "2024-03-01",
"time": "14:00",
"status": "예약"
}
]
```
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ 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-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
runtimeOnly 'com.h2database:h2'
implementation 'io.jsonwebtoken:jjwt:0.9.1'
Expand Down
29 changes: 29 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
## 요구사항 분석

### 예약
- [x] 예약 정보는 식별자, 이름, 일정, 예약 상태로 이뤄져있다.
- [x] 이름은 1자 이상, 20자 이하여야 한다.
- [x] 사용자의 식별자를 통해 해당하는 예약을 조회할 수 있다.

### 일정
- [x] 일정은 날짜, 예약 시간으로 이뤄져있다.
- [x] 일정은 현재 이후여야 한다.
- [x] 날짜는 올바른 형식으로 주어져야 한다.
- [x] 예약 시간은 이미 존재하는 시간들 중 하나이어야 한다.
- [x] 테마는 이미 존재하는 테마들 중 하나이어야 한다.

### 시간
- [x] 시간 정보는 식별자, 시작하는 시간으로 이뤄져있다.

### 테마
- [x] 테마는 식별자, 이름, 설명, 썸네일로 이뤄져있다.
- [x] 이름은 중복될 수 없다.
- [x] 이름은 1자 이상, 20자 이하여야 한다.
- [x] 설명은 100자를 초과할 수 없다.

### 사용자
- [x] 사용자는 식별자, 이름, 이메일, 비밀번호, 역할로 이뤄져있다.
- [x] 이름은 1자 이상, 20자 이하여야 한다.
- [x] 이메일은 중복될 수 없다.
- [x] 비밀번호는 6~12자리 이내이어야 한다.

Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Arrays;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import roomescape.domain.member.Role;
import roomescape.exception.ForbiddenException;
import roomescape.exception.UnauthorizedException;

import java.util.Arrays;

@Component
public class AdminRoleHandlerInterceptor implements HandlerInterceptor {
private static final String KEY = "token";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Arrays;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
Expand All @@ -11,11 +12,9 @@
import org.springframework.web.method.support.ModelAndViewContainer;
import roomescape.exception.UnauthorizedException;

import java.util.Arrays;

@Component
public class LoginMemberIdArgumentResolver implements HandlerMethodArgumentResolver {
private final static String KEY = "token";
private static final String KEY = "token";

private final TokenProvider tokenProvider;

Expand All @@ -30,7 +29,8 @@ public boolean supportsParameter(MethodParameter parameter) {
}

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
Cookie[] cookies = request.getCookies();
if (cookies == null) {
Expand Down
7 changes: 3 additions & 4 deletions src/main/java/roomescape/auth/TokenProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import roomescape.domain.member.Member;
import roomescape.exception.UnauthorizedException;

import java.util.Date;

@Component
public class TokenProvider {
@Value("${security.jwt.token.secret-key}")
Expand All @@ -24,8 +23,8 @@ public String create(Member member) {

return Jwts.builder()
.claim("id", member.getId())
.claim("role", member.getRole())
.claim("email", member.getEmail())
.claim("role", member.getRole().name())
.claim("email", member.getEmail().getValue())
.setIssuedAt(now)
.setExpiration(validity)
.signWith(SignatureAlgorithm.HS256, secretKey.getBytes())
Expand Down
32 changes: 25 additions & 7 deletions src/main/java/roomescape/config/GlobalExceptionHandler.java
Original file line number Diff line number Diff line change
@@ -1,32 +1,50 @@
package roomescape.config;

import java.time.format.DateTimeParseException;
import java.util.Optional;
import org.springframework.context.support.DefaultMessageSourceResolvable;
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.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import roomescape.exception.*;
import roomescape.exception.ExceptionTemplate;
import roomescape.exception.ForbiddenException;
import roomescape.exception.InvalidMemberException;
import roomescape.exception.InvalidReservationException;
import roomescape.exception.UnauthorizedException;

@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = {InvalidReservationException.class, InvalidMemberException.class})
protected ResponseEntity<ExceptionTemplate> handleInvalidReservationException(Exception exception) {
public ResponseEntity<ExceptionTemplate> handleInvalidReservationException(Exception exception) {
return ResponseEntity.badRequest().body(new ExceptionTemplate(exception.getMessage()));
}

@ExceptionHandler(value = {UnauthorizedException.class})
protected ResponseEntity<ExceptionTemplate> handleUnauthorizedException(Exception exception) {
public ResponseEntity<ExceptionTemplate> handleUnauthorizedException(UnauthorizedException exception) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ExceptionTemplate(exception.getMessage()));
}

@ExceptionHandler(value = {ForbiddenException.class})
protected ResponseEntity<ExceptionTemplate> handlerForbiddenException(Exception exception) {
public ResponseEntity<ExceptionTemplate> handlerForbiddenException(ForbiddenException exception) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new ExceptionTemplate(exception.getMessage()));
}

@ExceptionHandler
protected ResponseEntity<ExceptionTemplate> handleValidationException(MethodArgumentNotValidException exception) {
String message = exception.getBindingResult().getFieldError().getDefaultMessage();
@ExceptionHandler(value = {MethodArgumentNotValidException.class})
public ResponseEntity<ExceptionTemplate> handleValidationException(MethodArgumentNotValidException exception) {
String message = Optional.ofNullable(exception.getBindingResult().getFieldError())
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.orElse("요청 형식이 잘못되었습니다.");
return ResponseEntity.badRequest().body(new ExceptionTemplate(message));
}

@ExceptionHandler(value = {HttpMessageNotReadableException.class})
public ResponseEntity<ExceptionTemplate> handleValidationException(HttpMessageNotReadableException exception) {
if (exception.getRootCause() instanceof DateTimeParseException) {
return ResponseEntity.badRequest().body(new ExceptionTemplate("시간/날짜 형식이 잘못되었습니다."));
}
return ResponseEntity.badRequest().body(new ExceptionTemplate("잘못된 요청입니다."));
}
}
3 changes: 1 addition & 2 deletions src/main/java/roomescape/config/WebMvcConfiguration.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -8,8 +9,6 @@
import roomescape.auth.LoginMemberIdArgumentResolver;
import roomescape.auth.TokenProvider;

import java.util.List;

@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
package roomescape.controller.reservation;
package roomescape.controller;

import jakarta.validation.Valid;
import java.net.URI;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import roomescape.service.reservation.ReservationService;
import roomescape.service.reservation.dto.AdminReservationRequest;
import roomescape.service.reservation.dto.ReservationFindRequest;
import roomescape.service.reservation.dto.ReservationFilterRequest;
import roomescape.service.reservation.dto.ReservationResponse;

import java.net.URI;
import java.util.List;

@RestController
@RequestMapping("/admin/reservations")
public class AdminReservationController {
Expand All @@ -21,7 +27,8 @@ public AdminReservationController(ReservationService reservationService) {
}

@PostMapping
public ResponseEntity<ReservationResponse> createReservation(@RequestBody @Valid AdminReservationRequest adminReservationRequest) {
public ResponseEntity<ReservationResponse> createReservation(
@RequestBody @Valid AdminReservationRequest adminReservationRequest) {
ReservationResponse reservationResponse = reservationService.create(adminReservationRequest);
return ResponseEntity.created(URI.create("/reservations/" + reservationResponse.id()))
.body(reservationResponse);
Expand All @@ -34,8 +41,9 @@ public ResponseEntity<Void> deleteReservation(@PathVariable("id") long id) {
}

@GetMapping("/search")
public List<ReservationResponse> findReservations(@ModelAttribute("ReservationFindRequest") ReservationFindRequest reservationFindRequest) {
return reservationService.findByCondition(reservationFindRequest);
public List<ReservationResponse> findReservations(
@ModelAttribute("ReservationFindRequest") ReservationFilterRequest reservationFilterRequest) {
return reservationService.findByCondition(reservationFilterRequest);
}

}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package roomescape.controller.auth;
package roomescape.controller;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import java.net.URI;
import java.util.Arrays;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
Expand All @@ -16,9 +18,6 @@
import roomescape.service.auth.dto.SignUpRequest;
import roomescape.service.member.dto.MemberResponse;

import java.net.URI;
import java.util.Arrays;

@RestController
public class AuthController {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package roomescape.controller.member;
package roomescape.controller;

import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import roomescape.auth.LoginMemberId;
import roomescape.service.member.MemberService;
import roomescape.service.member.dto.MemberReservationResponse;
import roomescape.service.member.dto.MemberResponse;

import java.util.List;

@RestController
@RequestMapping("/members")
public class MemberController {
Expand All @@ -21,4 +22,9 @@ public MemberController(MemberService memberService) {
public List<MemberResponse> findAllMembers() {
return memberService.findAll();
}

@GetMapping("/reservations")
public List<MemberReservationResponse> findReservations(@LoginMemberId long memberId) {
return memberService.findReservations(memberId);
}
}
Loading