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 - 3단계 방탈출 사용자 예약] 리비(이근희) 미션 제출합니다. #3

Open
wants to merge 54 commits into
base: libienz
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 52 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
2622490
feat: setup project
woowabrie Apr 5, 2024
706c060
chore: 방탈출 예약 관리 미션 마이그레이션 완료
ehtjsv2 Apr 18, 2024
7033b39
docs: 기능 요구사항 작성
Libienz Apr 30, 2024
faf9920
fix: 예약시각 추가 후 새로고침 안되는 버그 수정
Libienz Apr 30, 2024
986ea9e
test: 조회 테스트에서 post요청을 날리고 있던 오류 수정
Libienz Apr 30, 2024
29f5ce3
test: 중복되는 Mock 테스트 제거 개선
Libienz Apr 30, 2024
ad9cb39
refactor: ReservationTimeDao가 request dto가 아닌 엔티티를 기반으로 동작하도록 개선
Libienz Apr 30, 2024
e7abc93
refactor: ReservationService가 TimeDao를 의존하도록 변경
Libienz Apr 30, 2024
08da946
refactor: Dao가 DTO가 아닌 엔티티와 상호작용하도록 개선
Libienz Apr 30, 2024
db26247
style: 파라미터 이름 가독성 개선
Libienz Apr 30, 2024
1fa2aa0
refactor: 저장 후 저장된 엔티티 반환 시 조회 쿼리가 아닌 새로운 생성으로 처리하도록 개선
Libienz Apr 30, 2024
f7e82a6
feat: ReservationDate 구현 및 null 데이터가 들어오지 않도록 검증하는 기능 추가
Libienz May 1, 2024
2c35a3a
feat: ReservationDate 과거 날짜는 검증 구현
Libienz May 1, 2024
fc95a5b
feat: Name 구현 및 검증 로직 구현
Libienz May 1, 2024
57d1f09
feat: 존재하지 않는 예약시각에 대한 예약 예외 구현
Libienz May 1, 2024
7f5aeae
feat: 예약 가능 시각이 null이 아님을 검증하는 기능 구현
Libienz May 1, 2024
f624f87
feat: 같은 시간이 존재하는지 확인하는 DAO 기능 구현
Libienz May 1, 2024
5d5c64f
fix: fakeDao 예약 시간 존재하는지 확인하는 기능 오류 수정
Libienz May 1, 2024
3650baa
feat: 이미 존재하는 예약 시간으로 예약 시각 추가 시 예외 발생 기능 구현
Libienz May 1, 2024
48ab4f5
feat: 도메인 게터 추가 및 Hash & Equals 구현
Libienz May 1, 2024
e86236f
refactor: 도메인 로직을 가지고 있는 포장 객체로 Reservation 구성 (관련 테스트 수정)
Libienz May 1, 2024
130cf54
feat: Reservation 추가 시 날짜와 시간이 같은 경우인지를 검증하도록 구현
Libienz May 1, 2024
d28495c
feat: 전역 예외 처리 로직 구현
Libienz May 1, 2024
e2cc555
feat: 테마 관련 뷰로직 추가
Libienz May 1, 2024
3333508
docs: 2단계 요구사항 작성
Libienz May 1, 2024
bfa4381
feat: Theme 클래스 추가
Libienz May 1, 2024
3f2b90a
chore: schema.sql에 Theme테이블 추가
Libienz May 1, 2024
6deace6
fix: 상태코드 수정에 따른 테스트 상태코드 수정
Libienz May 1, 2024
097dcfc
feat: 테마 전체 조회 기능 구현
Libienz May 1, 2024
79b8f13
feat: 테마 추가 기능 구현
Libienz May 1, 2024
f90f8ee
feat: 테마 삭제 기능 구현
Libienz May 1, 2024
9167b47
feat: ThemeService 전체 테마 조회 기능 구현
Libienz May 1, 2024
2bc40d2
faet: ThemeService 추가와 삭제기능 구현
ehtjsv2 May 1, 2024
f8e1eeb
feat: 테마 단건 조회 기능 구현
Libienz May 1, 2024
4675f58
feat: 존재하지 않는 Id로 테마 삭제시 예외 발생 기능 구현
Libienz May 1, 2024
8fc5de0
feat: 전체 테마를 조회하는 컨트롤러 메서드 구현
Libienz May 1, 2024
3c1d8f6
feat: ThemeController 추가와 삭제기능 구현
ehtjsv2 May 1, 2024
d1513c9
feat: Reservation 스키마에 theme_id 추가 및 관련 로직 수정
Libienz May 1, 2024
8716aa1
refactor: ThemeRequest 클래스 dto 패키지로 이동 개선
Libienz May 1, 2024
e665601
refactor: findById에서 예외 캐치 시 구체적 예외를 잡도록 개선 EmptyResultDataAccessExce…
Libienz May 1, 2024
1220dfb
feat: 사용자 예약 페이지 뷰 연결 구현
Libienz May 2, 2024
f94c579
fix: 테마 목록에서 테마 이름이 보이지 않는 문제 해결
Libienz May 2, 2024
3d6edf9
docs: 기능 요구사항 작성
Libienz May 2, 2024
1d9aa04
feat: 날짜와 테마를 기반으로 예약을 찾는 기능 구현
Libienz May 2, 2024
2ac3886
feat: ReservationService 테마와 날짜기반으로 예약 가능시각 구하는 기능 구현
ehtjsv2 May 2, 2024
e396b98
feat: 예약 가능 시각 반환 엔드포인트 지정 및 컨트롤러 구현
Libienz May 2, 2024
2793e21
fix: 요청 json 바디 매핑 안되는 문제 해결
Libienz May 2, 2024
c6fc316
fix: 엔드포인트 오타 수정
Libienz May 2, 2024
92fd7b9
fix: Get요청 시 부가정보를 경로변수로 받도록 수정
Libienz May 2, 2024
93cb710
feat: welcomePage 변경
ehtjsv2 May 2, 2024
87fa155
feat: ReservationDao 인기테마 조회 기능 구현
ehtjsv2 May 2, 2024
4e5d49f
feat: 인기 테마 필터링 날짜 조건 추가
Libienz May 2, 2024
c9ec69b
fix: 시간과 날짜 그리고 테마가 겹쳐야 중복되는 예약으로 간주하도록 수정
Libienz May 2, 2024
e09e2d0
chore: 커밋 시작점 다른 오류 해결
Libienz May 2, 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
65 changes: 34 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,35 @@
# 기능 요구사항

