Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
161 changes: 161 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,164 @@ public class MissionStepTest {
}
}
```
## 🚀 4단계 - JPA 전환
### 요구사항
- [x] JPA를 활용하여 데이터베이스에 접근하도록 수정하세요.
### 세부 요구사항
#### gradle 의존성 추가
- [x] build.gradle 파일을 이용하여 다음 의존성을 대체하세요.
- [x] as is: spring-boot-stater-jdbc
- [x] to be: spring-boot-starter-data-jpa
#### 엔티티 매핑
- [x] 다른 클래스를 의존하지 않는 클래스 먼저 엔티티 설정을 하세요.
- [x] ex) Theme나 Time 등
#### 연관관계 매핑
- [x] 다른 클래스에 의존하는 클래스는 연관관계 매핑을 추가로 하세요.
- [x] ex) Reservation은 Member나 Theme 등의 객체에 의존합니다.
### 요구사항 테스트
```java
@DataJpaTest
public class JpaTest {
@Autowired
private TestEntityManager entityManager;

@Autowired
private TimeRepository timeRepository;

@Test
void 사단계() {
Time time = new Time("10:00");
entityManager.persist(time);
entityManager.flush();

Time persistTime = timeRepository.findById(time.getId()).orElse(null);

assertThat(persistTime.getTime()).isEqualTo(time.getTime());
}
}

```

## 🚀 5단계 - 내 예약 목록 조회
### 요구사항
- [x] 내 예약 목록을 조회하는 API를 구현하세요.
### 내 예약 목록 기능
- [x] 아래의 request와 response 요구사항에 따라 기능을 구현하세요.
#### Request
```
GET /reservations-mine HTTP/1.1
cookie: token=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwibmFtZSI6IuyWtOuTnOuvvCIsInJvbGUiOiJBRE1JTiJ9.vcK93ONRQYPFCxT5KleSM6b7cl1FE-neSLKaFyslsZM
host: localhost:8080
```
#### 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": "예약"
}
]

```
### 요구사항 테스트
```java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
public class MissionStepTest {
@Test
void 오단계() {
String adminToken = createToken("[email protected]", "password");

List<MyReservationResponse> reservations = RestAssured.given().log().all()
.cookie("token", adminToken)
.get("/reservations-mine")
.then().log().all()
.statusCode(200)
.extract().jsonPath().getList(".", MyReservationResponse.class);

assertThat(reservations).hasSize(3);
}
}
```

## 🚀 6단계 - 예약 대기 기능
### 요구사항
- [x] 예약 대기 요청 기능을 구현하세요.
- [x] 예약 대기 취소 기능도 함께 구현하세요.
- [x] 내 예약 목록 조회 시 예약 대기 목록도 함께 포함하세요.
- [x] 중복 예약이 불가능 하도록 구현하세요.

> ⚠️ 심화 요구사항 - 내 예약 목록의 예약 대기 상태에 몇 번째 대기인지도 함께 표시하세요.
#### 예약 대기 요청
![img_1.png](img_1.png)
#### 내 예약 목록에서 조회 & 예약 대기 취소
![img_2.png](img_2.png)
### 요구사항 테스트
```java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
public class MissionStepTest {
@Test
void 육단계() {
String brownToken = createToken("[email protected]", "password");

Map<String, String> params = new HashMap<>();
params.put("date", "2024-03-01");
params.put("time", "1");
params.put("theme", "1");

// 예약 대기 생성
WaitingResponse waiting = RestAssured.given().log().all()
.body(params)
.cookie("token", brownToken)
.contentType(ContentType.JSON)
.post("/waitings")
.then().log().all()
.statusCode(201)
.extract().as(WaitingResponse.class);

// 내 예약 목록 조회
List<MyReservationResponse> myReservations = RestAssured.given().log().all()
.body(params)
.cookie("token", brownToken)
.contentType(ContentType.JSON)
.get("/reservations-mine")
.then().log().all()
.statusCode(200)
.extract().jsonPath().getList(".", MyReservationResponse.class);

// 예약 대기 상태 확인
String status = myReservations.stream()
.filter(it -> it.getId() == waiting.getId())
.filter(it -> !it.getStatus().equals("예약"))
.findFirst()
.map(it -> it.getStatus())
.orElse(null);

assertThat(status).isEqualTo("1번째 예약대기");
}
}


```

2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

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

Expand Down
Binary file added img_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 10 additions & 7 deletions src/main/java/roomescape/auth/AuthService.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,31 @@
import roomescape.auth.dto.LoginMember;
import roomescape.auth.dto.LoginRequest;
import roomescape.auth.jwt.TokenProvider;
import roomescape.exception.AuthorizationException;
import roomescape.member.Member;
import roomescape.member.MemberDao;
import roomescape.member.MemberRepository;

