diff --git a/build.gradle b/build.gradle index 57267157c..0e4d1f957 100644 --- a/build.gradle +++ b/build.gradle @@ -16,8 +16,18 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'io.rest-assured:rest-assured:5.3.1' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + + implementation 'org.springframework.boot:spring-boot-starter-validation' + + implementation 'org.springframework.boot:spring-boot-starter-jdbc' + runtimeOnly 'com.h2database:h2' } test { useJUnitPlatform() } + diff --git a/src/main/java/roomescape/MainPageController.java b/src/main/java/roomescape/MainPageController.java new file mode 100644 index 000000000..741228337 --- /dev/null +++ b/src/main/java/roomescape/MainPageController.java @@ -0,0 +1,21 @@ +package roomescape; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class MainPageController { + + @GetMapping("/") + public String showHomePage() { + return "home"; + } + + @GetMapping("/reservation") + public String showReservationForm() { + return "new-reservation"; + } + + @GetMapping("/time") + public String showTimeForm() { return "time"; } +} diff --git a/src/main/java/roomescape/RoomescapeApplication.java b/src/main/java/roomescape/RoomescapeApplication.java index 702706791..2ca0f743f 100644 --- a/src/main/java/roomescape/RoomescapeApplication.java +++ b/src/main/java/roomescape/RoomescapeApplication.java @@ -8,5 +8,4 @@ public class RoomescapeApplication { public static void main(String[] args) { SpringApplication.run(RoomescapeApplication.class, args); } - } diff --git a/src/main/java/roomescape/business/ReservationService.java b/src/main/java/roomescape/business/ReservationService.java new file mode 100644 index 000000000..b4b2e13d7 --- /dev/null +++ b/src/main/java/roomescape/business/ReservationService.java @@ -0,0 +1,42 @@ +package roomescape.business; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import roomescape.domain.Reservation; +import roomescape.domain.ReservationTime; +import roomescape.persistence.JdbcReservationRepository; +import roomescape.persistence.JdbcReservationTimeRepository; +import roomescape.presentation.dto.ReservationDto; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ReservationService { + + private final JdbcReservationRepository reservationRepository; + private final JdbcReservationTimeRepository timeRepository; + + public Reservation addReservation(ReservationDto reservationDto) { + Reservation reservation = convertToReservationEntity(reservationDto); + return reservationRepository.save(reservation); + } + + public List checkReservations() { + return reservationRepository.findAll(); + } + + public void deleteReservation(Long reservationId) { + reservationRepository.delete(reservationId); + } + + private Reservation convertToReservationEntity(ReservationDto reservationDto) { + ReservationTime time = convertToTimeEntity(reservationDto); + return new Reservation(null, reservationDto.name(), reservationDto.date(), time); + } + + private ReservationTime convertToTimeEntity(ReservationDto reservationDto) { + Long timeId = reservationDto.time(); + return timeRepository.findById(timeId); + } +} diff --git a/src/main/java/roomescape/business/ReservationTimeService.java b/src/main/java/roomescape/business/ReservationTimeService.java new file mode 100644 index 000000000..c14304569 --- /dev/null +++ b/src/main/java/roomescape/business/ReservationTimeService.java @@ -0,0 +1,34 @@ +package roomescape.business; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import roomescape.domain.ReservationTime; +import roomescape.persistence.JdbcReservationTimeRepository; +import roomescape.presentation.dto.ReservationTimeDto; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ReservationTimeService { + + private final JdbcReservationTimeRepository repository; + + public ReservationTime addTime(ReservationTimeDto reservationTimeDto) { + ReservationTime time = convertToEntity(reservationTimeDto); + return repository.save(time); + } + + public List checkTimes() { + return repository.findAll(); + } + + public void deleteTime(Long timeId) { + repository.delete(timeId); + } + + private ReservationTime convertToEntity(ReservationTimeDto reservationTimeDto) { + return new ReservationTime(null, reservationTimeDto.time()); + } + +} diff --git a/src/main/java/roomescape/persistence/JdbcReservationRepository.java b/src/main/java/roomescape/persistence/JdbcReservationRepository.java new file mode 100644 index 000000000..fdbcdda5c --- /dev/null +++ b/src/main/java/roomescape/persistence/JdbcReservationRepository.java @@ -0,0 +1,99 @@ +package roomescape.persistence; + +import lombok.RequiredArgsConstructor; +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.domain.Reservation; +import roomescape.domain.ReservationTime; +import roomescape.exception.NotFoundReservationException; + +import java.sql.PreparedStatement; +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class JdbcReservationRepository implements ReservationRepository { + + private final JdbcTemplate jdbcTemplate; + + @Override + public Reservation save(Reservation reservation) { + String sql = "insert into reservation (name, date, time_id) values (?,?,?)"; + + KeyHolder keyHolder = new GeneratedKeyHolder(); + + jdbcTemplate.update(connection -> { + PreparedStatement ps = connection.prepareStatement(sql, new String[]{"id"}); + ps.setString(1, reservation.getName()); + ps.setString(2, reservation.getDate()); + ps.setLong(3, reservation.getTime().getId()); + return ps; + }, keyHolder); + + Long generatedAutoId = keyHolder.getKey().longValue(); + return new Reservation(generatedAutoId, reservation.getName(), reservation.getDate(), reservation.getTime()); + } + + @Override + public Reservation findById(Long reservationId) { + String sql = "select id, name, date, time_id from reservation where id = ?"; + + try { + return jdbcTemplate.queryForObject(sql, reservationMapperForFindById(), reservationId); + } catch (EmptyResultDataAccessException e) { + throw new NotFoundReservationException(); + } + } + + @Override + public List findAll() { + String sql = "select \n" + + " r.id as reservation_id, \n" + + " r.name, \n" + + " r.date, \n" + + " t.id as time_id, \n" + + " t.time as time_value \n" + + "from reservation as r inner join time as t on r.time_id = t.id"; + + return jdbcTemplate.query(sql, reservationMapperForFindAll()); + } + + @Override + public void delete(Long reservationId) { + Reservation deletedReservation = this.findById(reservationId); + + String sql = "delete from reservation where id = ?"; + jdbcTemplate.update(sql, deletedReservation.getId()); + } + + private RowMapper reservationMapperForFindById() { + return ((rs, rowNum) -> { + + ReservationTime time = new ReservationTime(rs.getLong("time_id"), null); + + return new Reservation( + rs.getLong("id"), + rs.getString("name"), + rs.getString("date"), + time); + }); + } + + private RowMapper reservationMapperForFindAll() { + return ((rs, rowNum) -> { + + ReservationTime time = new ReservationTime(rs.getLong("time_id"), rs.getString("time_value")); + + return new Reservation( + rs.getLong("id"), + rs.getString("name"), + rs.getString("date"), + time + ); + }); + } +} diff --git a/src/main/java/roomescape/persistence/JdbcReservationTimeRepository.java b/src/main/java/roomescape/persistence/JdbcReservationTimeRepository.java new file mode 100644 index 000000000..3bf7b7c45 --- /dev/null +++ b/src/main/java/roomescape/persistence/JdbcReservationTimeRepository.java @@ -0,0 +1,72 @@ +package roomescape.persistence; + +import lombok.RequiredArgsConstructor; +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.domain.ReservationTime; +import roomescape.exception.NotFoundReservationTimeException; + +import java.sql.PreparedStatement; +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class JdbcReservationTimeRepository implements ReservationTimeRepository { + + private final JdbcTemplate jdbcTemplate; + + @Override + public ReservationTime save(ReservationTime time) { + String sql = "insert into time (time) values (?)"; + + KeyHolder keyHolder = new GeneratedKeyHolder(); + + jdbcTemplate.update(connection -> { + PreparedStatement ps = connection.prepareStatement(sql, new String[]{"id"}); + ps.setString(1, time.getTime()); + return ps; + }, keyHolder); + + Long generatedAutoId = keyHolder.getKey().longValue(); + return new ReservationTime(generatedAutoId, time.getTime()); + } + + @Override + public ReservationTime findById(Long timeId) { + String sql = "select id, time from time where id = ?"; + + try { + return jdbcTemplate.queryForObject(sql, timeMapper(), timeId); + } catch (EmptyResultDataAccessException e) { + throw new NotFoundReservationTimeException(); + } + } + + @Override + public List findAll() { + String sql = "select id, time from time"; + + return jdbcTemplate.query(sql, timeMapper()); + } + + @Override + public void delete(Long timeId) { + ReservationTime deletedTime = this.findById(timeId); + + String sql = "delete from time where id = ?"; + jdbcTemplate.update(sql, deletedTime.getId()); + } + + private RowMapper timeMapper() { + return ((rs, rowNum) -> { + return new ReservationTime( + rs.getLong("id"), + rs.getString("time") + ); + }); + } +} diff --git a/src/main/java/roomescape/persistence/MemoryReservationRepository.java b/src/main/java/roomescape/persistence/MemoryReservationRepository.java new file mode 100644 index 000000000..ebff7a103 --- /dev/null +++ b/src/main/java/roomescape/persistence/MemoryReservationRepository.java @@ -0,0 +1,46 @@ +package roomescape.persistence; + +import roomescape.domain.Reservation; +import roomescape.exception.NotFoundReservationException; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +public class MemoryReservationRepository implements ReservationRepository { + + private static Map reservationStore = new ConcurrentHashMap<>(); + private static AtomicLong index = new AtomicLong(1); + + @Override + public Reservation save(Reservation reservation) { + + Long reservationId = index.getAndIncrement(); + reservation.setId(reservationId); + reservationStore.put(reservationId, reservation); + + return reservation; + } + + @Override + public Reservation findById(Long reservationId) { + Reservation reservation = reservationStore.get(reservationId); + if (reservation == null) { + throw new NotFoundReservationException(); + } + return reservation; + } + + @Override + public List findAll() { + return new ArrayList<>(reservationStore.values()); + } + + @Override + public void delete(Long reservationId) { + Reservation deletedReservation = this.findById(reservationId); + reservationStore.remove(deletedReservation.getId()); + } +} diff --git a/src/main/java/roomescape/persistence/ReservationRepository.java b/src/main/java/roomescape/persistence/ReservationRepository.java new file mode 100644 index 000000000..d9cfaddcc --- /dev/null +++ b/src/main/java/roomescape/persistence/ReservationRepository.java @@ -0,0 +1,17 @@ +package roomescape.persistence; + +import roomescape.domain.Reservation; + +import java.util.List; + +public interface ReservationRepository { + + Reservation save(Reservation reservation); + + Reservation findById(Long reservationId); + + List findAll(); + + void delete(Long reservationId); + +} diff --git a/src/main/java/roomescape/persistence/ReservationTimeRepository.java b/src/main/java/roomescape/persistence/ReservationTimeRepository.java new file mode 100644 index 000000000..1957fd6a6 --- /dev/null +++ b/src/main/java/roomescape/persistence/ReservationTimeRepository.java @@ -0,0 +1,16 @@ +package roomescape.persistence; + +import roomescape.domain.ReservationTime; + +import java.util.List; + +public interface ReservationTimeRepository { + + ReservationTime save(ReservationTime time); + + ReservationTime findById(Long timeId); + + List findAll(); + + void delete(Long timeId); +} diff --git a/src/main/java/roomescape/presentation/ReservationController.java b/src/main/java/roomescape/presentation/ReservationController.java new file mode 100644 index 000000000..260202018 --- /dev/null +++ b/src/main/java/roomescape/presentation/ReservationController.java @@ -0,0 +1,36 @@ +package roomescape.presentation; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import roomescape.domain.Reservation; +import roomescape.presentation.dto.ReservationDto; +import roomescape.business.ReservationService; + +import java.net.URI; +import java.util.List; + +@RestController +@RequiredArgsConstructor +public class ReservationController { + + private final ReservationService reservationService; + + @GetMapping("/reservations") + public ResponseEntity> readReservation() { + return ResponseEntity.ok(reservationService.checkReservations()); + } + + @PostMapping("/reservations") + public ResponseEntity createReservation(@Valid @RequestBody ReservationDto reservationDto) { + Reservation savedReservation = reservationService.addReservation(reservationDto); + return ResponseEntity.created(URI.create("/reservations/" + savedReservation.getId())).body(savedReservation); + } + + @DeleteMapping("/reservations/{reservationId}") + public ResponseEntity deleteReservation(@PathVariable Long reservationId) { + reservationService.deleteReservation(reservationId); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/roomescape/presentation/ReservationTimeController.java b/src/main/java/roomescape/presentation/ReservationTimeController.java new file mode 100644 index 000000000..b8a785b2e --- /dev/null +++ b/src/main/java/roomescape/presentation/ReservationTimeController.java @@ -0,0 +1,36 @@ +package roomescape.presentation; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import roomescape.business.ReservationTimeService; +import roomescape.domain.ReservationTime; +import roomescape.presentation.dto.ReservationTimeDto; + +import java.net.URI; +import java.util.List; + +@RestController +@RequiredArgsConstructor +public class ReservationTimeController { + + private final ReservationTimeService reservationTimeService; + + @GetMapping("/times") + public ResponseEntity> readTimes() { + return ResponseEntity.ok(reservationTimeService.checkTimes()); + } + + @PostMapping("/times") + public ResponseEntity createTime(@Valid @RequestBody ReservationTimeDto reservationTimeDto) { + ReservationTime savedTime = reservationTimeService.addTime(reservationTimeDto); + return ResponseEntity.created(URI.create("/times/" + savedTime.getId())).body(savedTime); + } + + @DeleteMapping("/times/{timeId}") + public ResponseEntity deleteTime(@PathVariable Long timeId) { + reservationTimeService.deleteTime(timeId); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/roomescape/presentation/dto/ReservationDto.java b/src/main/java/roomescape/presentation/dto/ReservationDto.java new file mode 100644 index 000000000..d173fe92a --- /dev/null +++ b/src/main/java/roomescape/presentation/dto/ReservationDto.java @@ -0,0 +1,10 @@ +package roomescape.presentation.dto; + +import jakarta.validation.constraints.NotBlank; + +public record ReservationDto( + @NotBlank(message = "예약자 이름을 입력하세요.") String name, + @NotBlank(message = "예약 날짜를 입력하세요.") String date, + Long time +) { +} diff --git a/src/main/java/roomescape/presentation/dto/ReservationTimeDto.java b/src/main/java/roomescape/presentation/dto/ReservationTimeDto.java new file mode 100644 index 000000000..edd66edb2 --- /dev/null +++ b/src/main/java/roomescape/presentation/dto/ReservationTimeDto.java @@ -0,0 +1,8 @@ +package roomescape.presentation.dto; + +import jakarta.validation.constraints.NotBlank; + +public record ReservationTimeDto( + @NotBlank(message = "에약 시간을 입력하세요.") String time +) { +} diff --git a/src/main/java/roomescape/reservation/business/ReservationService.java b/src/main/java/roomescape/reservation/business/ReservationService.java new file mode 100644 index 000000000..a244beaf5 --- /dev/null +++ b/src/main/java/roomescape/reservation/business/ReservationService.java @@ -0,0 +1,33 @@ +package roomescape.reservation.business; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import roomescape.reservation.domain.Reservation; +import roomescape.reservation.presentation.dto.ReservationDto; +import roomescape.reservation.persistence.ReservationRepository; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ReservationService { + + private final ReservationRepository repository; + + public Reservation addReservation(ReservationDto reservationDto) { + Reservation reservation = convertToEntity(reservationDto); + return repository.save(reservation); + } + + public List checkReservations() { + return repository.findAll(); + } + + public void deleteReservation(Long reservationId) { + repository.delete(reservationId); + } + + private Reservation convertToEntity(ReservationDto reservationDto) { + return new Reservation(null, reservationDto.name(), reservationDto.date(), reservationDto.time()); + } +} diff --git a/src/main/java/roomescape/reservation/domain/Reservation.java b/src/main/java/roomescape/reservation/domain/Reservation.java new file mode 100644 index 000000000..6307be847 --- /dev/null +++ b/src/main/java/roomescape/reservation/domain/Reservation.java @@ -0,0 +1,26 @@ +package roomescape.domain; + +import lombok.Getter; + +@Getter +public class Reservation { + + private Long id; + private String name; + private String date; + private ReservationTime time; + + public Reservation() { + } + + public Reservation(Long id, String name, String date, ReservationTime time) { + this.id = id; + this.name = name; + this.date = date; + this.time = time; + } + + public void setId(Long id) { + this.id = id; + } +} diff --git a/src/main/java/roomescape/reservation/domain/ReservationTime.java b/src/main/java/roomescape/reservation/domain/ReservationTime.java new file mode 100644 index 000000000..16fcf173a --- /dev/null +++ b/src/main/java/roomescape/reservation/domain/ReservationTime.java @@ -0,0 +1,18 @@ +package roomescape.domain; + +import lombok.Getter; + +@Getter +public class ReservationTime { + + private Long id; + private String time; + + public ReservationTime() { + } + + public ReservationTime(Long id, String time) { + this.id = id; + this.time = time; + } +} diff --git a/src/main/java/roomescape/reservation/persistence/JdbcReservationRepository.java b/src/main/java/roomescape/reservation/persistence/JdbcReservationRepository.java new file mode 100644 index 000000000..20d29211a --- /dev/null +++ b/src/main/java/roomescape/reservation/persistence/JdbcReservationRepository.java @@ -0,0 +1,77 @@ +package roomescape.reservation.persistence; + +import lombok.RequiredArgsConstructor; +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.reservation.domain.Reservation; +import roomescape.reservation.presentation.exception.NotFoundReservationException; + +import java.sql.PreparedStatement; +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class JdbcReservationRepository implements ReservationRepository { + + private final JdbcTemplate jdbcTemplate; + + @Override + 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, new String[]{"id"}); + ps.setString(1, reservation.getName()); + ps.setString(2, reservation.getDate()); + ps.setString(3, reservation.getTime()); + return ps; + }, keyHolder); + + Long generatedAutoId = keyHolder.getKey().longValue(); + return new Reservation(generatedAutoId, reservation.getName(), reservation.getDate(), reservation.getTime()); + } + + @Override + public Reservation findById(Long reservationId) { + String sql = "select id, name, date, time from reservation where id = ?"; + + try { + return jdbcTemplate.queryForObject(sql, reservationMapper(), reservationId); + } catch (EmptyResultDataAccessException e) { + throw new NotFoundReservationException(); + } + } + + @Override + public List findAll() { + String sql = "select id, name, date, time from reservation"; + + return jdbcTemplate.query(sql, reservationMapper()); + } + + @Override + public void delete(Long reservationId) { + Reservation deletedReservation = this.findById(reservationId); + + String sql = "delete from reservation where id = ?"; + jdbcTemplate.update(sql, deletedReservation.getId()); + + } + + private RowMapper reservationMapper() { + return ((rs, rowNum) -> { + return new Reservation( + rs.getLong("id"), + rs.getString("name"), + rs.getString("date"), + rs.getString("time") + ); + }); + } +} diff --git a/src/main/java/roomescape/reservation/persistence/MemoryReservationRepository.java b/src/main/java/roomescape/reservation/persistence/MemoryReservationRepository.java new file mode 100644 index 000000000..32cabf2e1 --- /dev/null +++ b/src/main/java/roomescape/reservation/persistence/MemoryReservationRepository.java @@ -0,0 +1,46 @@ +package roomescape.reservation.persistence; + +import roomescape.reservation.domain.Reservation; +import roomescape.reservation.presentation.exception.NotFoundReservationException; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +public class MemoryReservationRepository implements ReservationRepository { + + private static Map reservationStore = new ConcurrentHashMap<>(); + private static AtomicLong index = new AtomicLong(1); + + @Override + public Reservation save(Reservation reservation) { + + Long reservationId = index.getAndIncrement(); + reservation.setId(reservationId); + reservationStore.put(reservationId, reservation); + + return reservation; + } + + @Override + public Reservation findById(Long reservationId) { + Reservation reservation = reservationStore.get(reservationId); + if (reservation == null) { + throw new NotFoundReservationException(); + } + return reservation; + } + + @Override + public List findAll() { + return new ArrayList<>(reservationStore.values()); + } + + @Override + public void delete(Long reservationId) { + Reservation deletedReservation = this.findById(reservationId); + reservationStore.remove(deletedReservation.getId()); + } +} diff --git a/src/main/java/roomescape/reservation/persistence/ReservationRepository.java b/src/main/java/roomescape/reservation/persistence/ReservationRepository.java new file mode 100644 index 000000000..d0a7af0be --- /dev/null +++ b/src/main/java/roomescape/reservation/persistence/ReservationRepository.java @@ -0,0 +1,17 @@ +package roomescape.reservation.persistence; + +import roomescape.reservation.domain.Reservation; + +import java.util.List; + +public interface ReservationRepository { + + Reservation save(Reservation reservation); + + Reservation findById(Long reservationId); + + List findAll(); + + void delete(Long reservationId); + +} diff --git a/src/main/java/roomescape/reservation/presentation/ReservationController.java b/src/main/java/roomescape/reservation/presentation/ReservationController.java new file mode 100644 index 000000000..8e1247142 --- /dev/null +++ b/src/main/java/roomescape/reservation/presentation/ReservationController.java @@ -0,0 +1,36 @@ +package roomescape.reservation.presentation; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import roomescape.reservation.domain.Reservation; +import roomescape.reservation.presentation.dto.ReservationDto; +import roomescape.reservation.business.ReservationService; + +import java.net.URI; +import java.util.List; + +@RestController +@RequiredArgsConstructor +public class ReservationController { + + private final ReservationService reservationService; + + @GetMapping("/reservations") + public ResponseEntity> readReservation() { + return ResponseEntity.ok(reservationService.checkReservations()); + } + + @PostMapping("/reservations") + public ResponseEntity createReservation(@Valid @RequestBody ReservationDto reservationDto) { + Reservation savedReservation = reservationService.addReservation(reservationDto); + return ResponseEntity.created(URI.create("/reservations/" + savedReservation.getId())).body(savedReservation); + } + + @DeleteMapping("/reservations/{reservationId}") + public ResponseEntity deleteReservation(@PathVariable Long reservationId) { + reservationService.deleteReservation(reservationId); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/roomescape/reservation/presentation/dto/ReservationDto.java b/src/main/java/roomescape/reservation/presentation/dto/ReservationDto.java new file mode 100644 index 000000000..60e5e62a3 --- /dev/null +++ b/src/main/java/roomescape/reservation/presentation/dto/ReservationDto.java @@ -0,0 +1,10 @@ +package roomescape.reservation.presentation.dto; + +import jakarta.validation.constraints.NotBlank; + +public record ReservationDto( + @NotBlank(message = "예약자 이름을 입력하세요.") String name, + @NotBlank(message = "예약 날짜를 입력하세요.") String date, + @NotBlank(message = "예약 시간을 입력하세요.") String time +) { +} diff --git a/src/main/java/roomescape/reservation/presentation/exception/GeneralExceptionHandler.java b/src/main/java/roomescape/reservation/presentation/exception/GeneralExceptionHandler.java new file mode 100644 index 000000000..56a19c175 --- /dev/null +++ b/src/main/java/roomescape/reservation/presentation/exception/GeneralExceptionHandler.java @@ -0,0 +1,39 @@ +package roomescape.exception; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +import java.util.HashMap; +import java.util.Map; + +@ControllerAdvice +@Slf4j +public class GeneralExceptionHandler { + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidationExceptions(MethodArgumentNotValidException e) { + Map errors = new HashMap<>(); + + for (FieldError error : e.getBindingResult().getFieldErrors()) { + errors.put(error.getField(), error.getDefaultMessage()); + log.info("validation error on field {} : {}", error.getField(), error.getDefaultMessage()); + } + + return ResponseEntity.badRequest().body(errors); + } + + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity handleInvalidTypeException(HttpMessageNotReadableException e) { + return ResponseEntity.badRequest().body(e.getMessage()); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(Exception e) { + return ResponseEntity.internalServerError().body(e.getMessage()); + } +} diff --git a/src/main/java/roomescape/reservation/presentation/exception/MainPageExceptionHandler.java b/src/main/java/roomescape/reservation/presentation/exception/MainPageExceptionHandler.java new file mode 100644 index 000000000..df65c0a60 --- /dev/null +++ b/src/main/java/roomescape/reservation/presentation/exception/MainPageExceptionHandler.java @@ -0,0 +1,16 @@ +package roomescape.exception; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import roomescape.MainPageController; + +@Slf4j +@ControllerAdvice(assignableTypes = MainPageController.class) +public class MainPageExceptionHandler { + @ExceptionHandler(Exception.class) + public String handleException(Exception e) { + log.error("error: " + e.getMessage()); + return "error/500"; //view 렌더링 페이지는 만들지 않음! + } +} diff --git a/src/main/java/roomescape/reservation/presentation/exception/NotFoundReservationException.java b/src/main/java/roomescape/reservation/presentation/exception/NotFoundReservationException.java new file mode 100644 index 000000000..ef6a65877 --- /dev/null +++ b/src/main/java/roomescape/reservation/presentation/exception/NotFoundReservationException.java @@ -0,0 +1,10 @@ +package roomescape.reservation.presentation.exception; + +public class NotFoundReservationException extends RuntimeException { + + private static final String NOT_FOUND_RESERVATION_MESSAGE = "예악을 찾을 수 없습니다."; + + public NotFoundReservationException() { + super(NOT_FOUND_RESERVATION_MESSAGE); + } +} diff --git a/src/main/java/roomescape/reservation/presentation/exception/NotFoundReservationTimeException.java b/src/main/java/roomescape/reservation/presentation/exception/NotFoundReservationTimeException.java new file mode 100644 index 000000000..263430f31 --- /dev/null +++ b/src/main/java/roomescape/reservation/presentation/exception/NotFoundReservationTimeException.java @@ -0,0 +1,10 @@ +package roomescape.exception; + +public class NotFoundReservationTimeException extends RuntimeException { + + private static final String NOT_FOUND_TIME_MESSAGE = "시간을 찾을 수 없습니다."; + + public NotFoundReservationTimeException(){ + super(NOT_FOUND_TIME_MESSAGE); + } +} diff --git a/src/main/java/roomescape/reservation/presentation/exception/ReservationExceptionHandler.java b/src/main/java/roomescape/reservation/presentation/exception/ReservationExceptionHandler.java new file mode 100644 index 000000000..f215bc414 --- /dev/null +++ b/src/main/java/roomescape/reservation/presentation/exception/ReservationExceptionHandler.java @@ -0,0 +1,17 @@ +package roomescape.exception; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import roomescape.presentation.ReservationController; + +@Slf4j +@ControllerAdvice(assignableTypes = ReservationController.class) +public class ReservationExceptionHandler { + + @ExceptionHandler(NotFoundReservationException.class) + public ResponseEntity handleNotFoundReservationException(NotFoundReservationException e) { + return ResponseEntity.badRequest().body(e.getMessage()); + } +} diff --git a/src/main/java/roomescape/reservation/presentation/exception/ReservationTimeExceptionHandler.java b/src/main/java/roomescape/reservation/presentation/exception/ReservationTimeExceptionHandler.java new file mode 100644 index 000000000..c6e4db157 --- /dev/null +++ b/src/main/java/roomescape/reservation/presentation/exception/ReservationTimeExceptionHandler.java @@ -0,0 +1,17 @@ +package roomescape.exception; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import roomescape.presentation.ReservationTimeController; + +@Slf4j +@ControllerAdvice(assignableTypes = ReservationTimeController.class) +public class ReservationTimeExceptionHandler { + + @ExceptionHandler(NotFoundReservationTimeException.class) + public ResponseEntity handleNotFoundTimeException(NotFoundReservationTimeException e) { + return ResponseEntity.badRequest().body(e.getMessage()); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index e69de29bb..d2b42566e 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -0,0 +1,4 @@ +# h2-console ??? ?? +spring.h2.console.enabled=true +# db url +spring.datasource.url=jdbc:h2:mem:database diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 000000000..ca4b0ee4c --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,16 @@ +CREATE TABLE time +( + id BIGINT NOT NULL AUTO_INCREMENT, + time VARCHAR(255) NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE reservation +( + id BIGINT NOT NULL AUTO_INCREMENT, + name VARCHAR(255) NOT NULL, + date VARCHAR(255) NOT NULL, + time_id BIGINT, + PRIMARY KEY (id), + FOREIGN KEY (time_id) REFERENCES time(id) +); diff --git a/src/test/java/roomescape/MissionStepTest.java b/src/test/java/roomescape/MissionStepTest.java deleted file mode 100644 index cf4efbe91..000000000 --- a/src/test/java/roomescape/MissionStepTest.java +++ /dev/null @@ -1,19 +0,0 @@ -package roomescape; - -import io.restassured.RestAssured; -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.annotation.DirtiesContext; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) -@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) -public class MissionStepTest { - - @Test - void 일단계() { - RestAssured.given().log().all() - .when().get("/") - .then().log().all() - .statusCode(200); - } -} diff --git a/src/test/java/roomescape/SpringCOREMissionStepTest.java b/src/test/java/roomescape/SpringCOREMissionStepTest.java new file mode 100644 index 000000000..498ddb643 --- /dev/null +++ b/src/test/java/roomescape/SpringCOREMissionStepTest.java @@ -0,0 +1,79 @@ +package roomescape; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.annotation.DirtiesContext; +import roomescape.presentation.ReservationController; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.is; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +public class SpringCOREMissionStepTest { + + @Test + void 팔단계() { + Map params = new HashMap<>(); + params.put("time", "10:00"); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(params) + .when().post("/times") + .then().log().all() + .statusCode(201) + .header("Location", "/times/1"); + + RestAssured.given().log().all() + .when().get("/times") + .then().log().all() + .statusCode(200) + .body("size()", is(1)); + + RestAssured.given().log().all() + .when().delete("/times/1") + .then().log().all() + .statusCode(204); + } + + @Test + void 구단계() { + Map reservation = new HashMap<>(); + reservation.put("name", "브라운"); + reservation.put("date", "2023-08-05"); + reservation.put("time", "10:00"); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(reservation) + .when().post("/reservations") + .then().log().all() + .statusCode(400); + } + + @Autowired + private ReservationController reservationController; + + @Test + void 십단계() { + boolean isJdbcTemplateInjected = false; + + for (Field field : reservationController.getClass().getDeclaredFields()) { + if (field.getType().equals(JdbcTemplate.class)) { + isJdbcTemplateInjected = true; + break; + } + } + + assertThat(isJdbcTemplateInjected).isFalse(); + } +} diff --git a/src/test/java/roomescape/SpringJDBCMissionStepTest.java b/src/test/java/roomescape/SpringJDBCMissionStepTest.java new file mode 100644 index 000000000..d0620dac6 --- /dev/null +++ b/src/test/java/roomescape/SpringJDBCMissionStepTest.java @@ -0,0 +1,80 @@ +package roomescape; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.annotation.DirtiesContext; +import roomescape.domain.Reservation; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +public class SpringJDBCMissionStepTest { + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Test + void 오단계() { + try (Connection connection = jdbcTemplate.getDataSource().getConnection()) { + assertThat(connection).isNotNull(); + assertThat(connection.getCatalog()).isEqualTo("DATABASE"); + assertThat(connection.getMetaData().getTables(null, null, "RESERVATION", null).next()).isTrue(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @Test + void 육단계() { + jdbcTemplate.update("INSERT INTO reservation (name, date, time) VALUES (?, ?, ?)", "브라운", "2023-08-05", "15:40"); + + List reservations = RestAssured.given().log().all() + .when().get("/reservations") + .then().log().all() + .statusCode(200).extract() + .jsonPath().getList(".", Reservation.class); + + Integer count = jdbcTemplate.queryForObject("SELECT count(1) from reservation", Integer.class); + + assertThat(reservations.size()).isEqualTo(count); + } + + @Test + void 칠단계() { + Map params = new HashMap<>(); + params.put("name", "브라운"); + params.put("date", "2023-08-05"); + params.put("time", "10:00"); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(params) + .when().post("/reservations") + .then().log().all() + .statusCode(201) + .header("Location", "/reservations/1"); + + Integer count = jdbcTemplate.queryForObject("SELECT count(1) from reservation", Integer.class); + assertThat(count).isEqualTo(1); + + RestAssured.given().log().all() + .when().delete("/reservations/1") + .then().log().all() + .statusCode(204); + + Integer countAfterDelete = jdbcTemplate.queryForObject("SELECT count(1) from reservation", Integer.class); + assertThat(countAfterDelete).isEqualTo(0); + } + +} diff --git a/src/test/java/roomescape/SpringMVCMissionStepTest.java b/src/test/java/roomescape/SpringMVCMissionStepTest.java new file mode 100644 index 000000000..b306b58a7 --- /dev/null +++ b/src/test/java/roomescape/SpringMVCMissionStepTest.java @@ -0,0 +1,96 @@ +package roomescape; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; + +import java.util.HashMap; +import java.util.Map; + +import static org.hamcrest.Matchers.is; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +public class SpringMVCMissionStepTest { + + @Test + void 일단계() { + RestAssured.given().log().all() + .when().get("/") + .then().log().all() + .statusCode(200); + } + + @Test + void 이단계() { + RestAssured.given().log().all() + .when().get("/reservation") + .then().log().all() + .statusCode(200); + + RestAssured.given().log().all() + .when().get("/reservations") + .then().log().all() + .statusCode(200) + .body("size()", is(0)); // 아직 생성 요청이 없으니 Controller에서 임의로 넣어준 Reservation 갯수 만큼 검증하거나 0개임을 확인하세요. + } + + @Test + void 삼단계() { + Map params = new HashMap<>(); + params.put("name", "브라운"); + params.put("date", "2023-08-05"); + params.put("time", "15:40"); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(params) + .when().post("/reservations") + .then().log().all() + .statusCode(201) + .header("Location", "/reservations/1") + .body("id", is(1)); + + RestAssured.given().log().all() + .when().get("/reservations") + .then().log().all() + .statusCode(200) + .body("size()", is(1)); + + RestAssured.given().log().all() + .when().delete("/reservations/1") + .then().log().all() + .statusCode(204); + + RestAssured.given().log().all() + .when().get("/reservations") + .then().log().all() + .statusCode(200) + .body("size()", is(0)); + } + + @Test + void 사단계() { + Map params = new HashMap<>(); + params.put("name", "브라운"); + params.put("date", ""); + params.put("time", ""); + + // 필요한 인자가 없는 경우 + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(params) + .when().post("/reservations") + .then().log().all() + .statusCode(400); + + // 삭제할 예약이 없는 경우 + RestAssured.given().log().all() + .when().delete("/reservations/1") + .then().log().all() + .statusCode(400); + } + +}