-
Notifications
You must be signed in to change notification settings - Fork 0
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 - 6단계 - 방탈출 예약 관리] 리비(이근희) 미션 제출합니다. #6
base: libienz
Are you sure you want to change the base?
Changes from all commits
48c754f
4a8f81e
e899ccb
67fda49
6bfe713
0c6e7b2
5e199c7
6b8086a
451c499
b4bfc78
bbef49f
38d89f8
e87fbbd
2f9a19c
cfdf9ed
62c4a3e
b69655c
addeb22
25f0775
545ef8c
01299a5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
# 방탈출 API | ||
|
||
| Method | Endpoint | Description | | ||
|--------|----------------------|--------------| | ||
| GET | `/admin` | 어드민 페이지 요청 | | ||
| GET | `/admin/reservation` | 예약 관리 페이지 요청 | | ||
| GET | `/reservations` | 예약 정보 | | ||
| POST | `/reservations` | 예약 추가 | | ||
| DELETE | `/reservations/{id}` | 예약 취소 | | ||
|
||
# 방탈출 예약 도메인 명세 | ||
|
||
## Name | ||
|
||
- [x] 예약자 이름은 null이거나 빈 문자열일 수 없다. | ||
- [x] 예약자 이름은 1~5사이의 길이를 가져야 한다. | ||
|
||
## ReservationTime | ||
|
||
- [x] 예약하려는 시간에 다른 예약이 존재한다면 예약이 불가능하다. | ||
- [x] 예약 시간은 예약 시작 시간으로부터 한 시간의 길이를 가진다. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package roomescape.controller; | ||
|
||
import org.springframework.stereotype.Controller; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
|
||
@RequestMapping("/admin") | ||
@Controller | ||
public class AdminViewController { | ||
@GetMapping() | ||
public String showAdminMainPage() { | ||
return "admin/index"; | ||
} | ||
|
||
@GetMapping("/reservation") | ||
public String showAdminReservationPage() { | ||
return "admin/reservation-legacy"; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
package roomescape.controller; | ||
|
||
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.RequestMapping; | ||
import org.springframework.web.bind.annotation.RestController; | ||
import roomescape.controller.dto.ReservationRequest; | ||
import roomescape.controller.dto.ReservationResponse; | ||
import roomescape.entity.Reservation; | ||
import roomescape.service.ReservationService; | ||
|
||
@RequestMapping("/reservations") | ||
@RestController | ||
Comment on lines
+17
to
+18
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 리비만의 애노테이션 순서 컨벤션이 있는지 궁금해 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 없움 🫠 짚어줘서 땡큐! 안 짚어줬으면 그냥 넘어갔을 듯 |
||
public class ReservationController { | ||
private final ReservationService reservationService; | ||
|
||
public ReservationController(ReservationService reservationService) { | ||
this.reservationService = reservationService; | ||
} | ||
|
||
@GetMapping() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 확실히 |
||
public ResponseEntity<List<ReservationResponse>> readAllReservations() { | ||
List<ReservationResponse> reservations = reservationService.readAll() | ||
.stream() | ||
.map(ReservationResponse::from) | ||
.toList(); | ||
return ResponseEntity.ok().body(reservations); | ||
} | ||
|
||
@PostMapping() | ||
public ResponseEntity<ReservationResponse> createReservation(@RequestBody ReservationRequest reservationRequest) { | ||
Reservation savedReservation = reservationService.saveReservation(reservationRequest.toEntity()); | ||
return ResponseEntity.ok().body(ReservationResponse.from(savedReservation)); | ||
} | ||
|
||
@DeleteMapping("/{id}") | ||
public ResponseEntity<Void> deleteReservationById(@PathVariable("id") long id) { | ||
reservationService.deleteReservation(id); | ||
return ResponseEntity.ok().build(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package roomescape.controller.dto; | ||
|
||
import java.time.LocalDate; | ||
import java.time.LocalTime; | ||
import roomescape.entity.Reservation; | ||
|
||
public class ReservationRequest { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. dto를 record가 아닌 class로 설정한 이유가 궁금해 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 사실 별차이 없다고 생각해 record를 잘모르는 사람으로써 record 키워드를 보면 어떤 생성자가 있고 getter가 있는지 setter가 있는지 잘 기억이 나지 않아서리.. |
||
private String name; | ||
private LocalDate date; | ||
private LocalTime time; | ||
|
||
public ReservationRequest() { | ||
} | ||
|
||
public ReservationRequest(String name, LocalDate date, LocalTime time) { | ||
this.name = name; | ||
this.date = date; | ||
this.time = time; | ||
} | ||
|
||
public Reservation toEntity() { | ||
return new Reservation(name, date, time); | ||
} | ||
|
||
public String getName() { | ||
return name; | ||
} | ||
|
||
public LocalDate getDate() { | ||
return date; | ||
} | ||
|
||
public LocalTime getTime() { | ||
return time; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package roomescape.controller.dto; | ||
|
||
import java.time.LocalDate; | ||
import java.time.LocalTime; | ||
import roomescape.entity.Reservation; | ||
|
||
public class ReservationResponse { | ||
private final Long id; | ||
private final String name; | ||
private final LocalDate date; | ||
private final LocalTime time; | ||
|
||
public ReservationResponse(long id, String name, LocalDate date, LocalTime time) { | ||
this.id = id; | ||
this.name = name; | ||
this.date = date; | ||
this.time = time; | ||
} | ||
|
||
public static ReservationResponse from(Reservation reservation) { | ||
return new ReservationResponse( | ||
reservation.getId(), | ||
reservation.getName(), | ||
reservation.getStartDate(), | ||
reservation.getStartTime()); | ||
} | ||
|
||
public long getId() { | ||
return id; | ||
} | ||
|
||
public String getName() { | ||
return name; | ||
} | ||
|
||
public LocalDate getDate() { | ||
return date; | ||
} | ||
|
||
public LocalTime getTime() { | ||
return time; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package roomescape.entity; | ||
|
||
public class Name { | ||
private static final int MIN_LENGTH = 1; | ||
private static final int MAX_LENGTH = 5; | ||
|
||
private final String name; | ||
|
||
public Name(String name) { | ||
validate(name); | ||
this.name = name; | ||
} | ||
|
||
Comment on lines
+7
to
+13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이름 VO 만든거 최고...👍 |
||
private void validate(String name) { | ||
validateNonNull(name); | ||
validateLength(name); | ||
} | ||
|
||
private void validateNonNull(String name) { | ||
if (name == null) { | ||
throw new NullPointerException("예약자 이름은 Null이 될 수 없습니다"); | ||
} | ||
} | ||
Comment on lines
+19
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 굳이 이 메서드가 없어도 validateLength 에서 name.length() 메서드를 호출할 때 NPE가 발생할 거 같은데 만약 name이 null이라면 그대로 NPE를 발생시키는게 아니라 IllegalArgumentException으로 처리하는건 어때? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NPE에 메시지를 담는다는 맥락에서 단순 NPE발생보다는 효율적이라는 개인적인 생각! 다만 레디말처럼 다른 예외로 변환할 수도 있겠네 |
||
|
||
private void validateLength(String name) { | ||
if (MAX_LENGTH < name.length() || name.length() < MIN_LENGTH) { | ||
throw new IllegalArgumentException( | ||
"예약자 이름은 " + MIN_LENGTH + "자 이상, " + MAX_LENGTH + "자 미만이어야 합니다: " + name); | ||
} | ||
} | ||
|
||
public String getName() { | ||
return name; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package roomescape.entity; | ||
|
||
import java.time.LocalDate; | ||
import java.time.LocalDateTime; | ||
import java.time.LocalTime; | ||
|
||
public class Reservation { | ||
private final Long id; | ||
private final Name name; | ||
private final ReservationTime time; | ||
|
||
public Reservation(Long id, String name, LocalDate reservationDate, LocalTime reservationStartTime) { | ||
this.id = id; | ||
this.name = new Name(name); | ||
this.time = new ReservationTime(LocalDateTime.of(reservationDate, reservationStartTime)); | ||
} | ||
|
||
public Reservation(String name, LocalDate reservationDate, LocalTime reservationStartTime) { | ||
this(null, name, reservationDate, reservationStartTime); | ||
} | ||
|
||
public long getId() { | ||
return id; | ||
} | ||
Comment on lines
+22
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [질문] There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 약간의 의도를 담았는데 그 의도가 희석되었을 수도 있겠네 Reservation의 Id는 Null이 될 수도 있고 값을 담고 있을 수도 있어. |
||
|
||
public String getName() { | ||
return name.getName(); | ||
} | ||
|
||
public LocalDateTime getStartDateTime() { | ||
return time.getStartDateTime(); | ||
} | ||
|
||
public LocalDateTime getEndDateTime() { | ||
return time.getEndDateTime(); | ||
} | ||
|
||
public LocalDate getStartDate() { | ||
return time.getStartDate(); | ||
} | ||
|
||
public LocalTime getStartTime() { | ||
return time.getStart(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package roomescape.entity; | ||
|
||
import java.time.LocalDate; | ||
import java.time.LocalDateTime; | ||
import java.time.LocalTime; | ||
|
||
public class ReservationTime { | ||
public static final int RESERVATION_DURATION_HOUR = 1; | ||
|
||
private final LocalDateTime start; | ||
|
||
public ReservationTime(LocalDateTime start) { | ||
validateNonNull(start); | ||
this.start = start; | ||
} | ||
|
||
private void validateNonNull(LocalDateTime startTime) { | ||
if (startTime == null) { | ||
throw new NullPointerException("예약 시간은 Null이 될 수 없습니다"); | ||
} | ||
} | ||
Comment on lines
+17
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. name의 validateNonNull이랑 동일한 이야기! |
||
|
||
public LocalDateTime getStartDateTime() { | ||
return start; | ||
} | ||
|
||
public LocalDateTime getEndDateTime() { | ||
return start.plusHours(RESERVATION_DURATION_HOUR); | ||
} | ||
|
||
public LocalDate getStartDate() { | ||
return start.toLocalDate(); | ||
} | ||
|
||
public LocalTime getStart() { | ||
return start.toLocalTime(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
package roomescape.repository; | ||
|
||
import java.sql.Date; | ||
import java.sql.PreparedStatement; | ||
import java.sql.Statement; | ||
import java.sql.Time; | ||
import java.time.LocalDate; | ||
import java.time.LocalTime; | ||
import java.time.format.DateTimeFormatter; | ||
import java.util.List; | ||
import java.util.Optional; | ||
import org.springframework.dao.EmptyResultDataAccessException; | ||
import org.springframework.jdbc.core.JdbcTemplate; | ||
import org.springframework.jdbc.core.RowMapper; | ||
import org.springframework.jdbc.support.GeneratedKeyHolder; | ||
import org.springframework.jdbc.support.KeyHolder; | ||
import org.springframework.stereotype.Repository; | ||
import roomescape.entity.Reservation; | ||
import roomescape.entity.ReservationTime; | ||
|
||
@Repository | ||
public class ReservationRepository { | ||
private final JdbcTemplate jdbcTemplate; | ||
|
||
public ReservationRepository(JdbcTemplate jdbcTemplate) { | ||
this.jdbcTemplate = jdbcTemplate; | ||
} | ||
|
||
public List<Reservation> readAll() { | ||
String sql = "select * from reservation"; | ||
return jdbcTemplate.query(sql, reservationRowMapper()); | ||
} | ||
|
||
public Reservation save(Reservation reservation) { | ||
String sql = "insert into reservation (name, date, time) values(?, ?, ?)"; | ||
KeyHolder keyHolder = new GeneratedKeyHolder(); | ||
|
||
jdbcTemplate.update(connection -> { | ||
PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); | ||
ps.setString(1, reservation.getName()); | ||
ps.setDate(2, Date.valueOf(reservation.getStartDate())); | ||
ps.setTime(3, Time.valueOf(reservation.getStartTime())); | ||
return ps; | ||
}, keyHolder); | ||
long savedId = keyHolder.getKey().longValue(); | ||
return new Reservation(savedId, reservation.getName(), reservation.getStartDate(), reservation.getStartTime()); | ||
} | ||
|
||
public Optional<Reservation> findById(long id) { | ||
String sql = "select * from reservation where id=?"; | ||
try { | ||
return Optional.of(jdbcTemplate.queryForObject(sql, new Object[]{id}, reservationRowMapper())); | ||
} catch (EmptyResultDataAccessException ex) { | ||
return Optional.empty(); | ||
} | ||
} | ||
|
||
public void deleteById(long id) { | ||
String sql = "delete from reservation where id=?"; | ||
jdbcTemplate.update(sql, id); | ||
} | ||
|
||
public boolean isAnyReservationConflictWith(Reservation reservation) { | ||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); | ||
String startDateTime = reservation.getStartDateTime().format(formatter); | ||
String endDateTime = reservation.getEndDateTime().format(formatter); | ||
|
||
String sql = "select exists (" + | ||
" select 1 " + | ||
" from reservation " + | ||
" where ? between (date || ' ' || time) and dateadd('HOUR', ?, (date || ' ' || time)) " + | ||
" or ? between (date || ' ' || time) and dateadd('HOUR', ?, (date || ' ' || time)) " + | ||
") as exists_overlap;"; | ||
|
||
boolean conflict = jdbcTemplate.queryForObject(sql, Boolean.class, endDateTime, | ||
ReservationTime.RESERVATION_DURATION_HOUR, startDateTime, ReservationTime.RESERVATION_DURATION_HOUR); | ||
return conflict; | ||
} | ||
Comment on lines
+63
to
+78
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 예외 처리를 열심히 했네 👍 다만, 이 메서드가 너무 구체적이라는 생각이 드네! 재사용성을 생각해서 repository의 메서드는 좀 범용적인 것이 좋다고 생각해. 예를 들어 특정 범위에 있는 예약을 조회하는 방법이 있을 것 같아. 추가로 해당 sql문의 성능적인 측면도 궁금하고! 효율적인 sql문일까? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 해당 쿼리에 대한 평가가 절실했는데 망쵸가 달아주었군 😎 sql문의 성능적인 측면은 어떨까.. 🤔 우선 나는 sql의 성능은 Join의 횟수에서 많이 달라진다고 생각해 |
||
|
||
private RowMapper<Reservation> reservationRowMapper() { | ||
return (rs, rowNum) -> { | ||
long id = rs.getLong("id"); | ||
String name = rs.getString("name"); | ||
LocalDate date = rs.getDate("date").toLocalDate(); | ||
LocalTime time = rs.getTime("time").toLocalTime(); | ||
|
||
return new Reservation(id, name, date, time); | ||
}; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
뒤에 괄호 없애는게 조금 더 깔끔할 거 같은 느낌!
@GetMapping()
->@GetMapping
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
몰랐던 부분 👍 레디 감사!