Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public ResponseEntity<Void> saveWeeklyScheduleAndRegularReservations(@Valid @Req
// 일자별 관리 페이지 반환
@GetMapping("/daily-schedule")
public ResponseEntity<List<DailyAvailableResponse>> findAllDailySchedulesAndReservations(@RequestParam("month") String month) {
List<DailyAvailableResponse> responses = adminReservationService.findDailyAvailableByMonth(month);
List<DailyAvailableResponse> responses = adminReservationService.findDailyAvailableForTwoMonths(month);
return ResponseEntity.ok().body(responses);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,6 @@ public List<WeeklyScheduleResponse> findAllWeeklySchedules() {
.toList();
}


// 주간 스케줄 저장
@Transactional
public void saveWeeklySchedule(List<WeeklyScheduleUpdateRequest> requests) {
Expand Down Expand Up @@ -175,23 +174,14 @@ public void deleteReservationsByAdmin(List<Long> reservationIds) {
reservationRepository.deleteAll(reservations);
}

public List<DailyAvailableResponse> findDailyAvailableByMonth(String yearMonth) {

public List<DailyAvailableResponse> findDailyAvailableForTwoMonths(String yearMonth) {
LocalDate start = DateUtil.parseMonthToFirstDate(yearMonth);
LocalDate end = start.plusMonths(2);

return Stream.iterate(start, date -> date.isBefore(end), date -> date.plusDays(1))
.map(this::convertDateToDailyAvailableResponse).toList();
}

private DailyAvailableResponse convertDateToDailyAvailableResponse(LocalDate date) {
return dailyScheduleRepository.findByDate(date)
.map(DailyAvailableResponse::from)
.orElseGet(() -> weeklyScheduleRepository.findByDayOfWeek(date.getDayOfWeek())
.map(schedule -> DailyAvailableResponse.of(date, schedule))
.orElseGet(() -> DailyAvailableResponse.createInactiveDate(date)));
}

// 정기 예약 생성
@Transactional
public void createRegularReservations(List<RegularReservationCreateRequest> requests){
Expand Down Expand Up @@ -261,6 +251,14 @@ private void generateDailyReservationsOverwrite(RegularReservation savedRegularR
reservationRepository.saveAll(news);
}

// 하루의 연습실 이용 가능 시간 반환
private DailyAvailableResponse convertDateToDailyAvailableResponse(LocalDate date) {
return dailyScheduleRepository.findByDate(date)
.map(DailyAvailableResponse::from)
.orElseGet(() -> weeklyScheduleRepository.findByDayOfWeek(date.getDayOfWeek())
.map(schedule -> DailyAvailableResponse.of(date, schedule))
.orElseGet(() -> DailyAvailableResponse.createInactiveDate(date)));
}

// 검증 메서드
private void validateNotPastDateSchedule(DailySchedule schedule){
Expand Down Expand Up @@ -312,4 +310,4 @@ private List<Reservation> findReservationsToDeleteByDailySchedules(DailySchedule
return reservations;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.keunsori.keunsoriserver.domain.reservation.controller;

import com.keunsori.keunsoriserver.domain.admin.reservation.dto.response.DailyAvailableResponse;
import com.keunsori.keunsoriserver.domain.admin.reservation.service.AdminReservationService;
import com.keunsori.keunsoriserver.domain.reservation.dto.response.DailyUnavailableSlotsResponse;
import jakarta.validation.Valid;

import org.springframework.http.ResponseEntity;
Expand Down Expand Up @@ -31,7 +30,6 @@
public class ReservationController {

private final ReservationService reservationService;
private final AdminReservationService adminReservationService;

@GetMapping("/list")
public ResponseEntity<List<ReservationResponse>> findAllReservations(@RequestParam("month") String month) {
Expand Down Expand Up @@ -73,8 +71,8 @@ public ResponseEntity<List<ReservationResponse>> findMyReservations() {

// 예약 신청 페이지 가능한 날짜 반환
@GetMapping
public ResponseEntity<List<DailyAvailableResponse>> findMonthlySchedule(@RequestParam("month") String month){
List<DailyAvailableResponse> responses = adminReservationService.findDailyAvailableByMonth(month);
public ResponseEntity<List<DailyUnavailableSlotsResponse>> findMonthlySchedule(@RequestParam("month") String month){
List<DailyUnavailableSlotsResponse> responses = reservationService.findDailyUnavailableSlotsForTwoMonths(month);
return ResponseEntity.ok().body(responses);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.keunsori.keunsoriserver.domain.reservation.dto.response;

import java.time.LocalDate;
import java.util.List;

public record DailyUnavailableSlotsResponse(
LocalDate date,
boolean isActive,
// 예약 불가능한 시간의 슬롯(24시간을 30분단위로 나눈 48개의 슬롯) 인덱스 리스트 반환
List<Integer> unavailableSlots
) {
public static DailyUnavailableSlotsResponse of(LocalDate date, boolean isActive, List<Integer> unavailableSlots){
return new DailyUnavailableSlotsResponse(
date,
isActive,
List.copyOf(unavailableSlots)
);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package com.keunsori.keunsoriserver.domain.reservation.service;

import com.keunsori.keunsoriserver.domain.admin.reservation.domain.WeeklySchedule;
import com.keunsori.keunsoriserver.domain.reservation.dto.response.DailyUnavailableSlotsResponse;
import com.keunsori.keunsoriserver.domain.admin.reservation.repository.DailyScheduleRepository;
import com.keunsori.keunsoriserver.domain.admin.reservation.repository.WeeklyScheduleRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -17,7 +21,9 @@
import com.keunsori.keunsoriserver.global.util.MemberUtil;

import java.time.LocalDate;
import java.util.List;
import java.time.LocalTime;
import java.util.*;
import java.util.stream.Stream;

import lombok.RequiredArgsConstructor;

Expand All @@ -29,6 +35,8 @@
public class ReservationService {

private final ReservationRepository reservationRepository;
private final DailyScheduleRepository dailyScheduleRepository;
private final WeeklyScheduleRepository weeklyScheduleRepository;
private final ReservationValidator reservationValidator;
private final MemberUtil memberUtil;

Expand Down Expand Up @@ -106,4 +114,76 @@ public List<ReservationResponse> findAllMyReservations() {
return reservationRepository.findAllByMemberOrderByDateDescStartTimeDesc(member)
.stream().map(ReservationResponse::from).toList();
}

// 예약 가능한 시간 테이블 반환
public List<DailyUnavailableSlotsResponse> findDailyUnavailableSlotsForTwoMonths(String yearMonth) {

LocalDate start = DateUtil.parseMonthToFirstDate(yearMonth);
LocalDate end = start.plusMonths(2);

return Stream.iterate(start, date -> date.isBefore(end), date -> date.plusDays(1))
.map(this::convertDateToDailyUnavailableSlotsResponse).toList();
}

// 하루의 예약 불가능 시간 슬롯 Dto 반환
private DailyUnavailableSlotsResponse convertDateToDailyUnavailableSlotsResponse(LocalDate date) {
List<Reservation> reservations = reservationRepository.findAllByDate(date);

// 일간 스케줄이 있을 경우 계산, 없을 경우 주간 스케줄로 계산
return dailyScheduleRepository.findByDate(date)
.map(dailySchedule -> {
List<Integer> unavailableSlots = generateUnavailableSlots(
dailySchedule.getStartTime(),
dailySchedule.getEndTime(),
reservations
);
return DailyUnavailableSlotsResponse.of(date, dailySchedule.isActive(), unavailableSlots);
})
.orElseGet(() -> {
WeeklySchedule weeklySchedule = weeklyScheduleRepository.findByDayOfWeek(date.getDayOfWeek())
.orElseThrow(() -> new ReservationException(WEEKLY_SCHEDULE_NOT_FOUND));
List<Integer> unavailableSlots = generateUnavailableSlots(
weeklySchedule.getStartTime(),
weeklySchedule.getEndTime(),
reservations
);
return DailyUnavailableSlotsResponse.of(date, weeklySchedule.isActive(), unavailableSlots);
});
}

private List<Integer> generateUnavailableSlots(LocalTime start, LocalTime end, List<Reservation> reservations) {
Set<Integer> unavailable = new HashSet<>();
int scheduleStartIndex = toStartIndex(start);
int scheduleEndIndex = toEndIndex(end);

for (int i = 0; i < scheduleStartIndex; i++) {
unavailable.add(i);
}
for (int i = scheduleEndIndex; i < 48; i++) {
unavailable.add(i);
}

for (Reservation reservation : reservations) {
int startIndex = toStartIndex(reservation.getStartTime());
int endIndex = toEndIndex(reservation.getEndTime());
for (int i = startIndex; i < endIndex; i++) {
if (i >= 0 && i < 48) {
unavailable.add(i);
}
}
}

List<Integer> result = new ArrayList<>(unavailable);
Collections.sort(result);
return result;
}

private int toStartIndex(LocalTime time) {
return time.getHour() * 2 + (time.getMinute() >= 30 ? 1 : 0);
}

private int toEndIndex(LocalTime time) {
return time.getHour() * 2 + (time.getMinute() > 0 ? (time.getMinute() > 30 ? 2 : 1) : 0);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public class ErrorMessage {
public static final String INVALID_SCHEDULE_TIME = "시작 시간과 끝 시간의 순서가 올바르지 않습니다.";
public static final String PARTIAL_RESERVATION_NOT_FOUND = "삭제하려는 일부 예약이 존재하지 않습니다.";
public static final String EMPTY_MANAGEMENT_REQUEST = "주간 스케줄, 정기 예약 생성, 삭제 중 최소 하나는 존재해야 합니다.";
public static final String WEEKLY_SCHEDULE_NOT_FOUND = "주간 스케줄이 존재하지 않습니다.";

// Regular Reservation
public static final String REGULAR_RESERVATION_NOT_DELETABLE = "정기 예약은 관리자 또는 예약 팀장만 삭제할 수 있습니다.";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,31 @@
package com.keunsori.keunsoriserver.admin.init;

import com.keunsori.keunsoriserver.common.ApiTest;
import com.keunsori.keunsoriserver.global.init.WeeklyScheduleInitializer;
import com.keunsori.keunsoriserver.domain.admin.reservation.domain.WeeklySchedule;
import com.keunsori.keunsoriserver.domain.admin.reservation.repository.WeeklyScheduleRepository;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;

import java.time.DayOfWeek;
import java.time.LocalTime;

public class ApiTestWithWeeklyScheduleInit extends ApiTest {

@Autowired
private WeeklyScheduleInitializer weeklyScheduleInitializer;
private WeeklyScheduleRepository weeklyScheduleRepository;

@BeforeEach
void setupWeeklySchedule() {
weeklyScheduleInitializer.initializeSchedules();
weeklyScheduleRepository.deleteAll();

for (DayOfWeek day : DayOfWeek.values()) {
WeeklySchedule schedule = WeeklySchedule.builder()
.dayOfWeek(day)
.isActive(true)
.startTime(LocalTime.of(10, 0))
.endTime(LocalTime.of(23, 0))
.build();
weeklyScheduleRepository.save(schedule);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
import static com.keunsori.keunsoriserver.global.exception.ErrorMessage.INVALID_RESERVATION_TIME;
import static com.keunsori.keunsoriserver.global.exception.ErrorMessage.RESERVATION_ALREADY_COMPLETED;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
import static org.springframework.http.HttpHeaders.AUTHORIZATION;
import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
import static org.springframework.http.HttpHeaders.LOCATION;

import com.keunsori.keunsoriserver.admin.init.ApiTestWithWeeklyScheduleInit;
import com.keunsori.keunsoriserver.domain.reservation.domain.Reservation;
import com.keunsori.keunsoriserver.domain.reservation.domain.vo.ReservationType;
import com.keunsori.keunsoriserver.domain.reservation.domain.vo.Session;
Expand All @@ -22,14 +24,14 @@
import org.springframework.beans.factory.annotation.Autowired;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.keunsori.keunsoriserver.common.ApiTest;
import com.keunsori.keunsoriserver.domain.reservation.dto.requset.ReservationCreateRequest;

import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.stream.Stream;

public class ReservationApiTest extends ApiTest {
public class ReservationApiTest extends ApiTestWithWeeklyScheduleInit {

private String authorizationValue;

Expand Down Expand Up @@ -473,4 +475,21 @@ static Stream<Arguments> successReservationTimeTestData() {

Assertions.assertThat(errorMessage).isEqualTo(RESERVATION_ALREADY_COMPLETED);
}

@Test
void 예약_가능한_시간_테이블_반환에_성공한다(){
String yearMonth = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM"));

given().
header(AUTHORIZATION, authorizationValue).
header(CONTENT_TYPE, "application/json").
queryParam("month", yearMonth).
when().
get("/reservation").
then().
statusCode(200)
.body("[0].date", notNullValue())
.body("[0].unavailableSlots.size()", equalTo(22)) // ApiTestWithWeeklyScheduleInit 수정 시 반영 필요
.body("[0].unavailableSlots", everyItem(allOf(greaterThanOrEqualTo(0), lessThan(48))));
}
}
Loading