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단계 방탈출 예약 대기] 이든(최승준) 미션 제출합니다. #86

Merged
merged 31 commits into from
May 18, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
39ebbcb
init: 이전 미션 코드 반영
PgmJun May 14, 2024
a8e288f
refactor: Jdbc -> Jpa 마이그레이션
PgmJun May 14, 2024
c6d87a2
fix: 인기테마 조회 API Endpoint 변경
PgmJun May 14, 2024
b0fca6a
feat: 내 예약목록 조회 API 구현
PgmJun May 14, 2024
ec79bb0
style: 처리한 TODO 제거
PgmJun May 15, 2024
75da8ed
style: 처리한 TODO 제거
PgmJun May 16, 2024
dcb7d4f
refactor: LAZY 로딩 적용
PgmJun May 16, 2024
f35ff04
refactor: Spring-validation 적용
PgmJun May 16, 2024
78e632c
fix: 중복 메서드 제거
PgmJun May 16, 2024
a790f9e
fix: 코드라인 정리
PgmJun May 16, 2024
92bc885
refactor: JPA Column Nullable 설정
PgmJun May 16, 2024
428054a
refactor: AuthInterceptor 분리
PgmJun May 16, 2024
68f8d7a
refactor: 인증 Interceptor 코드 라인 정리
PgmJun May 16, 2024
e6c1d0a
feat: 회원가입 API 구현
PgmJun May 16, 2024
47a0914
feat: Cookie MaxAge 설정
PgmJun May 16, 2024
fb4d90c
feat: Logout API 구현
PgmJun May 16, 2024
4de3664
refactor: 적용하지 않은 TODO 제거
PgmJun May 16, 2024
a0bfd63
docs: auth API 관련 문서 작성
PgmJun May 16, 2024
3da51a3
refactor: Entity 객체 원시타입 필드에 VO 적용
PgmJun May 16, 2024
8c5ea0d
fix: 검증 기준 획일화
PgmJun May 16, 2024
79a70d9
feat: Reservation 객체 검증 로직 추가
PgmJun May 16, 2024
177c6cf
test: Member, Theme 객체 검증 로직 작성
PgmJun May 16, 2024
84cbb98
refactor: nativeQuery 사용 로직 제거
PgmJun May 17, 2024
5accf81
fix: 누락된 Embeddable 에노테이션 추가
PgmJun May 17, 2024
f2209cd
docs: ERD 추가
PgmJun May 17, 2024
db74b5c
refactor: VO 필드명 도메인적으로 변경
PgmJun May 17, 2024
9ac7445
test: Auth 관련 기능 테스트 구현
PgmJun May 17, 2024
a7b7a64
refactor: ReservationTime에서 지난 시간인지 직접 검증하도록 수정
PgmJun May 17, 2024
d17de2f
Merge remote-tracking branch 'origin/step1' into step1
PgmJun May 17, 2024
f0786d6
refactor: 내 예약목록 조회 API Endpoint를 RESTful하게 변경
PgmJun May 17, 2024
c611311
test: 내 예약목록 조회 API 테스트 작성
PgmJun 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
104 changes: 101 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,21 @@
|----------|--------|---------------------------------------------------------|-----------------------|----------------------------------------|-------------------|
| | GET | `/` | 인기 테마 페이지 요청 | `templates/index.html` | `@Controller` |
| | GET | `/reservation` | 사용자 예약 페이지 요청 | `templates/reservation.html` | `@Controller` |
| | GET | `/reservation-mine` | 내 예약 조회 페이지 요청 | `templates/reservation-mine.html` | `@Controller` |
| `ADMIN` | GET | `/admin` | 어드민 페이지 요청 | `templates/admin/index.html` | `@Controller` |
| `ADMIN` | GET | `/admin/reservation` | 예약 관리 페이지 요청 | `templates/admin/reservation-new.html` | `@Controller` |
| `ADMIN` | GET | `/admin/reservationTime` | 예약 시간 관리 페이지 요청 | `templates/admin/reservationTime.html` | `@Controller` |
| `ADMIN` | GET | `/admin/theme` | 테마 관리 페이지 요청 | `templates/admin/theme.html` | `@Controller` |
| | GET | `/login` | 로그인 페이지 요청 | `templates/login.html` | `@Controller` |
| | GET | `/signup` | 회원가입 페이지 요청 | | `@Controller` |
| | POST | `/login` | 로그인 요청 | | `@RestController` |
| | POST | `/signup` | 회원가입 요청 | | `@RestController` |
| | POST | `/logout` | 로그아웃 요청 | | `@RestController` |
| | GET | `/login/check` | 인증 정보 조회 | | `@RestController` |
| | GET | `/token-reissue` | JWT 토큰 재발급 | | `@RestController` |
| | GET | `/reservations` | 예약 정보 조회 | | `@RestController` |
| | GET | `/reservations/search?themeId&memberId&dateFrom&dateTo` | 예약 정보 조건 검색 | | `@RestController` |
| `MEMBER` | GET | `/token-reissue` | JWT 토큰 재발급 | | `@RestController` |
| `ADMIN` | GET | `/reservations` | 예약 정보 조회 | | `@RestController` |
| `MEMBER` | GET | `/reservations-mine` | 내 예약 정보 조회 | | `@RestController` |
| `ADMIN` | GET | `/reservations/search?themeId&memberId&dateFrom&dateTo` | 예약 정보 조건 검색 | | `@RestController` |
| | GET | `/reservations/themes/{themeId}/reservationTimes?date` | 특정 날짜의 특정 테마 예약 정보 조회 | | `@RestController` |
| `MEMBER` | POST | `/reservations` | 예약 추가 | | `@RestController` |
| | DELETE | `/reservations/{id}` | 예약 취소 | | `@RestController` |
Expand Down Expand Up @@ -55,6 +60,57 @@ Set-Cookie: refreshToken=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwibmFtZSI6ImFkbWluIi