@Service
public class AuthService {

private final MemberDao memberDao;
private final MemberRepository memberRepository;
private final TokenProvider tokenProvider;

public AuthService(MemberDao memberDao, TokenProvider tokenProvider) {
this.memberDao = memberDao;
public AuthService(MemberRepository memberRepository, TokenProvider tokenProvider) {
this.memberRepository = memberRepository;
this.tokenProvider = tokenProvider;
}

public String createToken(LoginRequest loginRequest) {
Member member = memberDao.findByEmailAndPassword(loginRequest.getEmail(),
loginRequest.getPassword());
Member member = memberRepository.findByEmailAndPassword(loginRequest.getEmail(),
loginRequest.getPassword())
.orElseThrow(() -> new AuthorizationException("사용자를 찾을 수 없습니다."));
return tokenProvider.createToken(member);
}

public LoginMember login(String email, String password) {
Member member = memberDao.findByEmailAndPassword(email, password);
Member member = memberRepository.findByEmailAndPassword(email, password)
.orElseThrow(() -> new AuthorizationException("사용자를 찾을 수 없습니다."));
return new LoginMember(
member.getId(),
member.getName(),
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/roomescape/member/Member.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
package roomescape.member;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)

Choose a reason for hiding this comment

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

@GeneratedValuestrategy 속성을 지정하지 않는다면 기본 값은 어떤게 사용되나요?

또한 strategy의 종류는 어떤게 있는지 알아봐도 좋을 것 같아요.

private Long id;
private String name;
private String email;
Expand Down Expand Up @@ -29,6 +37,9 @@ public Member(String name, String email, String password, String role) {
this.role = role;
}

protected Member() {
}

public Long getId() {
return id;
}
Expand Down
75 changes: 0 additions & 75 deletions src/main/java/roomescape/member/MemberDao.java

This file was deleted.

10 changes: 10 additions & 0 deletions src/main/java/roomescape/member/MemberRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package roomescape.member;

import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByEmailAndPassword(String email, String password);
}
11 changes: 6 additions & 5 deletions src/main/java/roomescape/member/MemberService.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@

@Service
public class MemberService {
private MemberDao memberDao;
private final MemberRepository memberRepository;

public MemberService(MemberDao memberDao) {
this.memberDao = memberDao;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}

public MemberResponse createMember(MemberRequest memberRequest) {
Member member = memberDao.save(new Member(memberRequest.getName(), memberRequest.getEmail(),
memberRequest.getPassword(), "USER"));
Member member = memberRepository.save(
new Member(memberRequest.getName(), memberRequest.getEmail(),
memberRequest.getPassword(), "USER"));
Comment on lines -7 to +16

Choose a reason for hiding this comment

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

데이터 접근 계층에 대한 변경이 있었을 뿐인데, 이와 연관 없는 비즈니스 계층까지 변경이 발생했네요.
(미션 요구사항을 따라했다면 당연히 발생한 문제이니, 본인의 실수나 잘못이 아니에요)

코드 변경이 많아질 수록, 실수로 인해 의도치 않은 버그의 발생 확률이 높아지고, 연관이 없는 코드의 변경이 많아질 수록, 명확한 변경 사항의 추적이 힘들어질 거에요.

또한 JPA의 학습 곡선이 너무 높거나, 내부 기술 제약으로 인해 다시 JdbcTemplate을 사용한 코드로 되돌려야 한다면, 다시 이러한 변경이 발생할 것 같아요.

그렇다면 데이터 접근 계층의 구조를 어떻게 설계하고 사용했어야 이러한 연관 없는 변경을 막을 수 있을까요?

return new MemberResponse(member.getId(), member.getName(), member.getEmail());
}
}
31 changes: 31 additions & 0 deletions src/main/java/roomescape/reservation/MyReservationResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package roomescape.reservation;

import roomescape.waiting.Waiting;

public record MyReservationResponse(
Long id,
String theme,
String date,
String time,
String status
) {
public static MyReservationResponse from(Reservation reservation) {
return new MyReservationResponse(
reservation.getId(),
reservation.getTheme().getName(),
reservation.getDate(),
reservation.getTime().getTime(),
"예약"
);
}

public static MyReservationResponse from(Waiting waiting, Long rank) {
return new MyReservationResponse(
waiting.getId(),
waiting.getTheme().getName(),
waiting.getDate(),
waiting.getTime().getTime(),
(rank + 1) + "번째 예약대기"
);
}
}
Loading