# 1~3단계 요구 사항
- [x] `localhost:8080/admin` get 요청 시 어드민 메인 페이지가 응답할 수 있다.
- [x] `localhost:8080` get 요청 시 어드민 메인 페이지가 응답할 수 있다.
- [x] `/admin/reservation` get 요청 시 예약 관리 페이지가 응답할 수 있다.
- [x] 예약 관리 페이지 응답 시, 현재 예약 목록을 함께 보여준다.
- [x] `/admin/reservation` post 요청 시 예약을 추가한다.
- [x] `/reservations/{id}` delete 요청 시 예약을 삭제한다.
- [x] id 값이 없는 경우 예외를 발생시킨다.

# 4단계 요구사항
- [x] JdbcTemplate을 이용하여 DataSource객체에 접근하기
- [x] DataSource 객체를 이용하여 Connection 확인하기
- [x] Connection 객체를 이용하여 데이터베이스 이름 검증
- [x] Connection 객체를 이용하여 테이블 이름 검증

# 5단계 요구사항
- [x] db의 reservation을 조회 할 수 있다.

# 6단계 요구사항
- [x] 예약 추가 api가 db와 연동된다.
- [x] 예약 취소 api를 통해 db의 예약 정보를 삭제가능하다.

# 7단계 요구사항
- [x] 시간 관리 페이지를 응답할 수 있다.
- [x] 예약 시간 추가를 할 수 있다.
- [x] 예약 시간 리스트 조회를 할 수 있다.
- [x] 예약 시간 삭제를 할 수 있다.