---

### 회원가입 요청 API

- Request

```
POST /login HTTP/1.1
Content-Type: application/json

{
"name: "name"
"password": "password",
"email": "admin@email.com"
}
```

- Response

```
HTTP/1.1 200
Content-Type: application/json
Keep-Alive: timeout=60
Set-Cookie: accessToken=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwibmFtZSI6ImFkbWluIiwicm9sZSI6IkFETUlOIn0.cwnHsltFeEtOzMHs2Q5-ItawgvBZ140OyWecppNlLoI; Path=/; HttpOnly
Set-Cookie: refreshToken=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwibmFtZSI6ImFkbWluIiwicm9sZSI6IkFETUlOIn0.cwnHsltFeEtOzMHs2Q5-ItawgvBZ140OyWecppNlLoI; Path=/; HttpOnly
```

---

### 로그아웃 요청 API

- Request

```
POST /login HTTP/1.1
Content-Type: application/json
accessToken=eyJhbGciOiJIUzI1NiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzE1NjE1OTMyLCJleHAiOjE3MTU2MTc3MzJ9.nfu6IZlKBccnmBbMtKDTP-5TbNWUMhcVY_ee09aNwhE;
```

- Response

> Cookie를 통해 만료된 토큰 Response

```
HTTP/1.1 200
Content-Type: application/json
Keep-Alive: timeout=60
Set-Cookie: accessToken=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwibmFtZSI6ImFkbWluIiwicm9sZSI6IkFETUlOIn0.cwnHsltFeEtOzMHs2Q5-ItawgvBZ140OyWecppNlLoI; Path=/; HttpOnly
Set-Cookie: refreshToken=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwibmFtZSI6ImFkbWluIiwicm9sZSI6IkFETUlOIn0.cwnHsltFeEtOzMHs2Q5-ItawgvBZ140OyWecppNlLoI; Path=/; HttpOnly
```

---

### JWT 토큰 재발급 API

- Request
Expand Down Expand Up @@ -134,6 +190,48 @@ Content-Type: application/json

---

### 내 예약 정보 조회 API

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

README에 구현해주신 테이블 명세에 대한 ERD를 추가해주시는건 어때요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

너무 좋은 것 같습니다:)
반영해볼게요!


- Request

