-
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단계 - 방탈출 예약 관리] 망쵸(김주환) 미션 제출합니다. #7
base: 3juhwan
Are you sure you want to change the base?
Changes from all commits
0a286f1
16a398b
b829c3c
0663d5a
cfa2452
c21deef
948d536
6fd8012
e941bed
03a8a4a
1cec379
ffbed0e
292223e
496ef6b
21b5bf4
5e7ccdf
37aa4e7
537a3e3
67b42f9
f357078
fbba62e
cf6eadd
78822a9
de4f254
f6d740b
8c2213a
0bfbd46
d47c5ae
248c01a
a31c4e0
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 |
---|---|---|
|
@@ -35,3 +35,6 @@ out/ | |
|
||
### VS Code ### | ||
.vscode/ | ||
|
||
### Git Commit Template ### | ||
.gitmessage.txt |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
# 1단계 기능 요구사항 - 홈 화면 | ||
|
||
- [x] `/admin` 으로 `GET` 요청 시 어드민 메인 페이지를 응답한다. | ||
|
||
# 2단계 기능 요구사항 - 예약 조회 | ||
- [x] `/reservations` 으로 `GET` 요청 시 저장된 예약 정보를 응답한다. | ||
- [x] `/admin/reservation` 으로 `GET` 요청 시 예약 관리 페이지를 응답한다. | ||
|
||
# 3단계 기능 요구사항 - 예약 추가/취소 | ||
- [x] `/reservations` 으로 `POST` 요청 시 예약을 추가한다. | ||
- [x] `/reservations/{id}` 으로 `DELETE` 요청 시 해당 `id`의 예약을 삭제한다. | ||
Comment on lines
+1
to
+11
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. 기능 요구사항 👍 |
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; | ||
|
||
@Controller | ||
@RequestMapping("/admin") | ||
public class AdminController { | ||
Comment on lines
+7
to
+9
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. 나도 그 방식이 좋다고 생각해 |
||
@GetMapping | ||
public String home() { | ||
return "admin/index"; | ||
} | ||
|
||
@GetMapping("/reservation") | ||
public String reservation() { | ||
return "admin/reservation-legacy"; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
package roomescape.controller; | ||
|
||
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.domain.Reservation; | ||
import roomescape.dto.ReservationRequestDto; | ||
import roomescape.dto.ReservationResponseDto; | ||
import roomescape.repository.ReservationRepository; | ||
|
||
import java.net.URI; | ||
import java.util.List; | ||
|
||
@RestController | ||
@RequestMapping("/reservations") | ||
public class ReservationController { | ||
private final ReservationRepository reservationRepository; | ||
|
||
public ReservationController(ReservationRepository reservationRepository) { | ||
this.reservationRepository = reservationRepository; | ||
} | ||
|
||
@GetMapping | ||
public ResponseEntity<List<ReservationResponseDto>> findAll() { | ||
List<Reservation> reservations = reservationRepository.findAll(); | ||
List<ReservationResponseDto> reservationResponseDtos = reservations.stream() | ||
.map(ReservationResponseDto::from) | ||
.toList(); | ||
Comment on lines
+30
to
+33
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는 뷰라고 생각했고 DTO는 뷰와 도메인을 모두 알고 있는 컨트롤러에서 다루어져야 한다는 개인적인 생각이야 |
||
|
||
return ResponseEntity.ok(reservationResponseDtos); | ||
} | ||
|
||
@PostMapping | ||
public ResponseEntity<ReservationResponseDto> add(@RequestBody ReservationRequestDto reservationRequestDto) { | ||
Reservation savedReservation = reservationRepository.save(new Reservation( | ||
reservationRequestDto.name(), | ||
reservationRequestDto.date(), | ||
reservationRequestDto.time() | ||
Comment on lines
+40
to
+43
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에서 값을 꺼내와서 Reservation 객체를 생성하고 있네 👍 웨지랑 나눈 이야기가 있는데 궁금하면 한번 확인해보시오! |
||
)); | ||
|
||
return ResponseEntity.created(URI.create("/reservations/" + savedReservation.getId())) | ||
.body(ReservationResponseDto.from(savedReservation)); | ||
} | ||
|
||
@DeleteMapping("/{id}") | ||
public ResponseEntity<Void> delete(@PathVariable(name = "id") long id) { | ||
reservationRepository.deleteById(id); | ||
|
||
return ResponseEntity.noContent().build(); | ||
} | ||
Comment on lines
+50
to
+55
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.
설정에 따라 동작이 달라질 수 있는 코드는 좋지 않기에 지금처럼 |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
package roomescape.domain; | ||
|
||
import java.time.LocalDate; | ||
import java.time.LocalTime; | ||
|
||
public class Reservation { | ||
private final long id; | ||
private final String name; | ||
private final LocalDate date; | ||
private final LocalTime time; | ||
|
||
Comment on lines
+1
to
+11
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. 도메인은 비즈니스 로직을 담은 협력 객체, 엔티티는 DB 테이블과 일대일 매핑되는 영속화 대상 객체 나는 레벨 1 체스 미션에서 위와 가이 학습했었어 다만 망쵸의 현행처럼 도메인과 엔티티를 엄격하게 구분하는 경우는 많지 않다고 하더라! |
||
public Reservation(String name, LocalDate date, LocalTime time) { | ||
this(0L, name, date, time); | ||
} | ||
Comment on lines
+12
to
+14
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. 0과 Null은 의미가 천차만별이라고 생각해 만약 아이디가 없음을 의도한 것이라면 nullable함을 설정해야된다고 생각함! (Wrapper Type) 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. 추가적으로 ID는 null 표현 때문에 Wrapper 타입이 거의 강제된다고 하더라고 🤔 |
||
|
||
public Reservation(long id, String name, LocalDate date, LocalTime time) { | ||
this.id = id; | ||
this.name = name; | ||
this.date = date; | ||
this.time = time; | ||
} | ||
|
||
public boolean hasId(long id) { | ||
return this.id == id; | ||
} | ||
|
||
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,53 @@ | ||
package roomescape.dto; | ||
|
||
import java.time.LocalDate; | ||
import java.time.LocalDateTime; | ||
import java.time.LocalTime; | ||
import java.util.Objects; | ||
|
||
public record ReservationRequestDto( | ||
String name, | ||
LocalDate date, | ||
LocalTime time | ||
) { | ||
private static final int NAME_LENGTH_MIN = 2; | ||
private static final int NAME_LENGTH_MAX = 10; | ||
private static final int RESERVATION_TIME_MIN = 9; | ||
private static final int RESERVATION_TIME_MAX = 20; | ||
|
||
public ReservationRequestDto { | ||
validateName(name); | ||
validateDate(date); | ||
validateTime(time); | ||
validateDateTime(date, time); | ||
} | ||
|
||
public static void validateName(String name) { | ||
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. 아무래도 가장 고민되는 지점이죠? 우린 레벨1에서 특정 도메인의 값 검증은 해당 도메인 클래스에 위임하는게 좋다고 배웠지만. 막상 스프링 관점에서 보면 DTO에서 검증하는게 크게 나쁘지 않은거 같구요 🤔 (더군다나 스프링의 @Validation을 사용하면 훨씬 편리하고 강력하게 검증을 할 수 있는데 말이죠...ㅎ) 값에 대한 검증 책임은 DTO vs Domain 망쵸의 의견이 궁금해요 PS. 무려 7년전에도 같은 주제로 선배 개발자들이 열심히 토론하고있더라구요! 의견 역시 가지각색이고요 👀 |
||
Objects.requireNonNull(name); | ||
if (name.length() < NAME_LENGTH_MIN || NAME_LENGTH_MAX < name.length()) { | ||
throw new IllegalArgumentException(); | ||
} | ||
} | ||
|
||
private void validateDate(LocalDate date) { | ||
Objects.requireNonNull(date); | ||
if (date.isBefore(LocalDate.now())) { | ||
throw new IllegalArgumentException(); | ||
} | ||
} | ||
|
||
private void validateTime(LocalTime time) { | ||
Objects.requireNonNull(time); | ||
if (time.isBefore(LocalTime.of(RESERVATION_TIME_MIN, 0)) | ||
|| time.isAfter(LocalTime.of(RESERVATION_TIME_MAX, 0))) { | ||
throw new IllegalArgumentException(); | ||
} | ||
Comment on lines
+25
to
+44
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. 어떤 것을 검증하는지 확실한 메서드 명을 주는게 좋을 듯!? 추가적으로 망쵸의 결론도 궁금하군 🤔 |
||
} | ||
|
||
private void validateDateTime(LocalDate date, LocalTime time) { | ||
LocalDateTime localDateTime = LocalDateTime.of(date, time); | ||
if (localDateTime.isBefore(LocalDateTime.now())) { | ||
throw new IllegalArgumentException(); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package roomescape.dto; | ||
|
||
import com.fasterxml.jackson.annotation.JsonFormat; | ||
import roomescape.domain.Reservation; | ||
|
||
import java.time.LocalDate; | ||
import java.time.LocalTime; | ||
|
||
public record ReservationResponseDto(long id, String name, LocalDate date, | ||
@JsonFormat(pattern = "HH:mm") LocalTime time) { | ||
Comment on lines
+9
to
+10
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 static ReservationResponseDto from(Reservation reservation) { | ||
return new ReservationResponseDto( | ||
reservation.getId(), | ||
reservation.getName(), | ||
reservation.getDate(), | ||
reservation.getTime() | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
package roomescape.repository; | ||
|
||
import org.springframework.context.annotation.Primary; | ||
import org.springframework.jdbc.core.JdbcTemplate; | ||
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; | ||
import org.springframework.jdbc.core.namedparam.SqlParameterSource; | ||
import org.springframework.jdbc.core.simple.SimpleJdbcInsert; | ||
import org.springframework.stereotype.Repository; | ||
import roomescape.domain.Reservation; | ||
|
||
import javax.sql.DataSource; | ||
import java.util.List; | ||
|
||
@Primary | ||
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 | ||
public class H2ReservationRepositoryImpl implements ReservationRepository { | ||
Comment on lines
+15
to
+16
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.
|
||
private final JdbcTemplate jdbcTemplate; | ||
private final SimpleJdbcInsert jdbcInsert; | ||
|
||
public H2ReservationRepositoryImpl(JdbcTemplate jdbcTemplate, DataSource dataSource) { | ||
this.jdbcTemplate = jdbcTemplate; | ||
this.jdbcInsert = new SimpleJdbcInsert(dataSource) | ||
.withTableName("reservation") | ||
.usingGeneratedKeyColumns("id"); | ||
} | ||
|
||
@Override | ||
public List<Reservation> findAll() { | ||
String sql = "select * from reservation"; | ||
|
||
return jdbcTemplate.query(sql, (resultSet, rowNum) -> new Reservation( | ||
resultSet.getLong("id"), | ||
resultSet.getString("name"), | ||
resultSet.getDate("date").toLocalDate(), | ||
resultSet.getTime("time").toLocalTime() | ||
)); | ||
} | ||
|
||
@Override | ||
public Reservation save(Reservation reservation) { | ||
SqlParameterSource source = new BeanPropertySqlParameterSource(reservation); | ||
long reservationId = jdbcInsert.executeAndReturnKey(source).longValue(); | ||
|
||
return new Reservation( | ||
reservationId, | ||
reservation.getName(), | ||
reservation.getDate(), | ||
reservation.getTime() | ||
); | ||
} | ||
|
||
@Override | ||
public void deleteById(long id) { | ||
String sql = "delete from reservation where id = ?"; | ||
|
||
jdbcTemplate.update(sql, id); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package roomescape.repository; | ||
|
||
import org.springframework.stereotype.Repository; | ||
import roomescape.domain.Reservation; | ||
|
||
import java.util.ArrayList; | ||
import java.util.Collections; | ||
import java.util.List; | ||
import java.util.concurrent.atomic.AtomicLong; | ||
|
||
@Repository | ||
public class InMemoryReservationRepositoryImpl implements ReservationRepository { | ||
private final List<Reservation> reservations = Collections.synchronizedList(new ArrayList<>()); | ||
private final AtomicLong index = new AtomicLong(0); | ||
Comment on lines
+13
to
+14
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.
|
||
|
||
@Override | ||
public List<Reservation> findAll() { | ||
return Collections.unmodifiableList(reservations); | ||
} | ||
|
||
@Override | ||
public Reservation save(final Reservation reservation) { | ||
Reservation newReservation = new Reservation(index.incrementAndGet(), reservation.getName(), | ||
reservation.getDate(), reservation.getTime()); | ||
reservations.add(newReservation); | ||
|
||
return newReservation; | ||
} | ||
|
||
@Override | ||
public void deleteById(final long id) { | ||
reservations.stream() | ||
.filter(reservation -> reservation.hasId(id)) | ||
.findFirst() | ||
.ifPresent(reservations::remove); | ||
} | ||
|
||
void deleteAll() { | ||
reservations.clear(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package roomescape.repository; | ||
|
||
import roomescape.domain.Reservation; | ||
|
||
import java.util.List; | ||
|
||
public interface ReservationRepository { | ||
List<Reservation> findAll(); | ||
|
||
Reservation save(Reservation reservation); | ||
|
||
void deleteById(long id); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
spring.h2.console.enabled=true | ||
spring.h2.console.path=/h2-console | ||
spring.datasource.url=jdbc:h2:mem:database |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
DROP TABLE IF EXISTS reservation; | ||
|
||
CREATE TABLE reservation | ||
( | ||
id BIGINT NOT NULL AUTO_INCREMENT, | ||
name VARCHAR(255) NOT NULL, | ||
date VARCHAR(255) NOT NULL, | ||
time VARCHAR(255) NOT NULL, | ||
PRIMARY KEY (id) | ||
); |
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.
예약 날짜는 현재보다 이전일 수 없다 등등 여러 도메인 규칙들이 보이는데 이러한 것도 정리되어 있는 것이 좋을 것 같아!
레벨 - 1을 까먹지 말라는 네오의 가르침..!