From f83b666e76eabd1dbb98b0ffc7fd172dc5f54429 Mon Sep 17 00:00:00 2001 From: yeo-li Date: Thu, 18 Jun 2026 17:20:42 +0900 Subject: [PATCH 1/2] =?UTF-8?q?refactor:=20=EB=AF=B8=EC=85=98=20=EB=A0=88?= =?UTF-8?q?=EB=B2=A81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 +- .../AdminReservationController.java | 8 + .../controller/ReservationController.java | 7 +- .../dto/command/ReservationCreateCommand.java | 4 +- .../ReservationTimeStartAtResponseDto.java | 7 + .../reservation/entity/Reservation.java | 124 +++++++- .../reservation/mapper/ReservationMapper.java | 9 +- .../repository/JdbcReservationRepository.java | 39 ++- .../repository/ReservationRepository.java | 265 +++++++++++++++++- .../service/ReservationService.java | 47 +++- .../domain/reservation/vo/ReserverName.java | 21 -- .../roomescape/domain/theme/entity/Theme.java | 56 +++- .../theme/repository/JdbcThemeRepository.java | 15 +- .../theme/repository/ThemeRepository.java | 46 ++- .../domain/theme/service/ThemeService.java | 3 +- .../roomescape/domain/time/entity/Time.java | 44 ++- .../time/repository/JdbcTimeRepository.java | 16 +- .../time/repository/TimeRepository.java | 26 +- src/main/resources/application.properties | 9 +- .../AdminReservationControllerTest.java | 17 ++ .../controller/ReservationControllerTest.java | 55 ++-- ...ReservationTransactionIntegrationTest.java | 20 +- .../JdbcReservationRepositoryTest.java | 127 +++++---- .../service/ReservationServiceTest.java | 127 +++++---- .../theme/service/ThemeServiceTest.java | 6 +- ...itoryTest.java => TimeRepositoryTest.java} | 51 ++-- .../fixture/ReservationFixture.java | 3 +- 27 files changed, 824 insertions(+), 330 deletions(-) create mode 100644 src/main/java/roomescape/domain/reservation/dto/response/ReservationTimeStartAtResponseDto.java delete mode 100644 src/main/java/roomescape/domain/reservation/vo/ReserverName.java rename src/test/java/roomescape/domain/time/repository/{JdbcTimeRepositoryTest.java => TimeRepositoryTest.java} (85%) diff --git a/build.gradle b/build.gradle index 8da4d34a3a..0d6166f2af 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-jdbc' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-validation' diff --git a/src/main/java/roomescape/domain/reservation/controller/AdminReservationController.java b/src/main/java/roomescape/domain/reservation/controller/AdminReservationController.java index 271a03a82d..6095baef6d 100644 --- a/src/main/java/roomescape/domain/reservation/controller/AdminReservationController.java +++ b/src/main/java/roomescape/domain/reservation/controller/AdminReservationController.java @@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import roomescape.domain.reservation.dto.response.ReservationResponseDto; +import roomescape.domain.reservation.dto.response.ReservationTimeStartAtResponseDto; import roomescape.domain.reservation.service.ReservationService; @RestController @@ -28,6 +29,13 @@ public ResponseEntity> getReservations() { return ResponseEntity.ok(reservationService.getReservations()); } + @GetMapping("/{id}/time-start-at") + public ResponseEntity getReservationTimeStartAt( + @PathVariable @Positive(message = "id의 값은 양수여야 합니다.") Long id + ) { + return ResponseEntity.ok(reservationService.getReservationTimeStartAtForSqlObservation(id)); + } + @DeleteMapping("/{id}") public ResponseEntity deleteReservation(@PathVariable @Positive(message = "id의 값은 양수여야 합니다.") Long id) { reservationService.deleteReservationById(id); diff --git a/src/main/java/roomescape/domain/reservation/controller/ReservationController.java b/src/main/java/roomescape/domain/reservation/controller/ReservationController.java index a83d09b333..bf458368d5 100644 --- a/src/main/java/roomescape/domain/reservation/controller/ReservationController.java +++ b/src/main/java/roomescape/domain/reservation/controller/ReservationController.java @@ -23,7 +23,6 @@ import roomescape.domain.reservation.dto.response.ReservationResponseDto; import roomescape.domain.reservation.mapper.ReservationMapper; import roomescape.domain.reservation.service.ReservationService; -import roomescape.domain.reservation.vo.ReserverName; @RestController @RequestMapping("/api/reservations") @@ -64,7 +63,7 @@ public ResponseEntity updateReservation( String name, @Valid @RequestBody ReservationUpdateRequestDto requestDto) { return ResponseEntity.ok( - reservationService.updateReservation(id, new ReserverName(name), + reservationService.updateReservation(id, name, reservationMapper.toUpdateCommand(requestDto))); } @@ -76,7 +75,7 @@ public ResponseEntity cancelReservation( @Size(max = 20, message = "예약자명의 길이는 1이상 20이하 입니다.") String name ) { - return ResponseEntity.ok(reservationService.cancelReservation(id, new ReserverName(name))); + return ResponseEntity.ok(reservationService.cancelReservation(id, name)); } @PostMapping("/waitings") @@ -94,6 +93,6 @@ public ResponseEntity cancelWaitingReservation( @Size(max = 20, message = "예약자명의 길이는 1이상 20이하 입니다.") String name ) { - return ResponseEntity.ok(reservationService.cancelWaitingReservation(id, new ReserverName(name))); + return ResponseEntity.ok(reservationService.cancelWaitingReservation(id, name)); } } diff --git a/src/main/java/roomescape/domain/reservation/dto/command/ReservationCreateCommand.java b/src/main/java/roomescape/domain/reservation/dto/command/ReservationCreateCommand.java index b1f3c1fab3..092ec99083 100644 --- a/src/main/java/roomescape/domain/reservation/dto/command/ReservationCreateCommand.java +++ b/src/main/java/roomescape/domain/reservation/dto/command/ReservationCreateCommand.java @@ -1,12 +1,12 @@ package roomescape.domain.reservation.dto.command; import java.time.LocalDate; -import roomescape.domain.reservation.vo.ReserverName; public record ReservationCreateCommand( - ReserverName name, + String name, LocalDate date, Long timeId, Long themeId ) { + } diff --git a/src/main/java/roomescape/domain/reservation/dto/response/ReservationTimeStartAtResponseDto.java b/src/main/java/roomescape/domain/reservation/dto/response/ReservationTimeStartAtResponseDto.java new file mode 100644 index 0000000000..8d1de8c594 --- /dev/null +++ b/src/main/java/roomescape/domain/reservation/dto/response/ReservationTimeStartAtResponseDto.java @@ -0,0 +1,7 @@ +package roomescape.domain.reservation.dto.response; + +import java.time.LocalTime; + +public record ReservationTimeStartAtResponseDto(Long reservationId, LocalTime startAt) { + +} diff --git a/src/main/java/roomescape/domain/reservation/entity/Reservation.java b/src/main/java/roomescape/domain/reservation/entity/Reservation.java index 77096df0da..2c292bf512 100644 --- a/src/main/java/roomescape/domain/reservation/entity/Reservation.java +++ b/src/main/java/roomescape/domain/reservation/entity/Reservation.java @@ -1,23 +1,113 @@ package roomescape.domain.reservation.entity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import java.time.LocalDate; import java.time.LocalDateTime; import roomescape.domain.reservation.vo.ReservationSchedule; -import roomescape.domain.reservation.vo.ReserverName; import roomescape.domain.theme.entity.Theme; import roomescape.domain.time.entity.Time; +@Entity +@Table( + name = "reservation", + indexes = { + @Index( + name = "uq_waiting_reservation", + columnList = "active_waiting, name, date, time_id, theme_id", + unique = true + ), + @Index( + name = "uq_active_reservation", + columnList = "active_date, active_time_id, active_theme_id", + unique = true + ) + } +) public class Reservation { - private final Long id; - private final ReserverName name; - private final LocalDate date; - private final Time time; - private final Theme theme; - private final ReservationStatus status; - private final Long version; - - private Reservation(Long id, ReserverName name, LocalDate date, Time time, Theme theme, ReservationStatus status, + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotBlank + @Column(name = "name", nullable = false) + private String name; + + @NotNull + @Column(name = "date", nullable = false, columnDefinition = "DATE") + private LocalDate date; + + @Enumerated(EnumType.STRING) + @Column( + name = "status", + columnDefinition = "ENUM('ACTIVE', 'CANCELED', 'WAITING') DEFAULT 'ACTIVE'" + ) + private ReservationStatus status; + + @NotNull + @Column(name = "version", nullable = false, columnDefinition = "BIGINT DEFAULT 0") + private Long version; + + @Column(name = "deleted_at", columnDefinition = "DATETIME DEFAULT NULL") + private LocalDateTime deletedAt; + + @Column( + name = "active_date", + insertable = false, + updatable = false, + columnDefinition = "DATE GENERATED ALWAYS AS " + + "(CASE WHEN status = 'ACTIVE' AND deleted_at IS NULL THEN date ELSE NULL END)" + ) + private LocalDate activeDate; + + @Column( + name = "active_time_id", + insertable = false, + updatable = false, + columnDefinition = "BIGINT GENERATED ALWAYS AS " + + "(CASE WHEN status = 'ACTIVE' AND deleted_at IS NULL THEN time_id ELSE NULL END)" + ) + private Long activeTimeId; + + @Column( + name = "active_theme_id", + insertable = false, + updatable = false, + columnDefinition = "BIGINT GENERATED ALWAYS AS " + + "(CASE WHEN status = 'ACTIVE' AND deleted_at IS NULL THEN theme_id ELSE NULL END)" + ) + private Long activeThemeId; + + @Column( + name = "active_waiting", + insertable = false, + updatable = false, + columnDefinition = "BOOLEAN GENERATED ALWAYS AS " + + "(CASE WHEN status = 'WAITING' AND deleted_at IS NULL THEN true ELSE NULL END)" + ) + private Boolean activeWaiting; + + @ManyToOne + @JoinColumn(name = "time_id", nullable = false) + private Time time; + + @ManyToOne + @JoinColumn(name = "theme_id", nullable = false) + private Theme theme; + + private Reservation(Long id, String name, LocalDate date, Time time, Theme theme, ReservationStatus status, Long version) { this.id = id; this.name = name; @@ -28,17 +118,21 @@ private Reservation(Long id, ReserverName name, LocalDate date, Time time, Theme this.version = version; } - public static Reservation create(ReserverName name, LocalDate date, Time time, Theme theme) { + public Reservation() { + + } + + public static Reservation create(String name, LocalDate date, Time time, Theme theme) { return new Reservation(null, name, date, time, theme, ReservationStatus.ACTIVE, 0L); } public static Reservation reconstruct( - Long id, ReserverName name, LocalDate date, Time time, Theme theme, ReservationStatus status, Long version) { + Long id, String name, LocalDate date, Time time, Theme theme, ReservationStatus status, Long version) { return new Reservation(id, name, date, time, theme, status, version); } public static Reservation reconstruct( - Long id, ReserverName name, LocalDate date, Time time, Theme theme, ReservationStatus status) { + Long id, String name, LocalDate date, Time time, Theme theme, ReservationStatus status) { return reconstruct(id, name, date, time, theme, status, 0L); } @@ -84,7 +178,7 @@ public ReservationSchedule getSchedule() { return new ReservationSchedule(date, theme.getId(), time.getId()); } - public boolean isReservedBy(ReserverName name) { + public boolean isReservedBy(String name) { return this.name.equals(name); } @@ -108,7 +202,7 @@ public Long getId() { return id; } - public ReserverName getName() { + public String getName() { return name; } diff --git a/src/main/java/roomescape/domain/reservation/mapper/ReservationMapper.java b/src/main/java/roomescape/domain/reservation/mapper/ReservationMapper.java index 706936e943..7c6ecc21b3 100644 --- a/src/main/java/roomescape/domain/reservation/mapper/ReservationMapper.java +++ b/src/main/java/roomescape/domain/reservation/mapper/ReservationMapper.java @@ -10,7 +10,6 @@ import roomescape.domain.reservation.dto.response.ReservationResponseDto; import roomescape.domain.reservation.entity.Reservation; import roomescape.domain.reservation.entity.ReservationEditableStatus; -import roomescape.domain.reservation.vo.ReserverName; import roomescape.domain.theme.mapper.ThemeMapper; import roomescape.domain.time.mapper.TimeMapper; @@ -26,7 +25,7 @@ public ReservationMapper(TimeMapper timeMapper, ThemeMapper themeMapper) { } public ReservationCreateCommand toCreateCommand(ReservationCreateRequestDto requestDto) { - return new ReservationCreateCommand(new ReserverName(requestDto.name()), requestDto.date(), requestDto.timeId(), + return new ReservationCreateCommand(requestDto.name(), requestDto.date(), requestDto.timeId(), requestDto.themeId()); } @@ -40,19 +39,19 @@ public ReservationResponseDto toResponseDto( ReservationEditableStatus status, Integer waitingNumber ) { - return new ReservationResponseDto(reservation.getId(), reservation.getName().value(), reservation.getDate(), + return new ReservationResponseDto(reservation.getId(), reservation.getName(), reservation.getDate(), timeMapper.toReservationResponseDto(reservation.getTime()), themeMapper.toReservationResponseDto(reservation.getTheme()), status, status.getMessage(), waitingNumber, reservation.getVersion()); } public ReservationCreateResponseDto toCreateResponseDto(Reservation reservation) { - return new ReservationCreateResponseDto(reservation.getId(), reservation.getName().value(), + return new ReservationCreateResponseDto(reservation.getId(), reservation.getName(), reservation.getDate(), reservation.getTime().getId(), reservation.getTheme().getId()); } public ReservationCancelResponseDto toCancelResponseDto(Reservation reservation) { - return new ReservationCancelResponseDto(reservation.getId(), reservation.getName().value(), + return new ReservationCancelResponseDto(reservation.getId(), reservation.getName(), reservation.getDate(), reservation.getTime().getId(), reservation.getTheme().getId()); } } diff --git a/src/main/java/roomescape/domain/reservation/repository/JdbcReservationRepository.java b/src/main/java/roomescape/domain/reservation/repository/JdbcReservationRepository.java index 181148c4d8..5cbbfee0d3 100644 --- a/src/main/java/roomescape/domain/reservation/repository/JdbcReservationRepository.java +++ b/src/main/java/roomescape/domain/reservation/repository/JdbcReservationRepository.java @@ -12,18 +12,15 @@ import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.jdbc.core.namedparam.SqlParameterSource; import org.springframework.jdbc.core.simple.SimpleJdbcInsert; -import org.springframework.stereotype.Repository; import roomescape.domain.reservation.entity.Reservation; import roomescape.domain.reservation.entity.ReservationStatus; import roomescape.domain.reservation.error.type.ReservationErrorType; import roomescape.domain.reservation.vo.ReservationSchedule; -import roomescape.domain.reservation.vo.ReserverName; import roomescape.domain.theme.entity.Theme; import roomescape.domain.time.entity.Time; import roomescape.global.error.exception.GeneralException; -@Repository -public class JdbcReservationRepository implements ReservationRepository { +public class JdbcReservationRepository { private static final String WAITING_NUMBER_EXPRESSION = """ CASE @@ -48,7 +45,7 @@ public JdbcReservationRepository(DataSource dataSource) { .usingGeneratedKeyColumns("id"); } - @Override + public List findReservationsByNotDeletedWithWaitingNumber() { String sql = """ SELECT r.id, r.name, r.date, r.status, r.version, @@ -73,7 +70,7 @@ private ReservationWithWaitingNumber mapReservationWithWaitingNumber(ResultSet r private Reservation mapReservation(ResultSet rs) throws SQLException { return Reservation.reconstruct( rs.getLong("id"), - new ReserverName(rs.getString("name")), + rs.getString("name"), rs.getDate("date").toLocalDate(), Time.reconstruct( rs.getLong("time_id"), @@ -109,7 +106,7 @@ private LocalDateTime getNullableLocalDateTime(ResultSet rs, String columnLabel) return timestamp.toLocalDateTime(); } - @Override + public List findReservationsByNameAndNotDeletedWithWaitingNumber(String name) { String sql = """ SELECT ranked.id, ranked.name, ranked.date, ranked.status, ranked.version, @@ -135,7 +132,7 @@ public List findReservationsByNameAndNotDeletedWit return jdbcTemplate.query(sql, parameters, (rs, rowNum) -> mapReservationWithWaitingNumber(rs)); } - @Override + public Optional findReservationByIdAndNotDeleted(Long id) { String sql = """ SELECT r.id, r.name, r.date, r.status, r.version, @@ -158,7 +155,7 @@ public Optional findReservationByIdAndNotDeleted(Long id) { return reservations.stream().findFirst(); } - @Override + public Optional lockReservationByIdAndNotDeleted(Long id) { String sql = """ SELECT r.id, r.name, r.date, r.status, r.version, @@ -182,7 +179,7 @@ public Optional lockReservationByIdAndNotDeleted(Long id) { return reservations.stream().findFirst(); } - @Override + public List findTimeIdsByDateAndThemeIdAndNotDeleted(LocalDate date, Long themeId) { String sql = """ SELECT r.time_id @@ -207,7 +204,7 @@ public List findTimeIdsByDateAndThemeIdAndNotDeleted(LocalDate date, Long (resultSet, rowNum) -> resultSet.getLong("time_id")); } - @Override + public boolean existsActiveReservationBySchedule(ReservationSchedule schedule) { String sql = """ SELECT EXISTS ( @@ -230,7 +227,7 @@ SELECT EXISTS ( return Boolean.TRUE.equals(exists); } - @Override + public Optional lockActiveReservationBySchedule(ReservationSchedule schedule) { String sql = """ SELECT r.id @@ -265,7 +262,7 @@ AND EXISTS ( return reservationIds.stream().findFirst(); } - @Override + public Optional lockFirstWaitingReservationBySchedule(ReservationSchedule schedule) { String sql = """ SELECT r.id, r.name, r.date, r.status, r.version, @@ -305,10 +302,10 @@ private SqlParameterSource toScheduleParameterSource(ReservationSchedule schedul )); } - @Override + public Reservation save(Reservation reservation) { Map args = Map.of( - "name", reservation.getName().value(), + "name", reservation.getName(), "date", reservation.getDate(), "time_id", reservation.getTime().getId(), "theme_id", reservation.getTheme().getId(), @@ -320,7 +317,7 @@ public Reservation save(Reservation reservation) { reservation.getTime(), reservation.getTheme(), reservation.getStatus()); } - @Override + public Reservation update(Reservation reservation) { String sql = """ UPDATE reservation @@ -337,7 +334,7 @@ public Reservation update(Reservation reservation) { """; SqlParameterSource parameters = new MapSqlParameterSource() .addValue("id", reservation.getId()) - .addValue("name", reservation.getName().value()) + .addValue("name", reservation.getName()) .addValue("date", reservation.getDate()) .addValue("timeId", reservation.getTime().getId()) .addValue("themeId", reservation.getTheme().getId()) @@ -375,7 +372,7 @@ SELECT EXISTS ( return Boolean.TRUE.equals(exists); } - @Override + public void deleteReservationById(Long id) { String sql = "UPDATE reservation SET deleted_at = CURRENT_TIMESTAMP WHERE id = :id AND deleted_at IS NULL"; SqlParameterSource parameters = new MapSqlParameterSource("id", id); @@ -385,7 +382,7 @@ public void deleteReservationById(Long id) { } } - @Override + public boolean existsReservationByIdAndNotDeleted(Long id) { String sql = """ SELECT EXISTS ( @@ -401,7 +398,7 @@ SELECT EXISTS ( return Boolean.TRUE.equals(exists); } - @Override + public boolean existsReservationAndStatus(Reservation reservation, ReservationStatus status) { String sql = """ SELECT EXISTS ( @@ -418,7 +415,7 @@ SELECT EXISTS ( SqlParameterSource parameters = new MapSqlParameterSource(Map.of( "date", reservation.getDate(), - "name", reservation.getName().value(), + "name", reservation.getName(), "timeId", reservation.getTime().getId(), "themeId", reservation.getTheme().getId(), "status", status.name() diff --git a/src/main/java/roomescape/domain/reservation/repository/ReservationRepository.java b/src/main/java/roomescape/domain/reservation/repository/ReservationRepository.java index f8e5a5290d..1c9b6c86c5 100644 --- a/src/main/java/roomescape/domain/reservation/repository/ReservationRepository.java +++ b/src/main/java/roomescape/domain/reservation/repository/ReservationRepository.java @@ -1,37 +1,274 @@ package roomescape.domain.reservation.repository; +import jakarta.persistence.LockModeType; import java.time.LocalDate; +import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; import roomescape.domain.reservation.entity.Reservation; import roomescape.domain.reservation.entity.ReservationStatus; +import roomescape.domain.reservation.error.type.ReservationErrorType; import roomescape.domain.reservation.vo.ReservationSchedule; +import roomescape.domain.theme.entity.Theme; +import roomescape.domain.time.entity.Time; +import roomescape.global.error.exception.GeneralException; -public interface ReservationRepository { +@Repository +public interface ReservationRepository extends JpaRepository { - List findReservationsByNotDeletedWithWaitingNumber(); + @EntityGraph(attributePaths = {"time", "theme"}) + List findAllByDeletedAtIsNullOrderByIdAsc(); - List findReservationsByNameAndNotDeletedWithWaitingNumber(String name); + default List findReservationsByNotDeletedWithWaitingNumber() { + return toReservationsWithWaitingNumber(findAllByDeletedAtIsNullOrderByIdAsc()); + } - Optional findReservationByIdAndNotDeleted(Long id); + default List findReservationsByNameAndNotDeletedWithWaitingNumber(String name) { + return toReservationsWithWaitingNumber(findAllByDeletedAtIsNullOrderByIdAsc()) + .stream() + .filter(reservation -> reservation.reservation().getName().equals(name)) + .sorted(Comparator + .comparing((ReservationWithWaitingNumber reservation) -> reservation.reservation().getDate()) + .thenComparing(reservation -> reservation.reservation().getTime().getStartAt()) + .thenComparing(reservation -> reservation.reservation().getId())) + .toList(); + } - Optional lockReservationByIdAndNotDeleted(Long id); + private static List toReservationsWithWaitingNumber( + List reservations) { + Map waitingCounts = new HashMap<>(); - List findTimeIdsByDateAndThemeIdAndNotDeleted(LocalDate localDate, Long themeId); + return reservations.stream() + .map(reservation -> { + Integer waitingNumber = null; + if (reservation.getStatus() == ReservationStatus.WAITING) { + ReservationSchedule schedule = reservation.getSchedule(); + waitingNumber = waitingCounts.getOrDefault(schedule, 0) + 1; + waitingCounts.put(schedule, waitingNumber); + } + return new ReservationWithWaitingNumber(reservation, waitingNumber); + }) + .toList(); + } - boolean existsActiveReservationBySchedule(ReservationSchedule schedule); + @EntityGraph(attributePaths = {"time", "theme"}) + Optional findByIdAndDeletedAtIsNull(Long id); - Optional lockActiveReservationBySchedule(ReservationSchedule schedule); + default Optional findReservationByIdAndNotDeleted(Long id) { + return findByIdAndDeletedAtIsNull(id); + } - Optional lockFirstWaitingReservationBySchedule(ReservationSchedule schedule); + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query(""" + SELECT r + FROM Reservation r + JOIN FETCH r.time + JOIN FETCH r.theme + WHERE r.id = :id + AND r.deletedAt IS NULL + """) + Optional lockReservationByIdAndNotDeleted(@Param("id") Long id); - Reservation save(Reservation reservation); + @Query(""" + SELECT r.time.id + FROM Reservation r + JOIN r.time rt + JOIN r.theme t + WHERE r.date = :date + AND t.id = :themeId + AND r.status = roomescape.domain.reservation.entity.ReservationStatus.ACTIVE + AND r.deletedAt IS NULL + AND rt.deletedAt IS NULL + AND t.deletedAt IS NULL + """) + List findTimeIdsByDateAndThemeIdAndNotDeleted( + @Param("date") LocalDate localDate, + @Param("themeId") Long themeId + ); - Reservation update(Reservation reservation); + default boolean existsActiveReservationBySchedule(ReservationSchedule schedule) { + return existsActiveReservationBySchedule(schedule.date(), schedule.themeId(), schedule.timeId()); + } - void deleteReservationById(Long id); + @Query(""" + SELECT COUNT(r) > 0 + FROM Reservation r + JOIN r.time rt + JOIN r.theme t + WHERE r.date = :date + AND t.id = :themeId + AND rt.id = :timeId + AND r.status = roomescape.domain.reservation.entity.ReservationStatus.ACTIVE + AND r.deletedAt IS NULL + AND rt.deletedAt IS NULL + AND t.deletedAt IS NULL + """) + boolean existsActiveReservationBySchedule( + @Param("date") LocalDate date, + @Param("themeId") Long themeId, + @Param("timeId") Long timeId + ); - boolean existsReservationByIdAndNotDeleted(Long id); + default Optional lockActiveReservationBySchedule(ReservationSchedule schedule) { + return lockActiveReservationBySchedule(schedule.date(), schedule.themeId(), schedule.timeId()) + .map(Reservation::getId); + } - boolean existsReservationAndStatus(Reservation reservation, ReservationStatus status); + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query(""" + SELECT r + FROM Reservation r + JOIN r.time rt + JOIN r.theme t + WHERE r.date = :date + AND t.id = :themeId + AND rt.id = :timeId + AND r.status = roomescape.domain.reservation.entity.ReservationStatus.ACTIVE + AND r.deletedAt IS NULL + AND rt.deletedAt IS NULL + AND t.deletedAt IS NULL + """) + Optional lockActiveReservationBySchedule( + @Param("date") LocalDate date, + @Param("themeId") Long themeId, + @Param("timeId") Long timeId + ); + + default Optional lockFirstWaitingReservationBySchedule(ReservationSchedule schedule) { + return lockWaitingReservationsBySchedule( + schedule.date(), schedule.themeId(), schedule.timeId(), PageRequest.of(0, 1)) + .stream() + .findFirst(); + } + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query(""" + SELECT r + FROM Reservation r + JOIN FETCH r.time rt + JOIN FETCH r.theme t + WHERE r.date = :date + AND t.id = :themeId + AND rt.id = :timeId + AND r.status = roomescape.domain.reservation.entity.ReservationStatus.WAITING + AND r.deletedAt IS NULL + AND rt.deletedAt IS NULL + AND t.deletedAt IS NULL + ORDER BY r.id ASC + """) + List lockWaitingReservationsBySchedule( + @Param("date") LocalDate date, + @Param("themeId") Long themeId, + @Param("timeId") Long timeId, + Pageable pageable + ); + + default Reservation update(Reservation reservation) { + int updatedRowCount = updateByIdAndVersion( + reservation.getId(), + reservation.getName(), + reservation.getDate(), + reservation.getTime(), + reservation.getTheme(), + reservation.getStatus(), + reservation.getVersion() + ); + + if (updatedRowCount == 0) { + if (!existsReservationByIdAndNotDeleted(reservation.getId())) { + throw new GeneralException(ReservationErrorType.RESERVATION_NOT_FOUND); + } + if (existsByIdAndStatusAndDeletedAtIsNull(reservation.getId(), ReservationStatus.CANCELED)) { + throw new GeneralException(ReservationErrorType.ALREADY_CANCELED); + } + throw new GeneralException(ReservationErrorType.RESERVATION_ALREADY_UPDATED); + } + + return Reservation.reconstruct( + reservation.getId(), + reservation.getName(), + reservation.getDate(), + reservation.getTime(), + reservation.getTheme(), + reservation.getStatus(), + reservation.getVersion() + 1 + ); + } + + @Modifying(flushAutomatically = true, clearAutomatically = true) + @Query(""" + UPDATE Reservation r + SET r.name = :name, + r.date = :date, + r.time = :time, + r.theme = :theme, + r.status = :status, + r.version = r.version + 1 + WHERE r.id = :id + AND r.deletedAt IS NULL + AND r.status <> roomescape.domain.reservation.entity.ReservationStatus.CANCELED + AND r.version = :version + """) + int updateByIdAndVersion( + @Param("id") Long id, + @Param("name") String name, + @Param("date") LocalDate date, + @Param("time") Time time, + @Param("theme") Theme theme, + @Param("status") ReservationStatus status, + @Param("version") Long version + ); + + default void deleteReservationById(Long id) { + int updatedRowCount = softDeleteById(id); + if (updatedRowCount == 0) { + throw new GeneralException(ReservationErrorType.RESERVATION_NOT_FOUND); + } + } + + @Modifying(flushAutomatically = true, clearAutomatically = true) + @Query(""" + UPDATE Reservation r + SET r.deletedAt = CURRENT_TIMESTAMP + WHERE r.id = :id + AND r.deletedAt IS NULL + """) + int softDeleteById(@Param("id") Long id); + + boolean existsByIdAndDeletedAtIsNull(Long id); + + default boolean existsReservationByIdAndNotDeleted(Long id) { + return existsByIdAndDeletedAtIsNull(id); + } + + boolean existsByIdAndStatusAndDeletedAtIsNull(Long id, ReservationStatus status); + + default boolean existsReservationAndStatus(Reservation reservation, ReservationStatus status) { + return existsByDateAndNameAndTimeIdAndThemeIdAndStatusAndDeletedAtIsNull( + reservation.getDate(), + reservation.getName(), + reservation.getTime().getId(), + reservation.getTheme().getId(), + status + ); + } + + boolean existsByDateAndNameAndTimeIdAndThemeIdAndStatusAndDeletedAtIsNull( + LocalDate date, + String name, + Long timeId, + Long themeId, + ReservationStatus status + ); } diff --git a/src/main/java/roomescape/domain/reservation/service/ReservationService.java b/src/main/java/roomescape/domain/reservation/service/ReservationService.java index dedd52842c..b5f0dfc60e 100644 --- a/src/main/java/roomescape/domain/reservation/service/ReservationService.java +++ b/src/main/java/roomescape/domain/reservation/service/ReservationService.java @@ -3,10 +3,13 @@ import java.time.Clock; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; import java.util.ArrayList; import java.util.List; import java.util.Optional; -import org.springframework.dao.DuplicateKeyException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import roomescape.domain.reservation.dto.command.ReservationCreateCommand; @@ -14,13 +17,13 @@ import roomescape.domain.reservation.dto.response.ReservationCancelResponseDto; import roomescape.domain.reservation.dto.response.ReservationCreateResponseDto; import roomescape.domain.reservation.dto.response.ReservationResponseDto; +import roomescape.domain.reservation.dto.response.ReservationTimeStartAtResponseDto; import roomescape.domain.reservation.entity.Reservation; import roomescape.domain.reservation.entity.ReservationStatus; import roomescape.domain.reservation.error.type.ReservationErrorType; import roomescape.domain.reservation.mapper.ReservationMapper; import roomescape.domain.reservation.repository.ReservationRepository; import roomescape.domain.reservation.repository.ReservationWithWaitingNumber; -import roomescape.domain.reservation.vo.ReserverName; import roomescape.domain.theme.entity.Theme; import roomescape.domain.theme.repository.ThemeRepository; import roomescape.domain.time.entity.Time; @@ -33,6 +36,8 @@ @Transactional(readOnly = true) public class ReservationService { + private static final Logger log = LoggerFactory.getLogger(ReservationService.class); + private final ReservationRepository reservationRepository; private final TimeRepository timeRepository; private final ThemeRepository themeRepository; @@ -60,6 +65,18 @@ public List getReservationsByName(String name) { return convertReservationsToDto(reservationsWithWaitingNumber); } + public ReservationTimeStartAtResponseDto getReservationTimeStartAtForSqlObservation(Long reservationId) { + log.info("=== SQL observation: findById({}) ===", reservationId); + Reservation reservation = reservationRepository.findById(reservationId) + .orElseThrow(() -> new GeneralException(ReservationErrorType.RESERVATION_NOT_FOUND)); + + log.info("=== SQL observation: getTime().getStartAt() ==="); + LocalTime startAt = reservation.getTime().getStartAt(); + log.info("=== SQL observation: startAt={} ===", startAt); + + return new ReservationTimeStartAtResponseDto(reservationId, startAt); + } + private List convertReservationsToDto(List reservations) { return reservations.stream() .map(reservationWithWaitingNumber -> { @@ -78,8 +95,10 @@ public ReservationCreateResponseDto saveReservation(ReservationCreateCommand com Reservation reservation = createReservation(command); try { - return reservationMapper.toCreateResponseDto(reservationRepository.save(reservation)); - } catch (DuplicateKeyException e) { + Reservation savedReservation = reservationRepository.save(reservation); + reservationRepository.flush(); + return reservationMapper.toCreateResponseDto(savedReservation); + } catch (DataIntegrityViolationException e) { throw new GeneralException(ReservationErrorType.ALREADY_RESERVED); } } @@ -111,7 +130,7 @@ private Reservation createReservation(ReservationCreateCommand command) { } @Transactional - public ReservationCreateResponseDto updateReservation(Long id, ReserverName name, + public ReservationCreateResponseDto updateReservation(Long id, String name, ReservationUpdateCommand command) { Reservation existingReservation = reservationRepository.findReservationByIdAndNotDeleted(id) .orElseThrow(() -> new GeneralException(ReservationErrorType.RESERVATION_NOT_FOUND)); @@ -128,12 +147,12 @@ public ReservationCreateResponseDto updateReservation(Long id, ReserverName name } return responseDto; - } catch (DuplicateKeyException e) { + } catch (DataIntegrityViolationException e) { throw new GeneralException(ReservationErrorType.ALREADY_RESERVED); } } - private void validateReservationCanBeUpdated(Reservation existingReservation, ReserverName name) { + private void validateReservationCanBeUpdated(Reservation existingReservation, String name) { if (!existingReservation.isReservedBy(name)) { throw new GeneralException(ReservationErrorType.RESERVATION_UPDATE_FORBIDDEN); } @@ -203,7 +222,7 @@ private void validateReservationUpdateFieldsExist(Time time, Theme theme) { } @Transactional - public ReservationCancelResponseDto cancelReservation(Long id, ReserverName name) { + public ReservationCancelResponseDto cancelReservation(Long id, String name) { Reservation reservation = reservationRepository.lockReservationByIdAndNotDeleted(id) .orElseThrow(() -> new GeneralException(ReservationErrorType.RESERVATION_NOT_FOUND)); @@ -217,7 +236,7 @@ public ReservationCancelResponseDto cancelReservation(Long id, ReserverName name return cancelResponseDto; } - private void validateReservationCanBeCanceled(Reservation reservation, ReserverName name) { + private void validateReservationCanBeCanceled(Reservation reservation, String name) { if (!reservation.isReservedBy(name)) { throw new GeneralException(ReservationErrorType.RESERVATION_CANCEL_FORBIDDEN); } @@ -248,8 +267,10 @@ public ReservationCreateResponseDto saveWaitingReservation(ReservationCreateComm validateWaitingReservationCreationAllowed(waitingReservation); try { - return reservationMapper.toCreateResponseDto(reservationRepository.save(waitingReservation)); - } catch (DuplicateKeyException e) { + Reservation savedReservation = reservationRepository.save(waitingReservation); + reservationRepository.flush(); + return reservationMapper.toCreateResponseDto(savedReservation); + } catch (DataIntegrityViolationException e) { throw new GeneralException(ReservationErrorType.ALREADY_WAITING); } } @@ -267,7 +288,7 @@ private void validateWaitingReservationCreationAllowed(Reservation reservation) } @Transactional - public ReservationCancelResponseDto cancelWaitingReservation(Long id, ReserverName name) { + public ReservationCancelResponseDto cancelWaitingReservation(Long id, String name) { Reservation reservation = reservationRepository.lockReservationByIdAndNotDeleted(id) .orElseThrow(() -> new GeneralException(ReservationErrorType.RESERVATION_NOT_FOUND)); @@ -276,7 +297,7 @@ public ReservationCancelResponseDto cancelWaitingReservation(Long id, ReserverNa return reservationMapper.toCancelResponseDto(reservationRepository.update(reservation.cancel())); } - private void validateWaitingReservationCanBeCanceled(Reservation reservation, ReserverName name) { + private void validateWaitingReservationCanBeCanceled(Reservation reservation, String name) { if (!reservation.isReservedBy(name)) { throw new GeneralException(ReservationErrorType.RESERVATION_CANCEL_FORBIDDEN); } diff --git a/src/main/java/roomescape/domain/reservation/vo/ReserverName.java b/src/main/java/roomescape/domain/reservation/vo/ReserverName.java deleted file mode 100644 index b7b21f956e..0000000000 --- a/src/main/java/roomescape/domain/reservation/vo/ReserverName.java +++ /dev/null @@ -1,21 +0,0 @@ -package roomescape.domain.reservation.vo; - -import org.springframework.util.StringUtils; -import roomescape.global.error.exception.GeneralException; -import roomescape.global.error.type.GeneralErrorType; - -public record ReserverName(String value) { - - private static final int MINIMUM_LENGTH = 1; - private static final int MAXIMUM_LENGTH = 20; - - public ReserverName { - if (!StringUtils.hasText(value) || isLengthOutOfRange(value)) { - throw new GeneralException(GeneralErrorType.ILLEGAL_STATE); - } - } - - private boolean isLengthOutOfRange(String value) { - return value.length() < MINIMUM_LENGTH || value.length() > MAXIMUM_LENGTH; - } -} diff --git a/src/main/java/roomescape/domain/theme/entity/Theme.java b/src/main/java/roomescape/domain/theme/entity/Theme.java index 9fb196e8ab..8ab13d62cd 100644 --- a/src/main/java/roomescape/domain/theme/entity/Theme.java +++ b/src/main/java/roomescape/domain/theme/entity/Theme.java @@ -1,14 +1,52 @@ package roomescape.domain.theme.entity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; import java.time.LocalDateTime; +@Entity +@Table( + name = "theme", + indexes = @Index( + name = "uq_active_theme", + columnList = "active_name", + unique = true + ) +) public class Theme { - private final Long id; - private final String name; - private final String description; - private final String imageUrl; - private final LocalDateTime deletedAt; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotBlank + @Column(name = "name", nullable = false) + private String name; + + @NotBlank + @Column(name = "description", nullable = false) + private String description; + + @NotBlank + @Column(name = "image_url", nullable = false, length = 2000) + private String imageUrl; + + @Column(name = "deleted_at", columnDefinition = "DATETIME DEFAULT NULL") + private LocalDateTime deletedAt; + + @Column( + name = "active_name", + insertable = false, + updatable = false, + columnDefinition = "VARCHAR(255) GENERATED ALWAYS AS (CASE WHEN deleted_at IS NULL THEN name ELSE NULL END)" + ) + private String activeName; private Theme(Long id, String name, String description, String imageUrl, LocalDateTime deletedAt) { this.id = id; @@ -18,6 +56,10 @@ private Theme(Long id, String name, String description, String imageUrl, LocalDa this.deletedAt = deletedAt; } + public Theme() { + + } + public static Theme create(String name, String description, String imageUrl) { return new Theme(null, name, description, imageUrl, null); } @@ -47,6 +89,10 @@ public String getImageUrl() { return imageUrl; } + public LocalDateTime getDeletedAt() { + return deletedAt; + } + public boolean isDeleted() { return deletedAt != null; } diff --git a/src/main/java/roomescape/domain/theme/repository/JdbcThemeRepository.java b/src/main/java/roomescape/domain/theme/repository/JdbcThemeRepository.java index 84e2d6b101..271b85a1d7 100644 --- a/src/main/java/roomescape/domain/theme/repository/JdbcThemeRepository.java +++ b/src/main/java/roomescape/domain/theme/repository/JdbcThemeRepository.java @@ -10,13 +10,11 @@ import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.jdbc.core.namedparam.SqlParameterSource; import org.springframework.jdbc.core.simple.SimpleJdbcInsert; -import org.springframework.stereotype.Repository; import roomescape.domain.theme.entity.Theme; import roomescape.domain.theme.error.type.ThemeErrorType; import roomescape.global.error.exception.GeneralException; -@Repository -public class JdbcThemeRepository implements ThemeRepository { +public class JdbcThemeRepository { private final NamedParameterJdbcTemplate jdbcTemplate; private final SimpleJdbcInsert simpleJdbcInsert; @@ -29,7 +27,6 @@ public JdbcThemeRepository(DataSource dataSource) { .usingGeneratedKeyColumns("id"); } - @Override public List findAllByDeletedAtIsNull() { String sql = "SELECT id, name, description, image_url FROM theme WHERE deleted_at IS NULL"; return jdbcTemplate.query( @@ -43,7 +40,6 @@ public List findAllByDeletedAtIsNull() { )); } - @Override public Theme save(Theme theme) { Map args = Map.of( "name", theme.getName(), @@ -54,7 +50,6 @@ public Theme save(Theme theme) { return Theme.reconstruct(generatedKey, theme.getName(), theme.getDescription(), theme.getImageUrl(), null); } - @Override public void deleteThemeById(Long id) { final String sql = "UPDATE theme SET deleted_at = CURRENT_TIMESTAMP WHERE id = :id AND deleted_at IS NULL"; final SqlParameterSource parameters = new MapSqlParameterSource("id", id); @@ -65,7 +60,7 @@ public void deleteThemeById(Long id) { } } - @Override + public Optional findThemeByIdAndDeletedAtIsNull(Long id) { final String sql = "SELECT id, name, description, image_url FROM theme WHERE id = :id AND deleted_at IS NULL"; final SqlParameterSource parameters = new MapSqlParameterSource("id", id); @@ -87,7 +82,7 @@ public Optional findThemeByIdAndDeletedAtIsNull(Long id) { } } - @Override + public boolean existsThemeByIdAndDeletedAtIsNull(Long id) { String sql = """ SELECT EXISTS ( @@ -103,7 +98,7 @@ SELECT EXISTS ( return Boolean.TRUE.equals(exists); } - @Override + public boolean existsThemeByNameAndDeletedAtIsNull(String name) { String sql = """ SELECT EXISTS ( @@ -119,7 +114,7 @@ SELECT EXISTS ( return Boolean.TRUE.equals(exists); } - @Override + public List findPopularThemesDateBetween(LocalDate startDate, LocalDate endDate, Integer limit) { String sql = """ SELECT t.id, t.name, t.description, t.image_url diff --git a/src/main/java/roomescape/domain/theme/repository/ThemeRepository.java b/src/main/java/roomescape/domain/theme/repository/ThemeRepository.java index a496c5d136..c529b7d559 100644 --- a/src/main/java/roomescape/domain/theme/repository/ThemeRepository.java +++ b/src/main/java/roomescape/domain/theme/repository/ThemeRepository.java @@ -3,15 +3,38 @@ import java.time.LocalDate; import java.util.List; import java.util.Optional; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import roomescape.domain.theme.error.type.ThemeErrorType; import roomescape.domain.theme.entity.Theme; +import roomescape.global.error.exception.GeneralException; -public interface ThemeRepository { +@Repository +public interface ThemeRepository extends JpaRepository { List findAllByDeletedAtIsNull(); Theme save(Theme theme); - void deleteThemeById(Long id); + default void deleteThemeById(Long id) { + int updatedRowCount = softDeleteById(id); + if (updatedRowCount == 0) { + throw new GeneralException(ThemeErrorType.THEME_NOT_FOUND); + } + } + + @Modifying + @Query(""" + UPDATE Theme t + SET t.deletedAt = CURRENT_TIMESTAMP + WHERE t.id = :id + AND t.deletedAt IS NULL + """) + int softDeleteById(@Param("id") Long id); Optional findThemeByIdAndDeletedAtIsNull(Long id); @@ -19,5 +42,22 @@ public interface ThemeRepository { boolean existsThemeByNameAndDeletedAtIsNull(String name); - List findPopularThemesDateBetween(LocalDate startDate, LocalDate endDate, Integer limit); + @Query(""" + SELECT t + FROM Reservation r + JOIN r.theme t + JOIN r.time rt + WHERE r.date BETWEEN :startDate AND :endDate + AND r.status = roomescape.domain.reservation.entity.ReservationStatus.ACTIVE + AND r.deletedAt IS NULL + AND t.deletedAt IS NULL + AND rt.deletedAt IS NULL + GROUP BY t + ORDER BY COUNT(r.id) DESC, t.id ASC + """) + List findPopularThemesDateBetween( + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + Pageable pageable + ); } diff --git a/src/main/java/roomescape/domain/theme/service/ThemeService.java b/src/main/java/roomescape/domain/theme/service/ThemeService.java index 83d4a9efcd..2496527bcc 100644 --- a/src/main/java/roomescape/domain/theme/service/ThemeService.java +++ b/src/main/java/roomescape/domain/theme/service/ThemeService.java @@ -4,6 +4,7 @@ import java.time.LocalDate; import java.util.List; import org.springframework.dao.DuplicateKeyException; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import roomescape.domain.theme.dto.command.ThemeCreateCommand; @@ -44,7 +45,7 @@ public List getPopularThemes() { LocalDate endDate = today.minusDays(1); return convertThemesToDto( - themeRepository.findPopularThemesDateBetween(startDate, endDate, 10)); + themeRepository.findPopularThemesDateBetween(startDate, endDate, PageRequest.of(0, 10))); } @Transactional diff --git a/src/main/java/roomescape/domain/time/entity/Time.java b/src/main/java/roomescape/domain/time/entity/Time.java index 941427b8d2..4ec84a63cc 100644 --- a/src/main/java/roomescape/domain/time/entity/Time.java +++ b/src/main/java/roomescape/domain/time/entity/Time.java @@ -1,20 +1,56 @@ package roomescape.domain.time.entity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; import java.time.LocalDateTime; import java.time.LocalTime; +@Entity +@Table( + name = "reservation_time", + indexes = @Index( + name = "uq_active_reservation_time", + columnList = "active_start_at", + unique = true + ) +) public class Time { - private final Long id; - private final LocalTime startAt; - private final LocalDateTime deletedAt; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; - private Time(Long id, LocalTime startAt, LocalDateTime deletedAt) { + @NotNull + @Column(name = "start_at", nullable = false, columnDefinition = "TIME") + private LocalTime startAt; + + @Column(name = "deleted_at", columnDefinition = "DATETIME DEFAULT NULL") + private LocalDateTime deletedAt; + + @Column( + name = "active_start_at", + insertable = false, + updatable = false, + columnDefinition = "TIME GENERATED ALWAYS AS (CASE WHEN deleted_at IS NULL THEN start_at ELSE NULL END)" + ) + private LocalTime activeStartAt; + + public Time(Long id, LocalTime startAt, LocalDateTime deletedAt) { this.id = id; this.startAt = startAt; this.deletedAt = deletedAt; } + public Time() { + + } + public static Time create(LocalTime startAt) { return new Time(null, startAt, null); } diff --git a/src/main/java/roomescape/domain/time/repository/JdbcTimeRepository.java b/src/main/java/roomescape/domain/time/repository/JdbcTimeRepository.java index 899e062733..a515111340 100644 --- a/src/main/java/roomescape/domain/time/repository/JdbcTimeRepository.java +++ b/src/main/java/roomescape/domain/time/repository/JdbcTimeRepository.java @@ -10,13 +10,11 @@ import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.jdbc.core.namedparam.SqlParameterSource; import org.springframework.jdbc.core.simple.SimpleJdbcInsert; -import org.springframework.stereotype.Repository; import roomescape.domain.time.entity.Time; import roomescape.domain.time.error.type.TimeErrorType; import roomescape.global.error.exception.GeneralException; -@Repository -public class JdbcTimeRepository implements TimeRepository { +public class JdbcTimeRepository { private final NamedParameterJdbcTemplate jdbcTemplate; private final SimpleJdbcInsert simpleJdbcInsert; @@ -29,7 +27,7 @@ public JdbcTimeRepository(DataSource dataSource) { .usingGeneratedKeyColumns("id"); } - @Override + public Time save(Time time) { Map args = Map.of("start_at", time.getStartAt()); @@ -38,7 +36,7 @@ public Time save(Time time) { return Time.reconstruct(generatedKey, time.getStartAt(), null); } - @Override + public List