```
GET /reservations-mine HTTP/1.1

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

주어진 엔드포인트에 대해서 어떻게 생각하세요?

Copy link
Author

@PgmJun PgmJun May 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 API endpoint는 RESTful 하지 않다고 느껴집니다.
REST의 모든 원칙은 지키지 못하더라도 최소한 리소스를 가리키는 의미있는 endpoint를 사용하는게 좋다고 생각이 들어요!
고려할 내용이 정말 많은 부분이라 우선은 LMS 요구사항에 나와있는대로 엔드포인트를 사용해주었는데
조앤이 언급해주신 김에 한번 변경해보겠습니다:)

Cookie: accessToken=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwibmFtZSI6IuyWtOuTnOuvvCIsInJvbGUiOiJBRE1JTiJ9.vcK93ONRQYPFCxT5KleSM6b7cl1FE-neSLKaFyslsZM;
```

- Response

```
HTTP/1.1 200
Content-Type: application/json

[
{
"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": "예약"
}
]
```

---

### 예약 정보 조회 API

- Request
Expand Down
5 changes: 3 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ 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'

implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation 'javax.xml.bind:jaxb-api:2.3.1'

Expand Down
1 change: 1 addition & 0 deletions src/main/java/roomescape/RoomescapeApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@SpringBootApplication
public class RoomescapeApplication {
Expand Down
37 changes: 29 additions & 8 deletions src/main/java/roomescape/auth/controller/AuthController.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
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.dto.LoginCheckResponse;
import roomescape.auth.dto.LoginRequest;
import roomescape.auth.dto.SignUpRequest;
import roomescape.auth.service.AuthService;
import roomescape.global.auth.annotation.Auth;
import roomescape.global.auth.annotation.MemberId;
import roomescape.global.auth.jwt.JwtHandler;
import roomescape.global.auth.jwt.dto.TokenDto;
import roomescape.global.dto.response.ApiResponse;

Expand All @@ -22,20 +26,38 @@ public AuthController(final AuthService authService) {
this.authService = authService;
}

@PostMapping("/signup")
public ApiResponse<Void> signup(@Valid @RequestBody final SignUpRequest signupRequest, final HttpServletResponse response) {
TokenDto tokenDto = authService.signUp(signupRequest);
addTokensToCookie(tokenDto, response);

return ApiResponse.success();
}

@PostMapping("/login")
public ApiResponse<Void> login(@RequestBody final LoginRequest loginRequest, final HttpServletResponse response) {
public ApiResponse<Void> login(@Valid @RequestBody final LoginRequest loginRequest, final HttpServletResponse response) {
TokenDto tokenDto = authService.login(loginRequest);
addTokensToCookie(tokenDto, response);

return ApiResponse.success();
}

@PostMapping("/logout")
public ApiResponse<Void> logout(final HttpServletResponse response) {
TokenDto logoutTokenDto = authService.logout();
addTokensToCookie(logoutTokenDto, response);

return ApiResponse.success();
}

@Auth
@GetMapping("/login/check")
public ApiResponse<LoginCheckResponse> checkLogin(@MemberId final Long memberId) {
LoginCheckResponse response = authService.checkLogin(memberId);
return ApiResponse.success(response);
}

// TODO: 토큰 재발급 자동화 로직 구현

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요건 왜 TODO예요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

클라이언트에서 401 Unauthorized가 발생하면 /token-reissue API를 다시 호출하도록 만들어주려고 해요!
그래서 TODO로 작성해두었는데 그 전에 리뷰 요청 기간이 되어서 마무리하지 못했던 작업입니다..!

@GetMapping("/token-reissue")
public ApiResponse<Void> reissueToken(final HttpServletRequest request, final HttpServletResponse response) {
TokenDto requestToken = getTokenFromCookie(request);
Expand All @@ -50,27 +72,26 @@ private TokenDto getTokenFromCookie(final HttpServletRequest request) {
String accessToken = "";
String refreshToken = "";
for (Cookie cookie : request.getCookies()) {
if (cookie.getName().equals("accessToken")) {
if (cookie.getName().equals(JwtHandler.ACCESS_TOKEN_HEADER_KEY)) {
accessToken = cookie.getValue();
cookie.setMaxAge(0);
}
if (cookie.getName().equals("refreshToken")) {
if (cookie.getName().equals(JwtHandler.REFRESH_TOKEN_HEADER_KEY)) {
refreshToken = cookie.getValue();
cookie.setMaxAge(0);
}
}

return new TokenDto(accessToken, refreshToken);
}

private void addTokensToCookie(TokenDto tokenInfo, HttpServletResponse response) {
addTokenToCookie("accessToken", tokenInfo.accessToken(), response);
addTokenToCookie("refreshToken", tokenInfo.refreshToken(), response);
addTokenToCookie(JwtHandler.ACCESS_TOKEN_HEADER_KEY, tokenInfo.accessToken(), response, JwtHandler.ACCESS_TOKEN_EXPIRE_TIME);
addTokenToCookie(JwtHandler.REFRESH_TOKEN_HEADER_KEY, tokenInfo.refreshToken(), response, JwtHandler.REFRESH_TOKEN_EXPIRE_TIME);
}

private void addTokenToCookie(String cookieName, String token, HttpServletResponse response) {
private void addTokenToCookie(String cookieName, String token, HttpServletResponse response, int maxAge) {
Cookie cookie = new Cookie(cookieName, token);
cookie.setHttpOnly(true);
cookie.setMaxAge(maxAge);

response.addCookie(cookie);
}
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/roomescape/auth/dto/LoginRequest.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package roomescape.auth.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;

public record LoginRequest(
@NotBlank(message = "이메일은 null 또는 공백일 수 없습니다.")
@Email(message = "이메일 형식이 일치하지 않습니다. (xxx@xxx.xxx)")
String email,
@NotBlank(message = "비밀번호는 null 또는 공백일 수 없습니다.")
String password
) {
}
27 changes: 27 additions & 0 deletions src/main/java/roomescape/auth/dto/SignUpRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package roomescape.auth.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import roomescape.member.domain.Member;
import roomescape.member.domain.MemberName;

public record SignUpRequest(
@NotBlank(message = "회원명은 null 또는 공백일 수 없습니다.")
@Size(min = MemberName.MIN_LENGTH, max = MemberName.MAX_LENGTH, message = "회원명은 " + MemberName.MIN_LENGTH + "~" + MemberName.MAX_LENGTH + "글자 사이여야 합니다.")
String name,
@NotBlank(message = "이메일은 null 또는 공백일 수 없습니다.")
@Email(message = "이메일 형식이 일치하지 않습니다. (xxx@xxx.xxx)")
String email,
@NotBlank(message = "비밀번호는 null 또는 공백일 수 없습니다.")
String password
) {
public Member toMemberEntity() {
return new Member(
name,
email,
password
);
}

}
13 changes: 12 additions & 1 deletion src/main/java/roomescape/auth/service/AuthService.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.springframework.stereotype.Service;
import roomescape.auth.dto.LoginCheckResponse;
import roomescape.auth.dto.LoginRequest;
import roomescape.auth.dto.SignUpRequest;
import roomescape.global.auth.jwt.JwtHandler;
import roomescape.global.auth.jwt.dto.TokenDto;
import roomescape.global.exception.error.ErrorType;
Expand All @@ -20,8 +21,14 @@ public AuthService(final MemberService memberService, final JwtHandler jwtHandle
this.jwtHandler = jwtHandler;
}

public TokenDto signUp(final SignUpRequest signupRequest) {
Member member = memberService.addMember(signupRequest);

return jwtHandler.createToken(member.getId());
}

public TokenDto login(final LoginRequest request) {
Member member = memberService.findMemberByEmailAndPassword(request.email(), request.password());
Member member = memberService.findMemberByEmailAndPassword(request);

return jwtHandler.createToken(member.getId());
}
Expand All @@ -42,4 +49,8 @@ public TokenDto reissueToken(final String accessToken, final String refreshToken
Long memberId = jwtHandler.getMemberIdFromTokenWithNotValidate(accessToken);
return jwtHandler.createToken(memberId);
}

public TokenDto logout() {
return jwtHandler.createLogoutToken();
}
}
11 changes: 11 additions & 0 deletions src/main/java/roomescape/global/auth/annotation/Auth.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package roomescape.global.auth.annotation;

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

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Auth {
}
Loading