From dce0690d1a6d7332cb17c679807770b130c67fbb Mon Sep 17 00:00:00 2001 From: ff1451 Date: Fri, 16 Oct 2026 13:56:25 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=98=88=EC=95=BD=20=EC=8B=9C=EA=B0=81?= =?UTF-8?q?=20=ED=8C=90=EB=8B=A8=EC=9D=84=20=EC=84=9C=EB=B2=84=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=EB=8C=80=EA=B0=80=20=EC=95=84=EB=8B=8C=20=ED=95=9C?= =?UTF-8?q?=EA=B5=AD=20=EC=8B=9C=EA=B0=84=20Clock=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 예약 서비스가 LocalDate.now()와 ZoneId.systemDefault()를 써서, UTC인 운영 컨테이너에서는 한국 시각으로 이미 지난 시간을 최대 9시간까지 예약하거나, 시작한 예약을 취소할 수 있었다. "오늘" 기준(예약 가능 기간, 지난/다가올 예약 구분)도 00~09시 사이에 하루 어긋났다. - 장부용이던 Asia/Seoul Clock 설정을 global/config/TimeConfig로 옮기고 예약 서비스에 주입한다. - 테스트용 MutableClock을 공용 테스트 패키지로 옮기고, 예약 테스트의 날짜도 한국 날짜로 계산한다. --- .../global/config/TimeConfig.java | 22 +++++ .../ledger/config/LedgerTimeConfig.java | 21 ----- .../service/ReservationService.java | 17 ++-- .../{ledger => }/MutableClock.java | 8 +- .../ledger/LedgerIntegrationTestSupport.java | 1 + .../MyReservationIntegrationTest.java | 9 +- .../ReservationCancelIntegrationTest.java | 17 ++-- .../ReservationConcurrencyTest.java | 3 +- .../ReservationCreateIntegrationTest.java | 19 ++-- .../ReservationDetailIntegrationTest.java | 7 +- ...eservationRepeatCreateIntegrationTest.java | 3 +- .../ReservationStatusIntegrationTest.java | 7 +- .../ReservationTimeZoneIntegrationTest.java | 89 +++++++++++++++++++ 13 files changed, 161 insertions(+), 62 deletions(-) create mode 100644 src/main/java/com/bcsdlab/bcsdinternalapiv2/global/config/TimeConfig.java delete mode 100644 src/main/java/com/bcsdlab/bcsdinternalapiv2/ledger/config/LedgerTimeConfig.java rename src/test/java/com/bcsdlab/bcsdinternalapiv2/{ledger => }/MutableClock.java (73%) create mode 100644 src/test/java/com/bcsdlab/bcsdinternalapiv2/reservation/ReservationTimeZoneIntegrationTest.java diff --git a/src/main/java/com/bcsdlab/bcsdinternalapiv2/global/config/TimeConfig.java b/src/main/java/com/bcsdlab/bcsdinternalapiv2/global/config/TimeConfig.java new file mode 100644 index 0000000..96e6952 --- /dev/null +++ b/src/main/java/com/bcsdlab/bcsdinternalapiv2/global/config/TimeConfig.java @@ -0,0 +1,22 @@ +package com.bcsdlab.bcsdinternalapiv2.global.config; + +import java.time.Clock; +import java.time.ZoneId; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 서비스의 "오늘"과 "지금"(회비 학기, 예약 가능 시각 등)은 한국 시간으로 정한다. 운영 컨테이너의 + * 시간대(UTC일 수 있음)에 기대지 않으려고 시스템 기본 시간대 대신 Asia/Seoul 고정 Clock을 주입한다. + * 테스트는 이 빈을 바꿔 시각을 고정한다. + */ +@Configuration +public class TimeConfig { + + public static final ZoneId SEOUL = ZoneId.of("Asia/Seoul"); + + @Bean + public Clock clock() { + return Clock.system(SEOUL); + } +} diff --git a/src/main/java/com/bcsdlab/bcsdinternalapiv2/ledger/config/LedgerTimeConfig.java b/src/main/java/com/bcsdlab/bcsdinternalapiv2/ledger/config/LedgerTimeConfig.java deleted file mode 100644 index 72f06d9..0000000 --- a/src/main/java/com/bcsdlab/bcsdinternalapiv2/ledger/config/LedgerTimeConfig.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.bcsdlab.bcsdinternalapiv2.ledger.config; - -import java.time.Clock; -import java.time.ZoneId; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * 회비의 "오늘"(현재 학기)은 한국 날짜로 정한다. 운영 컨테이너의 시간대(UTC일 수 있음)에 기대지 않으려고 - * 시스템 기본 시간대 대신 Asia/Seoul 고정 Clock을 주입한다. 테스트는 이 빈을 바꿔 날짜를 고정한다. - */ -@Configuration -public class LedgerTimeConfig { - - public static final ZoneId SEOUL = ZoneId.of("Asia/Seoul"); - - @Bean - public Clock ledgerClock() { - return Clock.system(SEOUL); - } -} diff --git a/src/main/java/com/bcsdlab/bcsdinternalapiv2/reservation/service/ReservationService.java b/src/main/java/com/bcsdlab/bcsdinternalapiv2/reservation/service/ReservationService.java index e5874a8..a491d6b 100644 --- a/src/main/java/com/bcsdlab/bcsdinternalapiv2/reservation/service/ReservationService.java +++ b/src/main/java/com/bcsdlab/bcsdinternalapiv2/reservation/service/ReservationService.java @@ -16,12 +16,12 @@ import com.bcsdlab.bcsdinternalapiv2.reservation.model.RepeatFrequency; import com.bcsdlab.bcsdinternalapiv2.reservation.repository.ReservationGroupRepository; import com.bcsdlab.bcsdinternalapiv2.reservation.repository.ReservationRepository; +import java.time.Clock; import java.time.DayOfWeek; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.YearMonth; -import java.time.ZoneId; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.HashSet; @@ -47,10 +47,11 @@ public class ReservationService { private final ReservationRepository reservationRepository; private final ReservationGroupRepository reservationGroupRepository; private final MemberRepository memberRepository; + private final Clock clock; @Transactional public ReservationCreateResponse create(Long memberId, ReservationCreateRequest request) { - Instant now = Instant.now(); + Instant now = Instant.now(clock); validateCommon(request, now); if (!request.isRepeat()) { @@ -89,7 +90,7 @@ public MonthlyOccupancyResponse getMyMonthlyOccupancy(Long memberId, YearMonth m @Transactional(readOnly = true) public MyReservationResponse getMyReservations(Long memberId, String status) { - LocalDate today = LocalDate.now(); + LocalDate today = LocalDate.now(clock); List reservations = "past".equalsIgnoreCase(status) ? reservationRepository .findByMemberIdAndCancelledAtIsNullAndReservationDateLessThanOrderByReservationDateDescStartMinuteDesc( @@ -116,7 +117,7 @@ public ReservationDetailResponse getDetail(Long memberId, Long reservationId) { @Transactional public void cancel(Long memberId, Long reservationId) { - Instant now = Instant.now(); + Instant now = Instant.now(clock); Reservation reservation = getOwnedReservation(memberId, reservationId); if (reservation.isCancelled()) { throw new ReservationException(ReservationExceptionType.ALREADY_CANCELLED); @@ -129,7 +130,7 @@ public void cancel(Long memberId, Long reservationId) { @Transactional public void cancelGroup(Long memberId, Long groupId) { - Instant now = Instant.now(); + Instant now = Instant.now(clock); ReservationGroup group = reservationGroupRepository.findById(groupId) .orElseThrow(() -> new ReservationException(ReservationExceptionType.RESERVATION_GROUP_NOT_FOUND)); if (!group.getMemberId().equals(memberId)) { @@ -200,11 +201,11 @@ private void validateCommon(ReservationCreateRequest request, Instant now) { throw new ReservationException(ReservationExceptionType.INVALID_HEADCOUNT); } LocalDateTime startDateTime = request.date().atStartOfDay().plusMinutes(start); - if (!startDateTime.isAfter(LocalDateTime.ofInstant(now, ZoneId.systemDefault()))) { + if (!startDateTime.isAfter(LocalDateTime.ofInstant(now, clock.getZone()))) { throw new ReservationException(ReservationExceptionType.PAST_TIME); } - LocalDate today = LocalDate.now(); + LocalDate today = LocalDate.now(clock); if (!request.isRepeat()) { if (request.date().isAfter(today.plusDays(SINGLE_BOOKING_WINDOW_DAYS))) { throw new ReservationException(ReservationExceptionType.OUT_OF_SINGLE_BOOKING_WINDOW); @@ -291,7 +292,7 @@ private Reservation getOwnedReservation(Long memberId, Long reservationId) { private boolean hasStarted(Reservation reservation, Instant now) { LocalDateTime startDateTime = reservation.getReservationDate().atStartOfDay() .plusMinutes(reservation.getStartMinute()); - return !startDateTime.isAfter(LocalDateTime.ofInstant(now, ZoneId.systemDefault())); + return !startDateTime.isAfter(LocalDateTime.ofInstant(now, clock.getZone())); } private List pickGroupRepresentatives(List reservations) { diff --git a/src/test/java/com/bcsdlab/bcsdinternalapiv2/ledger/MutableClock.java b/src/test/java/com/bcsdlab/bcsdinternalapiv2/MutableClock.java similarity index 73% rename from src/test/java/com/bcsdlab/bcsdinternalapiv2/ledger/MutableClock.java rename to src/test/java/com/bcsdlab/bcsdinternalapiv2/MutableClock.java index 76a9d65..90f2245 100644 --- a/src/test/java/com/bcsdlab/bcsdinternalapiv2/ledger/MutableClock.java +++ b/src/test/java/com/bcsdlab/bcsdinternalapiv2/MutableClock.java @@ -1,6 +1,6 @@ -package com.bcsdlab.bcsdinternalapiv2.ledger; +package com.bcsdlab.bcsdinternalapiv2; -import com.bcsdlab.bcsdinternalapiv2.ledger.config.LedgerTimeConfig; +import com.bcsdlab.bcsdinternalapiv2.global.config.TimeConfig; import java.time.Clock; import java.time.Instant; import java.time.LocalDate; @@ -12,7 +12,7 @@ public class MutableClock extends Clock { private volatile Instant instant = Instant.now(); public void setToday(LocalDate date) { - this.instant = date.atTime(12, 0).atZone(LedgerTimeConfig.SEOUL).toInstant(); + this.instant = date.atTime(12, 0).atZone(TimeConfig.SEOUL).toInstant(); } public void setInstant(Instant instant) { @@ -21,7 +21,7 @@ public void setInstant(Instant instant) { @Override public ZoneId getZone() { - return LedgerTimeConfig.SEOUL; + return TimeConfig.SEOUL; } @Override diff --git a/src/test/java/com/bcsdlab/bcsdinternalapiv2/ledger/LedgerIntegrationTestSupport.java b/src/test/java/com/bcsdlab/bcsdinternalapiv2/ledger/LedgerIntegrationTestSupport.java index c301a30..ee95975 100644 --- a/src/test/java/com/bcsdlab/bcsdinternalapiv2/ledger/LedgerIntegrationTestSupport.java +++ b/src/test/java/com/bcsdlab/bcsdinternalapiv2/ledger/LedgerIntegrationTestSupport.java @@ -4,6 +4,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.bcsdlab.bcsdinternalapiv2.IntegrationTestSupport; +import com.bcsdlab.bcsdinternalapiv2.MutableClock; import com.bcsdlab.bcsdinternalapiv2.auth.repository.RefreshTokenRepository; import com.bcsdlab.bcsdinternalapiv2.ledger.model.EntryType; import com.bcsdlab.bcsdinternalapiv2.ledger.model.LedgerCategory; diff --git a/src/test/java/com/bcsdlab/bcsdinternalapiv2/reservation/MyReservationIntegrationTest.java b/src/test/java/com/bcsdlab/bcsdinternalapiv2/reservation/MyReservationIntegrationTest.java index 5613be5..b160b7e 100644 --- a/src/test/java/com/bcsdlab/bcsdinternalapiv2/reservation/MyReservationIntegrationTest.java +++ b/src/test/java/com/bcsdlab/bcsdinternalapiv2/reservation/MyReservationIntegrationTest.java @@ -5,6 +5,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.bcsdlab.bcsdinternalapiv2.global.config.TimeConfig; import com.bcsdlab.bcsdinternalapiv2.member.model.Member; import com.bcsdlab.bcsdinternalapiv2.reservation.model.Reservation; import java.time.DayOfWeek; @@ -17,7 +18,7 @@ class MyReservationIntegrationTest extends ReservationIntegrationTestSupport { @Test void 예정된_예약_목록을_조회할_수_있다() throws Exception { String token = createActiveMemberAndLogin("20250401"); - LocalDate date = LocalDate.now().plusDays(3); + LocalDate date = LocalDate.now(TimeConfig.SEOUL).plusDays(3); mockMvc.perform(post("/v1/reservations") .header("Authorization", "Bearer " + token) .contentType(MediaType.APPLICATION_JSON) @@ -38,7 +39,7 @@ class MyReservationIntegrationTest extends ReservationIntegrationTestSupport { String token = login("20250402"); reservationRepository.save(Reservation.builder() .memberId(member.getId()) - .reservationDate(LocalDate.now().minusDays(3)) + .reservationDate(LocalDate.now(TimeConfig.SEOUL).minusDays(3)) .startMinute((short) 600) .endMinute((short) 660) .purpose("지난스터디") @@ -82,7 +83,7 @@ class MyReservationIntegrationTest extends ReservationIntegrationTestSupport { void 다른_회원의_예약은_조회되지_않는다() throws Exception { String ownerToken = createActiveMemberAndLogin("20250404"); String otherToken = createActiveMemberAndLogin("20250405"); - LocalDate date = LocalDate.now().plusDays(3); + LocalDate date = LocalDate.now(TimeConfig.SEOUL).plusDays(3); mockMvc.perform(post("/v1/reservations") .header("Authorization", "Bearer " + ownerToken) @@ -98,7 +99,7 @@ class MyReservationIntegrationTest extends ReservationIntegrationTestSupport { } private LocalDate nextDateWithWeekday(DayOfWeek dayOfWeek, int minDaysAhead) { - LocalDate date = LocalDate.now().plusDays(minDaysAhead); + LocalDate date = LocalDate.now(TimeConfig.SEOUL).plusDays(minDaysAhead); while (date.getDayOfWeek() != dayOfWeek) { date = date.plusDays(1); } diff --git a/src/test/java/com/bcsdlab/bcsdinternalapiv2/reservation/ReservationCancelIntegrationTest.java b/src/test/java/com/bcsdlab/bcsdinternalapiv2/reservation/ReservationCancelIntegrationTest.java index 41498a0..bb0a2a6 100644 --- a/src/test/java/com/bcsdlab/bcsdinternalapiv2/reservation/ReservationCancelIntegrationTest.java +++ b/src/test/java/com/bcsdlab/bcsdinternalapiv2/reservation/ReservationCancelIntegrationTest.java @@ -5,6 +5,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.bcsdlab.bcsdinternalapiv2.global.config.TimeConfig; import com.bcsdlab.bcsdinternalapiv2.member.model.Member; import com.bcsdlab.bcsdinternalapiv2.reservation.model.Reservation; import com.bcsdlab.bcsdinternalapiv2.reservation.model.ReservationGroup; @@ -20,7 +21,7 @@ class ReservationCancelIntegrationTest extends ReservationIntegrationTestSupport @Test void 본인_예약을_취소할_수_있다() throws Exception { String token = createActiveMemberAndLogin("20250601"); - LocalDate date = LocalDate.now().plusDays(3); + LocalDate date = LocalDate.now(TimeConfig.SEOUL).plusDays(3); Long reservationId = createSingleReservationAndGetId(token, date); mockMvc.perform(delete("/v1/reservations/{id}", reservationId) @@ -33,7 +34,7 @@ class ReservationCancelIntegrationTest extends ReservationIntegrationTestSupport @Test void 이미_취소된_예약을_다시_취소하면_409가_반환된다() throws Exception { String token = createActiveMemberAndLogin("20250602"); - LocalDate date = LocalDate.now().plusDays(3); + LocalDate date = LocalDate.now(TimeConfig.SEOUL).plusDays(3); Long reservationId = createSingleReservationAndGetId(token, date); mockMvc.perform(delete("/v1/reservations/{id}", reservationId) @@ -49,7 +50,7 @@ class ReservationCancelIntegrationTest extends ReservationIntegrationTestSupport void 타인의_예약을_취소하면_403이_반환된다() throws Exception { String ownerToken = createActiveMemberAndLogin("20250603"); String otherToken = createActiveMemberAndLogin("20250604"); - LocalDate date = LocalDate.now().plusDays(3); + LocalDate date = LocalDate.now(TimeConfig.SEOUL).plusDays(3); Long reservationId = createSingleReservationAndGetId(ownerToken, date); mockMvc.perform(delete("/v1/reservations/{id}", reservationId) @@ -63,7 +64,7 @@ class ReservationCancelIntegrationTest extends ReservationIntegrationTestSupport String token = login("20250605"); Reservation started = reservationRepository.save(Reservation.builder() .memberId(member.getId()) - .reservationDate(LocalDate.now().minusDays(1)) + .reservationDate(LocalDate.now(TimeConfig.SEOUL).minusDays(1)) .startMinute((short) 600) .endMinute((short) 660) .purpose("지난예약") @@ -79,9 +80,9 @@ class ReservationCancelIntegrationTest extends ReservationIntegrationTestSupport void 반복_예약_그룹을_전체_취소하면_미래_회차만_취소된다() throws Exception { Member member = createActiveMember("20250606"); String token = login("20250606"); - LocalDate pastDate = LocalDate.now().minusDays(7); - LocalDate futureDate1 = LocalDate.now().plusDays(3); - LocalDate futureDate2 = LocalDate.now().plusDays(10); + LocalDate pastDate = LocalDate.now(TimeConfig.SEOUL).minusDays(7); + LocalDate futureDate1 = LocalDate.now(TimeConfig.SEOUL).plusDays(3); + LocalDate futureDate2 = LocalDate.now(TimeConfig.SEOUL).plusDays(10); ReservationGroup group = reservationGroupRepository.save(ReservationGroup.builder() .memberId(member.getId()) @@ -134,7 +135,7 @@ class ReservationCancelIntegrationTest extends ReservationIntegrationTestSupport .weekdays(List.of(DayOfWeek.MONDAY)) .startMinute((short) 600) .endMinute((short) 660) - .repeatEndDate(LocalDate.now().plusWeeks(2)) + .repeatEndDate(LocalDate.now(TimeConfig.SEOUL).plusWeeks(2)) .createdAt(java.time.Instant.now()) .build()); diff --git a/src/test/java/com/bcsdlab/bcsdinternalapiv2/reservation/ReservationConcurrencyTest.java b/src/test/java/com/bcsdlab/bcsdinternalapiv2/reservation/ReservationConcurrencyTest.java index 3625a69..4c16874 100644 --- a/src/test/java/com/bcsdlab/bcsdinternalapiv2/reservation/ReservationConcurrencyTest.java +++ b/src/test/java/com/bcsdlab/bcsdinternalapiv2/reservation/ReservationConcurrencyTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.bcsdlab.bcsdinternalapiv2.global.config.TimeConfig; import java.time.LocalDate; import java.util.List; import java.util.concurrent.Callable; @@ -20,7 +21,7 @@ class ReservationConcurrencyTest extends ReservationIntegrationTestSupport { void 동시에_같은_시간대를_예약하면_정확히_1건만_성공한다() throws Exception { String firstToken = createActiveMemberAndLogin("20250701"); String secondToken = createActiveMemberAndLogin("20250702"); - LocalDate date = LocalDate.now().plusDays(3); + LocalDate date = LocalDate.now(TimeConfig.SEOUL).plusDays(3); String body = """ {"date":"%s","start":600,"end":660,"purpose":"동시성테스트","headcount":2} """.formatted(date); diff --git a/src/test/java/com/bcsdlab/bcsdinternalapiv2/reservation/ReservationCreateIntegrationTest.java b/src/test/java/com/bcsdlab/bcsdinternalapiv2/reservation/ReservationCreateIntegrationTest.java index 7241e69..3479b81 100644 --- a/src/test/java/com/bcsdlab/bcsdinternalapiv2/reservation/ReservationCreateIntegrationTest.java +++ b/src/test/java/com/bcsdlab/bcsdinternalapiv2/reservation/ReservationCreateIntegrationTest.java @@ -4,6 +4,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.bcsdlab.bcsdinternalapiv2.global.config.TimeConfig; import java.time.LocalDate; import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; @@ -14,7 +15,7 @@ class ReservationCreateIntegrationTest extends ReservationIntegrationTestSupport @Test void 로그인한_회원은_단건_예약을_생성할_수_있다() throws Exception { String token = createActiveMemberAndLogin("20250201"); - LocalDate date = LocalDate.now().plusDays(3); + LocalDate date = LocalDate.now(TimeConfig.SEOUL).plusDays(3); createReservation(token, date, 600, 660, "스터디", 4) .andExpect(status().isOk()) @@ -26,7 +27,7 @@ class ReservationCreateIntegrationTest extends ReservationIntegrationTestSupport @Test void 시간이_30분_단위가_아니면_예약이_거부된다() throws Exception { String token = createActiveMemberAndLogin("20250202"); - LocalDate date = LocalDate.now().plusDays(3); + LocalDate date = LocalDate.now(TimeConfig.SEOUL).plusDays(3); createReservation(token, date, 601, 660, "스터디", 4) .andExpect(status().isBadRequest()); @@ -35,7 +36,7 @@ class ReservationCreateIntegrationTest extends ReservationIntegrationTestSupport @Test void 시작시간이_종료시간보다_늦으면_예약이_거부된다() throws Exception { String token = createActiveMemberAndLogin("20250203"); - LocalDate date = LocalDate.now().plusDays(3); + LocalDate date = LocalDate.now(TimeConfig.SEOUL).plusDays(3); createReservation(token, date, 660, 600, "스터디", 4) .andExpect(status().isBadRequest()); @@ -44,7 +45,7 @@ class ReservationCreateIntegrationTest extends ReservationIntegrationTestSupport @Test void 이미_지난_시간은_예약할_수_없다() throws Exception { String token = createActiveMemberAndLogin("20250204"); - LocalDate date = LocalDate.now().minusDays(1); + LocalDate date = LocalDate.now(TimeConfig.SEOUL).minusDays(1); createReservation(token, date, 600, 660, "스터디", 4) .andExpect(status().isBadRequest()); @@ -53,7 +54,7 @@ class ReservationCreateIntegrationTest extends ReservationIntegrationTestSupport @Test void 이주를_초과한_날짜는_예약할_수_없다() throws Exception { String token = createActiveMemberAndLogin("20250205"); - LocalDate date = LocalDate.now().plusDays(20); + LocalDate date = LocalDate.now(TimeConfig.SEOUL).plusDays(20); createReservation(token, date, 600, 660, "스터디", 4) .andExpect(status().isBadRequest()); @@ -62,7 +63,7 @@ class ReservationCreateIntegrationTest extends ReservationIntegrationTestSupport @Test void 인원수가_한도를_초과하면_예약이_거부된다() throws Exception { String token = createActiveMemberAndLogin("20250206"); - LocalDate date = LocalDate.now().plusDays(3); + LocalDate date = LocalDate.now(TimeConfig.SEOUL).plusDays(3); createReservation(token, date, 600, 660, "스터디", 13) .andExpect(status().isBadRequest()); @@ -72,7 +73,7 @@ class ReservationCreateIntegrationTest extends ReservationIntegrationTestSupport void 겹치는_시간대는_예약할_수_없다() throws Exception { String firstToken = createActiveMemberAndLogin("20250207"); String secondToken = createActiveMemberAndLogin("20250208"); - LocalDate date = LocalDate.now().plusDays(3); + LocalDate date = LocalDate.now(TimeConfig.SEOUL).plusDays(3); createReservation(firstToken, date, 600, 660, "스터디", 4) .andExpect(status().isOk()); @@ -84,7 +85,7 @@ class ReservationCreateIntegrationTest extends ReservationIntegrationTestSupport @Test void 하루_3시간_한도를_초과하면_예약할_수_없다() throws Exception { String token = createActiveMemberAndLogin("20250209"); - LocalDate date = LocalDate.now().plusDays(3); + LocalDate date = LocalDate.now(TimeConfig.SEOUL).plusDays(3); createReservation(token, date, 540, 600, "스터디1", 2).andExpect(status().isOk()); createReservation(token, date, 600, 660, "스터디2", 2).andExpect(status().isOk()); @@ -96,7 +97,7 @@ class ReservationCreateIntegrationTest extends ReservationIntegrationTestSupport @Test void 비로그인_요청은_401이_반환된다() throws Exception { - LocalDate date = LocalDate.now().plusDays(3); + LocalDate date = LocalDate.now(TimeConfig.SEOUL).plusDays(3); mockMvc.perform(post("/v1/reservations") .contentType(MediaType.APPLICATION_JSON) .content(requestBody(date, 600, 660, "스터디", 4))) diff --git a/src/test/java/com/bcsdlab/bcsdinternalapiv2/reservation/ReservationDetailIntegrationTest.java b/src/test/java/com/bcsdlab/bcsdinternalapiv2/reservation/ReservationDetailIntegrationTest.java index 565217d..3fecbd9 100644 --- a/src/test/java/com/bcsdlab/bcsdinternalapiv2/reservation/ReservationDetailIntegrationTest.java +++ b/src/test/java/com/bcsdlab/bcsdinternalapiv2/reservation/ReservationDetailIntegrationTest.java @@ -6,6 +6,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.bcsdlab.bcsdinternalapiv2.global.config.TimeConfig; import com.fasterxml.jackson.databind.JsonNode; import java.time.DayOfWeek; import java.time.LocalDate; @@ -17,7 +18,7 @@ class ReservationDetailIntegrationTest extends ReservationIntegrationTestSupport @Test void 본인_예약_상세를_조회할_수_있다() throws Exception { String token = createActiveMemberAndLogin("20250501"); - LocalDate date = LocalDate.now().plusDays(3); + LocalDate date = LocalDate.now(TimeConfig.SEOUL).plusDays(3); Long reservationId = createSingleReservationAndGetId(token, date); mockMvc.perform(get("/v1/reservations/{id}", reservationId) @@ -57,7 +58,7 @@ class ReservationDetailIntegrationTest extends ReservationIntegrationTestSupport void 타인의_예약을_조회하면_403이_반환된다() throws Exception { String ownerToken = createActiveMemberAndLogin("20250503"); String otherToken = createActiveMemberAndLogin("20250504"); - LocalDate date = LocalDate.now().plusDays(3); + LocalDate date = LocalDate.now(TimeConfig.SEOUL).plusDays(3); Long reservationId = createSingleReservationAndGetId(ownerToken, date); mockMvc.perform(get("/v1/reservations/{id}", reservationId) @@ -88,7 +89,7 @@ private Long createSingleReservationAndGetId(String token, LocalDate date) throw } private LocalDate nextDateWithWeekday(DayOfWeek dayOfWeek, int minDaysAhead) { - LocalDate date = LocalDate.now().plusDays(minDaysAhead); + LocalDate date = LocalDate.now(TimeConfig.SEOUL).plusDays(minDaysAhead); while (date.getDayOfWeek() != dayOfWeek) { date = date.plusDays(1); } diff --git a/src/test/java/com/bcsdlab/bcsdinternalapiv2/reservation/ReservationRepeatCreateIntegrationTest.java b/src/test/java/com/bcsdlab/bcsdinternalapiv2/reservation/ReservationRepeatCreateIntegrationTest.java index df19568..3c502c5 100644 --- a/src/test/java/com/bcsdlab/bcsdinternalapiv2/reservation/ReservationRepeatCreateIntegrationTest.java +++ b/src/test/java/com/bcsdlab/bcsdinternalapiv2/reservation/ReservationRepeatCreateIntegrationTest.java @@ -4,6 +4,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.bcsdlab.bcsdinternalapiv2.global.config.TimeConfig; import com.bcsdlab.bcsdinternalapiv2.member.model.Member; import com.bcsdlab.bcsdinternalapiv2.reservation.model.Reservation; import java.time.DayOfWeek; @@ -95,7 +96,7 @@ class ReservationRepeatCreateIntegrationTest extends ReservationIntegrationTestS } private LocalDate nextDateWithWeekday(DayOfWeek dayOfWeek, int minDaysAhead) { - LocalDate date = LocalDate.now().plusDays(minDaysAhead); + LocalDate date = LocalDate.now(TimeConfig.SEOUL).plusDays(minDaysAhead); while (date.getDayOfWeek() != dayOfWeek) { date = date.plusDays(1); } diff --git a/src/test/java/com/bcsdlab/bcsdinternalapiv2/reservation/ReservationStatusIntegrationTest.java b/src/test/java/com/bcsdlab/bcsdinternalapiv2/reservation/ReservationStatusIntegrationTest.java index fe27e6b..1fd342d 100644 --- a/src/test/java/com/bcsdlab/bcsdinternalapiv2/reservation/ReservationStatusIntegrationTest.java +++ b/src/test/java/com/bcsdlab/bcsdinternalapiv2/reservation/ReservationStatusIntegrationTest.java @@ -5,6 +5,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.bcsdlab.bcsdinternalapiv2.global.config.TimeConfig; import com.bcsdlab.bcsdinternalapiv2.member.model.Member; import com.bcsdlab.bcsdinternalapiv2.reservation.model.Reservation; import java.time.LocalDate; @@ -16,7 +17,7 @@ class ReservationStatusIntegrationTest extends ReservationIntegrationTestSupport @Test void 비로그인_상태로_일별_예약을_조회하면_예약자_이름과_목적이_마스킹된다() throws Exception { Member member = createActiveMember("20250101"); - LocalDate date = LocalDate.now().plusDays(3); + LocalDate date = LocalDate.now(TimeConfig.SEOUL).plusDays(3); reservationRepository.save(Reservation.builder() .memberId(member.getId()) .reservationDate(date) @@ -39,7 +40,7 @@ class ReservationStatusIntegrationTest extends ReservationIntegrationTestSupport void 로그인_상태로_일별_예약을_조회하면_예약자_이름과_목적이_그대로_보인다() throws Exception { Member member = createActiveMember("20250102"); String token = login("20250102"); - LocalDate date = LocalDate.now().plusDays(3); + LocalDate date = LocalDate.now(TimeConfig.SEOUL).plusDays(3); reservationRepository.save(Reservation.builder() .memberId(member.getId()) .reservationDate(date) @@ -61,7 +62,7 @@ class ReservationStatusIntegrationTest extends ReservationIntegrationTestSupport @Test void 월별_예약_현황은_비로그인_상태에서도_조회할_수_있다() throws Exception { Member member = createActiveMember("20250103"); - LocalDate date = LocalDate.now().plusDays(3); + LocalDate date = LocalDate.now(TimeConfig.SEOUL).plusDays(3); reservationRepository.save(Reservation.builder() .memberId(member.getId()) .reservationDate(date) diff --git a/src/test/java/com/bcsdlab/bcsdinternalapiv2/reservation/ReservationTimeZoneIntegrationTest.java b/src/test/java/com/bcsdlab/bcsdinternalapiv2/reservation/ReservationTimeZoneIntegrationTest.java new file mode 100644 index 0000000..10f579b --- /dev/null +++ b/src/test/java/com/bcsdlab/bcsdinternalapiv2/reservation/ReservationTimeZoneIntegrationTest.java @@ -0,0 +1,89 @@ +package com.bcsdlab.bcsdinternalapiv2.reservation; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.bcsdlab.bcsdinternalapiv2.MutableClock; +import com.bcsdlab.bcsdinternalapiv2.global.config.TimeConfig; +import java.time.LocalDate; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Primary; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.ResultActions; + +/** + * 예약의 "지금"은 서버 시간대가 아니라 한국 시간이다. 운영 컨테이너는 UTC라서, 시스템 기본 시간대를 쓰면 + * 한국 시각 기준으로 이미 지난 시간을 최대 9시간까지 예약·취소할 수 있었다. + */ +@Import(ReservationTimeZoneIntegrationTest.TestClockConfig.class) +class ReservationTimeZoneIntegrationTest extends ReservationIntegrationTestSupport { + + private static final int NINE = 9 * 60; + private static final int TEN = 10 * 60; + private static final int ELEVEN = 11 * 60; + + @Autowired + private MutableClock clock; + + private LocalDate today; + + @BeforeEach + void setUpClock() { + today = LocalDate.now(TimeConfig.SEOUL).plusDays(1); + clock.setInstant(today.atTime(9, 30).atZone(TimeConfig.SEOUL).toInstant()); + } + + @Test + void 한국_시각으로_이미_지난_시간은_예약할_수_없다() throws Exception { + String token = createActiveMemberAndLogin("20250501"); + + createReservation(token, today, NINE, TEN) + .andExpect(status().isBadRequest()); + } + + @Test + void 한국_시각으로_아직_오지_않은_시간은_예약할_수_있다() throws Exception { + String token = createActiveMemberAndLogin("20250502"); + + createReservation(token, today, TEN, ELEVEN) + .andExpect(status().isOk()); + } + + @Test + void 한국_시각으로_이미_시작한_예약은_취소할_수_없다() throws Exception { + String token = createActiveMemberAndLogin("20250503"); + createReservation(token, today, TEN, ELEVEN).andExpect(status().isOk()); + Long reservationId = reservationRepository.findAll().getFirst().getId(); + + clock.setInstant(today.atTime(10, 30).atZone(TimeConfig.SEOUL).toInstant()); + + mockMvc.perform(delete("/v1/reservations/{id}", reservationId) + .header("Authorization", "Bearer " + token)) + .andExpect(status().isConflict()); + } + + private ResultActions createReservation(String token, LocalDate date, int start, int end) throws Exception { + return mockMvc.perform(post("/v1/reservations") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + {"date":"%s","start":%d,"end":%d,"purpose":"스터디","headcount":4} + """.formatted(date, start, end))); + } + + @TestConfiguration + static class TestClockConfig { + + @Bean + @Primary + MutableClock testClock() { + return new MutableClock(); + } + } +}