diff --git a/src/main/java/com/sideProject/PlanIT/common/response/ErrorCode.java b/src/main/java/com/sideProject/PlanIT/common/response/ErrorCode.java index 5e3882f..52cfe6e 100644 --- a/src/main/java/com/sideProject/PlanIT/common/response/ErrorCode.java +++ b/src/main/java/com/sideProject/PlanIT/common/response/ErrorCode.java @@ -41,6 +41,7 @@ public enum ErrorCode { TrainerSchedule_NOT_FOUND(404, "트레이너 일정을 찾을 수 없습니다"), NOT_SUSPEND_PROGRAM(422, "일시정지 요청이 거부되었습니다."), + IS_WORK_TIME(422, "근무 시간에는 예약설정이 불가능합니다."), SUSPEND_REQUEST_DENIED(422, "일시정지 요청이 거부되었습니다."); diff --git a/src/main/java/com/sideProject/PlanIT/domain/program/controller/ProgramAdminController.java b/src/main/java/com/sideProject/PlanIT/domain/program/controller/ProgramAdminController.java index 8fd6e24..d1eb41c 100644 --- a/src/main/java/com/sideProject/PlanIT/domain/program/controller/ProgramAdminController.java +++ b/src/main/java/com/sideProject/PlanIT/domain/program/controller/ProgramAdminController.java @@ -35,8 +35,6 @@ public ApiResponse> find( @RequestParam(value = "option", required = false, defaultValue = "VALID") ProgramSearchStatus option, @PageableDefault(size = 10) Pageable pageable, Principal principal) { - //todo : spring security 개발 후 토큰에서 userID를 전달해 줘야함. - Authentication loggedInUser = SecurityContextHolder.getContext().getAuthentication(); Long id = Long.parseLong(principal.getName()); return ApiResponse.ok( @@ -50,7 +48,6 @@ public ApiResponse> find( @PathVariable("id") Long id, @PageableDefault(size = 10) Pageable pageable, @RequestParam(value = "option", required = false, defaultValue = "VALID") ProgramSearchStatus option) { - //todo : spring security 개발 후 토큰에서 userID를 전달해 줘야함. return ApiResponse.ok( programService.findByUser(id, option, pageable) ); diff --git a/src/main/java/com/sideProject/PlanIT/domain/reservation/entity/Reservation.java b/src/main/java/com/sideProject/PlanIT/domain/reservation/entity/Reservation.java index a244ced..84efb11 100644 --- a/src/main/java/com/sideProject/PlanIT/domain/reservation/entity/Reservation.java +++ b/src/main/java/com/sideProject/PlanIT/domain/reservation/entity/Reservation.java @@ -6,18 +6,24 @@ import com.sideProject.PlanIT.domain.reservation.entity.ENUM.ReservationStatus; import com.sideProject.PlanIT.domain.user.entity.Employee; import com.sideProject.PlanIT.domain.user.entity.Member; +import com.sideProject.PlanIT.domain.user.entity.WorkTime; +import com.sideProject.PlanIT.domain.user.entity.enums.Week; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.apache.catalina.User; import java.sql.Time; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.util.List; @Entity +@Slf4j @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter public class Reservation { @@ -100,4 +106,23 @@ private boolean isPastReservationThreshold(LocalDateTime now) { return now.isAfter(reservedTime.minusMinutes(RESERVATION_THRESHOLD_MINUTES)); } + // 예약 시간이 출근 시간 안에 포함 되는지 체크 + public boolean isWithinEmployeeWorkTime(List employeeWorkTimes) { + LocalDate reservationDate = reservedTime.toLocalDate(); + LocalTime reservationTime = reservedTime.toLocalTime(); + + for (WorkTime workTime : employeeWorkTimes) { + if (workTime.getWeek().getDayOfWeek() == reservationDate.getDayOfWeek()) { + if ( isWithinTime(workTime.getStartAt(), workTime.getEndAt(), reservationTime) ) { + return true; // 예약 시간이 직원의 근무 시간 내 + } + } + } + return false; // 근무 시간 외 + } + + // 특정 시간이 사이에 존재하는지 확인 + private boolean isWithinTime(LocalTime start, LocalTime end, LocalTime reservedTime) { + return (reservedTime.isAfter(start) && reservedTime.isBefore(end)) || reservedTime.equals(start) || reservedTime.equals(end); + } } diff --git a/src/main/java/com/sideProject/PlanIT/domain/reservation/service/ReservationServiceImpl.java b/src/main/java/com/sideProject/PlanIT/domain/reservation/service/ReservationServiceImpl.java index e012252..0f310cf 100644 --- a/src/main/java/com/sideProject/PlanIT/domain/reservation/service/ReservationServiceImpl.java +++ b/src/main/java/com/sideProject/PlanIT/domain/reservation/service/ReservationServiceImpl.java @@ -11,9 +11,12 @@ import com.sideProject.PlanIT.domain.reservation.repository.ReservationRepository; import com.sideProject.PlanIT.domain.user.entity.Employee; import com.sideProject.PlanIT.domain.user.entity.Member; +import com.sideProject.PlanIT.domain.user.entity.WorkTime; import com.sideProject.PlanIT.domain.user.entity.enums.MemberRole; +import com.sideProject.PlanIT.domain.user.entity.enums.Week; import com.sideProject.PlanIT.domain.user.repository.EmployeeRepository; import com.sideProject.PlanIT.domain.user.repository.MemberRepository; +import com.sideProject.PlanIT.domain.user.repository.WorkTimeRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -38,6 +41,7 @@ public class ReservationServiceImpl implements ReservationService { private final MemberRepository memberRepository; private final EmployeeRepository employeeRepository; private final ProgramRepository programRepository; + private final WorkTimeRepository workTimeRepository; @Override @Transactional @@ -49,11 +53,21 @@ public String changeAvailability(LocalDate reservedDate,List reserved new CustomException(member.getId() + "은 직원이 아닙니다.", ErrorCode.NO_AUTHORITY) ); + // 해당 직원의 해당 요일의 근무 시간 조회 + Week week = Week.from(reservedDate); + List workTimesForDay = workTimeRepository.findByEmployeeIdAndWeek(employee.getId(), week); + List reservedDateTimes = createLocalDateTimes(reservedDate, reservedTimes); List existingReservations = reservationRepository.findByEmployeeAndReservedTimeIn(employee, reservedDateTimes); + //근무시간 내인지 체크 + for(LocalDateTime dateTime: reservedDateTimes) { + if(isAvailableForReservation(dateTime, workTimesForDay)) { + throw new CustomException(employee.getId() + " " + dateTime + "은 근무시간 입니다.", ErrorCode.EMPLOYEE_NOT_FOUND); + } + } // 기존 예약 삭제 List reservedReservations = existingReservations.stream() @@ -79,6 +93,16 @@ public String changeAvailability(LocalDate reservedDate,List reserved return "ok"; } + // 예약 가능성 확인: 지정된 예약 시간이 직원의 근무 시간 외인지 확인 + private boolean isAvailableForReservation(LocalDateTime dateTime, List workTimesForDay) { + LocalTime time = dateTime.toLocalTime(); + + return workTimesForDay.stream() + .noneMatch(workTime -> + (time.isAfter(workTime.getStartAt()) || time.equals(workTime.getStartAt())) && + (time.isBefore(workTime.getEndAt()) || time.equals(workTime.getEndAt()))); + } + public static List createLocalDateTimes(LocalDate date, List times) { // Stream을 사용하여 각 LocalTime 요소에 대해 LocalDate와 결합 return times.stream() @@ -136,6 +160,12 @@ public Map> findReservationForWeekByMemb new CustomException("존재하지 않는 트레이너입니다.", ErrorCode.MEMBER_NOT_FOUND) ); reservations = findReservationByEmployee(employee,startOfWeek,endOfWeek,option); + + //예약 시간이 출퇴근 시간 사이에 존재하는지 확인 + List workTimes = workTimeRepository.findByEmployeeId(employee.getId()); + reservations = reservations.stream() + .filter(reservation -> !reservation.isWithinEmployeeWorkTime(workTimes)) + .toList(); } else { reservations = findReservationByMember(member,startOfWeek,endOfWeek,option); } @@ -185,15 +215,17 @@ public List findReservationForDayByEmployee(LocalDate da LocalDateTime startOfWeek = calStartOfDay(date); LocalDateTime endOfWeek = calEndOfDay(date); - List reservations; //트레이너이면 Employee employee = employeeRepository.findById(employeeId).orElseThrow(() -> new CustomException("존재하지 않는 트레이너입니다.", ErrorCode.MEMBER_NOT_FOUND) ); - reservations = reservationRepository.findByEmployeeAndDateTimeBetween(employee,startOfWeek,endOfWeek); + List workTimes = workTimeRepository.findByEmployeeId(employeeId); + List reservations = reservationRepository.findByEmployeeAndDateTimeBetween(employee,startOfWeek,endOfWeek); + return reservations.stream() - .map(ReservationResponseDto::of) + .filter(reservation -> !reservation.isWithinEmployeeWorkTime(workTimes)) + .map(ReservationResponse::of) .toList(); } diff --git a/src/main/java/com/sideProject/PlanIT/domain/user/entity/enums/Week.java b/src/main/java/com/sideProject/PlanIT/domain/user/entity/enums/Week.java index 4578443..d121709 100644 --- a/src/main/java/com/sideProject/PlanIT/domain/user/entity/enums/Week.java +++ b/src/main/java/com/sideProject/PlanIT/domain/user/entity/enums/Week.java @@ -1,19 +1,36 @@ package com.sideProject.PlanIT.domain.user.entity.enums; +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalDateTime; + public enum Week { + //todo : week 양식 수정 + Mon(DayOfWeek.MONDAY), // 월 + Tue(DayOfWeek.TUESDAY), // 화 + wed(DayOfWeek.WEDNESDAY), // 수 + thu(DayOfWeek.THURSDAY), // 목 + fri(DayOfWeek.FRIDAY), // 금 + sat(DayOfWeek.SATURDAY), // 토 + sun(DayOfWeek.SUNDAY); // 일 + + private final DayOfWeek dayOfWeek; + + Week(DayOfWeek dayOfWeek) { + this.dayOfWeek = dayOfWeek; + } + + public DayOfWeek getDayOfWeek() { + return this.dayOfWeek; + } - //월 - Mon, - //화 - Tue, - //수 - wed, - //목 - thu, - //금 - fri, - //토 - sat, - //일 - sun + public static Week from(LocalDate dateTime) { + DayOfWeek dayOfWeek = dateTime.getDayOfWeek(); + for (Week week : Week.values()) { + if (week.getDayOfWeek() == dayOfWeek) { + return week; + } + } + throw new IllegalArgumentException("No Week enum for DayOfWeek: " + dayOfWeek); + } } diff --git a/src/main/java/com/sideProject/PlanIT/domain/user/repository/WorktimeRepository.java b/src/main/java/com/sideProject/PlanIT/domain/user/repository/WorkTimeRepository.java similarity index 62% rename from src/main/java/com/sideProject/PlanIT/domain/user/repository/WorktimeRepository.java rename to src/main/java/com/sideProject/PlanIT/domain/user/repository/WorkTimeRepository.java index 9beafe7..db4f3dd 100644 --- a/src/main/java/com/sideProject/PlanIT/domain/user/repository/WorktimeRepository.java +++ b/src/main/java/com/sideProject/PlanIT/domain/user/repository/WorkTimeRepository.java @@ -2,11 +2,13 @@ import com.sideProject.PlanIT.domain.user.entity.WorkTime; +import com.sideProject.PlanIT.domain.user.entity.enums.Week; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.util.List; @Repository -public interface WorktimeRepository extends JpaRepository { +public interface WorkTimeRepository extends JpaRepository { List findByEmployeeId(Long employee_id); + List findByEmployeeIdAndWeek(Long employeeId, Week week); } diff --git a/src/main/java/com/sideProject/PlanIT/domain/user/service/WorktimeServiceImpl.java b/src/main/java/com/sideProject/PlanIT/domain/user/service/WorktimeServiceImpl.java index 23c1275..aba527f 100644 --- a/src/main/java/com/sideProject/PlanIT/domain/user/service/WorktimeServiceImpl.java +++ b/src/main/java/com/sideProject/PlanIT/domain/user/service/WorktimeServiceImpl.java @@ -13,7 +13,7 @@ import com.sideProject.PlanIT.domain.user.entity.enums.MemberRole; import com.sideProject.PlanIT.domain.user.repository.EmployeeRepository; import com.sideProject.PlanIT.domain.user.repository.MemberRepository; -import com.sideProject.PlanIT.domain.user.repository.WorktimeRepository; +import com.sideProject.PlanIT.domain.user.repository.WorkTimeRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; @@ -33,7 +33,7 @@ public class WorktimeServiceImpl implements WorktimeService { // 트레이너 출퇴근 등록 private final EmployeeRepository employeeRepository; private final MemberRepository memberRepository; - private final WorktimeRepository worktimeRepository; + private final WorkTimeRepository worktimeRepository; @Override public TrainerScheduleRegistrationResponseDto trainerScheduleRegistration(List request, Long id){ @@ -41,11 +41,15 @@ public TrainerScheduleRegistrationResponseDto trainerScheduleRegistration(List new CustomException("존재하지 않는 직원입니다", ErrorCode.EMPLOYEE_NOT_FOUND)); - for (TrainerScheduleRequestDto requestdto : request){ - worktimeRepository.save(WorkTime.builder().week(requestdto.getWeek()).startAt(requestdto.getStartAt()).endAt(requestdto.getEndAt()).employee(trainer).build()); + for (TrainerScheduleRequestDto requestdto : request){ + worktimeRepository.save(WorkTime.builder() + .week(requestdto.getWeek()) + .startAt(requestdto.getStartAt()) + .endAt(requestdto.getEndAt()) + .employee(trainer) + .build()); - } - return TrainerScheduleRegistrationResponseDto.of(trainer.getId(),"출퇴근시간이 등록되었습니다."); + return TrainerScheduleRegistrationResponse.of(trainer.getId(),"출퇴근시간이 등록되었습니다."); } // 특정 일정 가져오기 diff --git a/src/test/java/com/sideProject/PlanIT/domain/reservation/service/ReservationServiceTest.java b/src/test/java/com/sideProject/PlanIT/domain/reservation/service/ReservationServiceTest.java index c404817..5e5bc4f 100644 --- a/src/test/java/com/sideProject/PlanIT/domain/reservation/service/ReservationServiceTest.java +++ b/src/test/java/com/sideProject/PlanIT/domain/reservation/service/ReservationServiceTest.java @@ -16,9 +16,12 @@ import com.sideProject.PlanIT.domain.reservation.repository.ReservationRepository; import com.sideProject.PlanIT.domain.user.entity.Employee; import com.sideProject.PlanIT.domain.user.entity.Member; +import com.sideProject.PlanIT.domain.user.entity.WorkTime; import com.sideProject.PlanIT.domain.user.entity.enums.MemberRole; +import com.sideProject.PlanIT.domain.user.entity.enums.Week; import com.sideProject.PlanIT.domain.user.repository.EmployeeRepository; import com.sideProject.PlanIT.domain.user.repository.MemberRepository; +import com.sideProject.PlanIT.domain.user.repository.WorkTimeRepository; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; @@ -59,11 +62,14 @@ class ReservationServiceTest { ReservationRepository reservationRepository; @Autowired ReservationService reservationService; + @Autowired + WorkTimeRepository worktimeRepository; DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd H:mm"); @AfterEach void tearDown() { + worktimeRepository.deleteAllInBatch(); reservationRepository.deleteAllInBatch(); programRepository.deleteAllInBatch(); registrationRepository.deleteAllInBatch(); @@ -265,6 +271,34 @@ void addReservation5() { .isInstanceOf(CustomException.class) .hasMessage(trainer.getId() + "은 직원이 아닙니다."); } + + @DisplayName("실패 : 근무시간 내에는 예약이 설정이 불가능하다") + @Test + void addReservation6() { + //given + Employee trainer = initTrainer("trainer"); + + WorkTime workTime = WorkTime.builder() + .employee(trainer) + .week(Week.Tue) + .startAt(LocalTime.of(10,0,0)) + .endAt(LocalTime.of(11,0,0)) + .build(); + worktimeRepository.save(workTime); + + LocalDate date = LocalDate.of(2023,3,19); + LocalTime time1 = LocalTime.of(10, 0); + LocalTime time2 = LocalTime.of(11, 0, 0); + LocalTime time3 = LocalTime.of(12, 0, 0); + LocalTime time4 = LocalTime.of(13, 0, 0); + + List times = List.of(time1, time2, time3, time4); + //when + //then + assertThatThrownBy(() -> reservationService.changeAvailability(date,times, trainer.getId())) + .isInstanceOf(CustomException.class) + .hasMessage(trainer.getId() + " 2023-03-19T10:00은 근무시간 입니다."); + } } @Nested @@ -1553,5 +1587,104 @@ void findReservationForDayByEmployee(){ ).containsExactly( null,reservationTime4, ReservationStatus.POSSIBLE); } + + @DisplayName("트레이너의 출근 시간 외의 예약을 조회 가능하다") + @Test + void findReservationForDayByEmployee2(){ + //given + Period periodOfTenDays = Period.ofMonths(0); + Product product = initProduct("PT 30회권", periodOfTenDays,30,ProductType.PT); + Employee trainer = initTrainer("trainer"); + Member member1 = initMember("tester1",MemberRole.MEMBER); + + Registration registration = Registration.builder() + .product(product) + .member(member1) + .discount(0) + .totalPrice(30000) + .status(RegistrationStatus.ACCEPTED) + .paymentAt(LocalDateTime.parse("2024-03-10 00:00", DATE_TIME_FORMATTER)) + .registrationAt(LocalDateTime.parse("2000-03-10 00:00", DATE_TIME_FORMATTER)) + .refundAt(null) + .build(); + Registration saveRegistration = registrationRepository.save(registration); + + Program program = Program.builder() + .registration(saveRegistration) + .product(saveRegistration.getProduct()) + .member(saveRegistration.getMember()) + .employee(trainer) + .status(IN_PROGRESS) + .startAt(LocalDate.parse("2024-03-10", DateTimeFormatter.ISO_DATE)) + .build(); + Program program1 = programRepository.save(program); + + WorkTime workTime = WorkTime.builder() + .employee(trainer) + .week(Week.Tue) + .startAt(LocalTime.of(10,0,0)) + .endAt(LocalTime.of(11,0,0)) + .build(); + worktimeRepository.save(workTime); + + LocalDateTime reservationTime1 = LocalDateTime.of(2024, 3, 19, 10, 0, 0); + Reservation reservation1 = Reservation.builder() + .reservedTime(reservationTime1) + .employee(trainer) + .status(ReservationStatus.POSSIBLE) + .classTime(LocalTime.of(1,0)) + .build(); + + LocalDateTime reservationTime2= LocalDateTime.of(2024, 3, 19, 11, 0, 0); + Reservation reservation2 = Reservation.builder() + .reservedTime(reservationTime2) + .employee(trainer) + .status(ReservationStatus.POSSIBLE) + .classTime(LocalTime.of(1,0)) + .build(); + + LocalDateTime reservationTime3 = LocalDateTime.of(2024, 3, 19, 12, 0, 0); + Reservation reservation3 = Reservation.builder() + .reservedTime(reservationTime3) + .employee(trainer) + .status(ReservationStatus.POSSIBLE) + .classTime(LocalTime.of(1,0)) + .build(); + + LocalDateTime reservationTime4 = LocalDateTime.of(2024, 3, 20, 12, 0, 0); + Reservation reservation4 = Reservation.builder() + .reservedTime(reservationTime4) + .employee(trainer) + .status(ReservationStatus.POSSIBLE) + .classTime(LocalTime.of(1,0)) + .build(); + + LocalDateTime reservationTime = LocalDateTime.of(2024, 3, 16, 10, 0, 0); + + reservation1.reservation(program1,member1,reservationTime); + + LocalDate today = LocalDate.of(2024, 3, 19); + LocalDate today2 = LocalDate.of(2024, 3, 20); + + List reservations = List.of(reservation1,reservation2,reservation3,reservation4); + reservationRepository.saveAll(reservations); + //when + List result1 = reservationService.findReservationForDayByEmployee(today, trainer.getId()); + List result2 = reservationService.findReservationForDayByEmployee(today2, trainer.getId()); + + + //then + assertThat(result1).hasSize(1); + assertThat(result1).extracting( + "programId","reservationTime", "status" + ).contains( + tuple(null,reservationTime3, ReservationStatus.POSSIBLE) + ); + assertThat(result2).hasSize(1); + assertThat(result2.get(0)).extracting( + "programId","reservationTime", "status" + ).containsExactly( + null,reservationTime4, ReservationStatus.POSSIBLE); + } } } \ No newline at end of file