# 8단계 요구사항
- [x] 예약 페이지를 reservation.html로 변경
- [x] 방탈출 예약 시 정해진 시간만을 예약 가능합니다.
## 예약 가능 시각

- [x] 예약 가능 시간은 null일 수 없다.
- [x] 예약 가능 시간은 이미 등록되어 있는 시간과 겹칠 수 없다.

## 예약

- [x] 예약 시각이 중복되는 예약이 존재하는 경우 예약을 할 수 없다.

## 예약자 이름

- [x] 예약자 이름은 빈 문자열일 수 없다.
- [x] 예약자 이름은 Null일 수 없다.

## 예약 날짜

- [x] 예약 날짜는 오늘보다 이전일 수 없다.
- [x] 예약 날짜는 null일 수 없다.

## 예약 시간

- [x] 예약시간은 지정된 예약시간 중 하나여야 한다.

### 2단계 요구사항

- [x] 테마를 조회할 수 있다
- [x] 테마를 추가할 수 있다
- [x] 테마를 삭제할 수 있다

### 3단계 요구사항

- [ ] 날짜와 테마를 기반으로 예약 가능한 시각과 예약 불가능한 시각을 응답할 수 있다.
- [ ] `/`요청 시 인기 테마 페이지를 응답한다.
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,25 @@
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class ViewController {
public class AdminViewController {

@GetMapping(path = {"/", "/admin"})
public String wellComePage() {
@GetMapping( "/admin")
public String adminMainPage() {
return "admin/index";
}

@GetMapping("/admin/reservation")
public String reservationPage() {
return "admin/reservation";
return "admin/reservation-new";
}

@GetMapping("admin/time")
@GetMapping("/admin/time")
public String reservationTimePage() {
return "admin/time";
}

@GetMapping("/admin/theme")
public String themePage() {
return "admin/theme";
}
}
13 changes: 13 additions & 0 deletions src/main/java/roomescape/controller/ClientViewController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package roomescape.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class ClientViewController {

@GetMapping("/reservation")
public String reservationPage() {
return "reservation";
}
}
16 changes: 16 additions & 0 deletions src/main/java/roomescape/controller/ReservationController.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package roomescape.controller;

import java.net.URI;
import java.time.LocalDate;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
Expand All @@ -10,6 +11,9 @@
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import roomescape.domain.Reservation;
import roomescape.domain.Theme;
import roomescape.dto.BookableTimeResponse;
import roomescape.dto.BookableTimesRequest;
import roomescape.dto.ReservationAddRequest;
import roomescape.service.ReservationService;

Expand Down Expand Up @@ -38,4 +42,16 @@ public ResponseEntity<Void> removeReservation(@PathVariable("id") Long id) {
reservationService.removeReservation(id);
return ResponseEntity.noContent().build();
}

@GetMapping("/reservations/bookable-times/{date}/{themeId}")
public ResponseEntity<List<BookableTimeResponse>> getTimesWithStatus(
@PathVariable("date") LocalDate date,
@PathVariable("themeId") Long themeId) {
Comment on lines +48 to +49
Copy link
Member

Choose a reason for hiding this comment

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

많은 방법 중에 왜 PathVariable 을 사용하는 API 설계를 했어?

Copy link
Member

Choose a reason for hiding this comment

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

쿼리 스트링을 이용하는 방법도 있잖아

Copy link
Member Author

Choose a reason for hiding this comment

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

지금 다시 짠다면 쿼리파라미터를 이용하도록 할 듯!
구현이 급해서 api 설계를 대충한 부분이야

오늘 강의 들어보니 이러한 부분이 이번 미션의 핵심이었던 것 같은데 반성 중 .. 😭

return ResponseEntity.ok(reservationService.findBookableTimes(new BookableTimesRequest(date, themeId)));
}

@GetMapping("/reservations/theme-rank")
public ResponseEntity<List<Theme>> getThemeRank() {
return ResponseEntity.ok(reservationService.getThemeRanking());
}
Comment on lines +53 to +56
Copy link
Member

Choose a reason for hiding this comment

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

이미 피드백 강의에서 인지했겠지만, 요구사항이 조금만 변경되도 많은 수정이 필요할 것 같아!

Copy link
Member

Choose a reason for hiding this comment

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

ThemeController에서 이걸 구현해야 할 것 같은데 왜 여기서 했어?

Copy link
Member Author

Choose a reason for hiding this comment

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

자원간의 연관관계를 잘못해석했어 😂
사실 자원이라는 개념도 모르고 구현했던 것이쥐

현재는 uri가 자원의 식별자임을 알게되었고 개선이 필요한 점 인지하게 되었음 👍

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package roomescape.controller;

import java.net.URI;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
Expand Down Expand Up @@ -27,8 +28,10 @@ public ResponseEntity<List<ReservationTime>> getReservationTimeList() {
}

@PostMapping("/times")
public ResponseEntity<ReservationTime> addReservationTime(@RequestBody ReservationTimeAddRequest reservationTimeAddRequest) {
return ResponseEntity.ok(reservationTimeService.addReservationTime(reservationTimeAddRequest));
public ResponseEntity<ReservationTime> addReservationTime(
@RequestBody ReservationTimeAddRequest reservationTimeAddRequest) {
ReservationTime reservationTime = reservationTimeService.addReservationTime(reservationTimeAddRequest);
return ResponseEntity.created(URI.create("/times/" + reservationTime.getId())).body(reservationTime);
}

@DeleteMapping("/times/{id}")
Expand Down
40 changes: 40 additions & 0 deletions src/main/java/roomescape/controller/ThemeController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package roomescape.controller;

import java.net.URI;
import java.util.List;
import org.springframework.http.ResponseEntity;
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.dto.ThemeAddRequest;
import roomescape.service.ThemeService;

@RestController
public class ThemeController {
private final ThemeService themeService;

public ThemeController(ThemeService themeService) {
this.themeService = themeService;
}

@GetMapping("/themes")
public ResponseEntity<List<Theme>> getThemeList() {
return ResponseEntity.ok(themeService.findAllTheme());
}

@PostMapping("/themes")
public ResponseEntity<Theme> addTheme(@RequestBody ThemeAddRequest themeAddRequest) {
Theme theme = themeService.addTheme(themeAddRequest);
return ResponseEntity.created(URI.create("/themes" + theme.getId())).body(theme);
}

@DeleteMapping("/themes/{id}")
public ResponseEntity<Void> deleteTheme(@PathVariable("id") Long id) {
themeService.removeTheme(id);
return ResponseEntity.noContent().build();
}
}
40 changes: 40 additions & 0 deletions src/main/java/roomescape/domain/Name.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package roomescape.domain;

import java.util.Objects;

public class Name {

private final String name;

public Name(String name) {
validateNonBlank(name);
this.name = name;
}

private void validateNonBlank(String name) {
if (name == null || name.isEmpty()) {
Copy link
Member

Choose a reason for hiding this comment

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

" " 과 같은 값도 검증이 필요할 거 같은데 isEmpty() 대신 isBlank()를 사용하면 가능!

Copy link
Member Author

Choose a reason for hiding this comment

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

검증에 구멍이 있었군 👀

throw new NullPointerException("이름은 비어있을 수 없습니다.");
}
}

public String getName() {
return name;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Name name1 = (Name) o;
return Objects.equals(name, name1.name);
}

@Override
public int hashCode() {
return Objects.hash(name);
}
}
33 changes: 24 additions & 9 deletions src/main/java/roomescape/domain/Reservation.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,47 @@
public class Reservation {

private final Long id;
private final String name;
private final LocalDate date;
private final Name name;
private final ReservationDate date;
private final ReservationTime time;
private Theme theme;

public Reservation(Long id, String name, LocalDate date, ReservationTime time) {
public Reservation(Long id, String name, LocalDate date, ReservationTime time, Theme theme) {
this.id = id;
this.name = name;
this.date = date;
this.name = new Name(name);
this.date = new ReservationDate(date);
this.time = time;
this.theme = theme;
}

public Long getId() {
return id;
}

public Long getTimeId() {
return time.getId();
}

public Long getThemeId() {
return theme.getId();
}

public String getName() {
return name;
return name.getName();
}

public LocalDate getDate() {
return date;
return date.getDate();
}

public ReservationTime getTime() {
return time;
}

public Theme getTheme() {
return theme;
}

@Override
public boolean equals(Object o) {
if (this == o) {
Expand All @@ -43,12 +57,13 @@ public boolean equals(Object o) {
}
Reservation that = (Reservation) o;
return Objects.equals(id, that.id) && Objects.equals(name, that.name)
&& Objects.equals(date, that.date) && Objects.equals(time, that.time);
&& Objects.equals(date, that.date) && Objects.equals(time, that.time)
&& Objects.equals(theme, that.theme);
}

@Override
public int hashCode() {
return Objects.hash(id, name, date, time);
return Objects.hash(id, name, date, time, theme);
}

@Override
Expand Down
52 changes: 52 additions & 0 deletions src/main/java/roomescape/domain/ReservationDate.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package roomescape.domain;

import java.time.LocalDate;
import java.util.Objects;

public class ReservationDate {

private final LocalDate date;

public ReservationDate(LocalDate date) {
validate(date);
this.date = date;
}

private void validate(LocalDate date) {
validateNonNull(date);
validateNonPastDate(date);
}

private void validateNonNull(LocalDate date) {
if (date == null) {
throw new NullPointerException("날짜는 null일 수 없습니다");
}
}

private void validateNonPastDate(LocalDate date) {
if (date.isBefore(LocalDate.now())) {
throw new IllegalArgumentException(date + ": 예약 날짜는 현재 보다 이전일 수 없습니다");
}
}
Comment on lines +26 to +30
Copy link
Member

Choose a reason for hiding this comment

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

나는 이 로직을 service 계층에서 이루어졌는데, 리비의 의견이 궁금!
체스 미션으로 예를 들어보면 piece는 현재 위치를 알 필요가 없고, piece의 위치를 아는 것은 board가 자연스러운 흐름같은데, 비슷한 맥락으로 ReservationDate가 현재 시각을 알 필요가 없지 않을까?

Copy link
Member Author

Choose a reason for hiding this comment

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

ReservationDate 도메인을 어떻게 해석하는지에 따라 달라지는 문제라고 생각!

예약 날짜라는 도메인을 데이터만 담고있는 것으로 생각한다면 service계층에서의 검증이 적절한 것 같아
하지만 위같은 형태는 도메인 주도 개발이라기보다는 데이터 주도 개발에 가깝다고 생각해

도메인 주도 개발측면에서 예약 날짜는 과거일 수 없다라는 명세는 ReservationDate관련 로직이라고 생각해!

서비스는 물론 비즈니스 로직을 담는 계층이고 범용적으로 운용될 수 있지만 각각의 도메인 명세는 도메인 클래스에 있어야 자연스럽지 않나 하는 개인적인 생각이야 🧐


public LocalDate getDate() {
return date;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ReservationDate that = (ReservationDate) o;
return Objects.equals(date, that.date);
}

@Override
public int hashCode() {
return Objects.hash(date);
}
}
7 changes: 7 additions & 0 deletions src/main/java/roomescape/domain/ReservationTime.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,17 @@ public class ReservationTime {
private final LocalTime startAt;

public ReservationTime(Long id, LocalTime startAt) {
validateNonNull(startAt);
this.id = id;
this.startAt = startAt;
}

private void validateNonNull(LocalTime startAt) {
if (startAt == null) {
throw new NullPointerException("예약 가능 시각은 null일 수 없습니다");
}
}

public Long getId() {
return id;
}
Expand Down
Loading