사용자가 예약을 생성하고 업데이트하며, 예약 상태를 확인하고 처리할 수 있도록 구현하였습니다.
즉, 구현된 기능 요약을 하면 아래와 같습니다.
DTO 와 toResponse()활용:
예약 정보를 전송하기 위해 ReservationDto라는 DTO를 사용했습니다. 이 DTO는 예약을 생성하거나 상태를 업데이트하는 데 필요한 정보를 포함하고 있습니다. 즉, 클라이언트로부터 전달받은 예약 관련 정보를 담고 있습니다.
@Getter
@Setter
public class ReservationDto {
public enum UserType{
MEMBER, ADMIN
}
private UserType userType;
private Long memberId;
private Long hospitalId;
private Long subjectId;
private ReservationStatus status;
}
toResponse() 메서드는 Reservation 엔티티의 정보를 DTO로 변환하는 역할을 합니다. 이 메서드는 클라이언트로 응답할 때 예약 정보를 적절한 형식으로 전달하기 위해 사용됩니다. 엔티티를 DTO로 변환하여 클라이언트에게 응답하는 데 사용됩니다.
@Entity
@Getter
@SuperBuilder(toBuilder = true)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Reservation extends BaseEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "admin_id")
private Admin admin;
@Column(nullable = false)
private LocalDateTime date;
@ColumnDefault("FALSE")
@Column(nullable = false)
private Boolean isDeleted;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private ReservationStatus status;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private RegisterChartStatus registerStatus;
public void setMember(Member member) {
this.member = member;
}
public void setAdmin(Admin admin) {
this.admin = admin;
}
public void markAsDeleted(Boolean b) {
this.isDeleted = b;
}
public void setStatus(ReservationStatus status) {
this.status = status;
}
public void setRegisterStatus(RegisterChartStatus registerStatus) {
this.registerStatus = registerStatus;
}
public String getFormattedDate() {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
return date.format(formatter);
}
public RegisterChartDto toResponse() {
return RegisterChartDto.builder()
.adminId(this.admin.getId())
.memberId(this.member.getId())
.subjectId(this.admin.getSubject().getId())
.subjectName(this.admin.getSubject().getName())
.hospitalId(this.admin.getHospital().getId())
.hospitalName(this.admin.getHospital().getName())
.memberUsername(this.member.getUsername())
.time(this.getModifyDate())
.status(this.registerStatus)
.build();
}
}
A. 예약 조회
/members/subjects/{subject-id}/hospitals/{hospital-id}/reservations 엔드포인트에 GET 요청을 보냅니다.ReservationController의 showReservation 메서드가 호출됩니다.reservationService를 사용하여 과목과 병원에 대한 예약 정보를 조회합니다.ModelAndView 객체에 추가되고, 클라이언트에게 "members/reservations" 페이지로 전달됩니다.B. 예약 생성
/members/subjects/{subject-id}/hospitals/{hospital-id}/reservations 엔드포인트에 POST 요청을 보냅니다.ReservationController의 createReservation 메서드가 호출됩니다.C. 예약 삭제
/members/subjects/{subject-id}/hospitals/{hospital-id}/reservations/{reservation-id} 엔드포인트에 DELETE 요청을 보냅니다.ReservationController의 deleteReservation 메서드가 호출됩니다.reservationService를 사용하여 예약을 삭제하고, 삭제된 예약 정보는 복구할 수 없습니다./members 페이지로 리디렉션됩니다.D. 예약 확인
/members/subjects/{subject-id}/hospitals/{hospital-id}/reservations/{reservation-id}/confirm 엔드포인트에 GET 요청을 보냅니다.ReservationController의 confirmReservation 메서드가 호출됩니다.E. WebSocket을 통한 실시간 업데이트
ReservationDto 객체를 생성하고 WebSocket 메시지를 통해 클라이언트에게 전송합니다.A. 예약 조회 및 생성
- 사용자는 예약을 조회하거나 새로운 예약을 생성할 수 있습니다.
findById(Long id): 특정 예약 ID로 예약을 조회합니다. 해당 ID에 해당하는 예약이 없으면 예외를 발생시킵니다.findByMemberIdAndIsDeletedFalse(Long memberId): 특정 회원 ID로 삭제되지 않은 모든 예약을 조회합니다.findByAdminIdAndMemberId(Long adminId, Long memberId): 특정 관리자 ID와 회원 ID로 해당 관리자와 회원 간의 예약을 조회합니다.getTodayReservation(Admin admin): 오늘 날짜의 예약 목록을 조회합니다. 이 때, 관리자 정보를 매개변수로 받아 해당 관리자의 오늘 예약 목록을 가져옵니다.findReservationByAdminIdAndMemberIdAndIsDeletedFalse(Long adminId, Long memberId): 특정 관리자 ID와 회원 ID로 삭제되지 않은 해당 관리자와 회원 간의 예약을 조회합니다.createReservation(Long hospitalId, Long subjectId, String selectedDate, String selectedTime): 특정 병원 ID, 진료 과목 ID, 선택된 날짜 및 시간으로 새로운 예약을 생성합니다.
B. 예약 업데이트 및 처리
예약의 상태를 업데이트하거나 처리할 수 있습니다.
updateStatus(Reservation reservation, ReservationStatus status): 주어진 예약에 대해 예약 상태를 업데이트합니다.
updateStatusByAdminAndMember(Admin admin, Long memberId, RegisterChartStatus status): 관리자와 회원 ID, 예약 상태를 기반으로 예약의 상태를 업데이트합니다. 상태가 취소 또는 완료인 경우 삭제 플래그를 설정합니다.
deleteReservation(Long id): 특정 예약 ID로 예약 정보를 삭제합니다.
예약 상태 관련 비지니스 로직을 좀 더 자세히 살펴보면 아래와 같습니다.
// 예약 상태를 업데이트하는 메서드
@Override
@Transactional
public RsData<Reservation> updateStatus(Reservation reservation, ReservationStatus status) {
// 주어진 예약의 상태를 새로운 상태로 업데이트
reservation.setStatus(status);
// 예약이 완료된 경우
if (status.equals(ReservationStatus.COMPLETE)) {
// 해당 시간대의 예약을 삭제
reservation.markAsDeleted(true);
}
// 예약이 취소된 경우
else if (status.equals(ReservationStatus.CANCELLED)) {
// 해당 시간대의 예약을 삭제
reservation.markAsDeleted(true);
}
// 업데이트된 예약을 저장하고 반환
return saveAndReturnRsData(reservation, "예약 상태가 업데이트 되었습니다.");
}// 회원 및 관리자 간 예약 상태 업데이트 메서드
@Override
@Transactional
public RsData<Reservation> updateStatusByAdminAndMember(Admin admin, Long memberId, ReservationStatus status) {
// 관리자와 회원 ID로 특정 예약을 찾습니다.
Reservation reservation = reservationRepository.findReservationByAdminIdAndMemberIdAndIsDeletedFalse(admin.getId(), memberId)
.orElseThrow(() -> new IllegalArgumentException("예약 정보를 찾을 수 없습니다."));
// 예약이 진행되었다면 상태를 "완료"로 설정
if (status.equals(ReservationStatus.COMPLETE)) {
reservation.setStatus(ReservationStatus.COMPLETE);
// 진료가 완료되면 삭제될 수 있도록 마킹
reservation.markAsDeleted(true);
}
// 진행되지 않았다면 상태를 "취소"로 설정
else if (status.equals(ReservationStatus.CANCELLED)) {
reservation.setStatus(ReservationStatus.CANCELLED);
// 해당 시간대의 예약을 삭제
reservation.markAsDeleted(true);
}
// 업데이트된 예약을 저장하고 반환
return saveAndReturnRsData(reservation, "진료 상태가 업데이트 되었습니다.");
}// 예약 삭제 메서드
@Override
@Transactional
public RsData<String> deleteReservation(Long id) {
// 주어진 ID로 예약을 찾습니다.
Reservation reservation = reservationRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("예약 정보를 찾을 수 없습니다."));
// 예약을 논리적으로 삭제합니다.
reservation.markAsDeleted(true);
// 예약 상태를 "취소"로 설정합니다.
reservation.setStatus(ReservationStatus.CANCELLED);
// 등록 차트 상태도 "취소"로 설정합니다.
reservation.setRegisterStatus(RegisterChartStatus.CANCELLED);
// 업데이트된 예약을 저장하고 성공 메시지를 반환합니다.
reservationRepository.save(reservation);
return RsData.of("S-2", "예약 정보가 삭제되었습니다.");
}C. 예약 현황(완료된 예약 목록 및 상세 정보)
완료된 예약 목록과 상세 정보를 가져올 수 있습니다.
getReservationsByMemberIdAndRegisterStatus(Long memberId): 특정 회원 ID로 완료된 진료 목록을 조회합니다.
getReservationDetails(Long reservationId): 특정 예약 ID로 예약 상세 정보를 조회합니다.
getReservationHistoryByMemberId(Long memberId): 특정 회원 ID로 예약 기록을 조회합니다.
getReservationDetailsByAdminAndMember(Admin admin, Long memberId): 특정 관리자와 회원 ID로 해당 관리자와 회원 간의 예약 상세 정보를 조회합니다.
D. 유틸리티 메서드
데이터 유효성 검사, 시간 계산, 정렬 등의 기능을 수행하는 유틸리티 메서드가 구현되어 있습니다.
parseDateTime(String selectedDate, String selectedTime): 선택된 날짜와 시간을 파싱하여 LocalDateTime 객체를 반환합니다.
validateReservation(LocalDateTime dateTime, Long adminId): 예약을 유효성 검사합니다. 중복된 시간이나 이미 예약된 시간인지 확인합니다.
checkReservationExists(Long adminId, Long memberId): 예약이 이미 존재하는지 확인합니다.
checkTimeIsBooked(LocalDateTime dateTime, Long adminId): 특정 시간이 이미 예약되었는지 확인합니다.
@Service
@RequiredArgsConstructor
public class ReservationServiceImpl implements ReservationService {
private final ReservationRepository reservationRepository;
private final AdminService adminService;
private final MemberService memberService;
// 현재 사용자 정보를 가져오는 메서드
private Member getCurrentUser() {
return memberService.getCurrentUser().orElseThrow(() -> new RuntimeException("현재 로그인한 사용자 정보를 가져오지 못했습니다."));
}
// 예약 ID로 예약 정보를 찾는 메서드
@Override
@Transactional(readOnly = true)
public Reservation findById(Long id) {
return reservationRepository.findById(id).orElseThrow(() -> new RuntimeException("해당 예약을 찾을 수 없습니다."));
}
// 회원 ID로 삭제되지 않은 예약 목록을 조회하는 메서드
@Override
@Transactional(readOnly = true)
public List<Reservation> **findByMemberIdAndIsDeletedFalse**(Long memberId) {
return reservationRepository.findByMemberIdAndIsDeletedFalse(memberId);
}
// 관리자 ID와 회원 ID로 예약을 조회하는 메서드
@Override
@Transactional(readOnly = true)
public Optional<Reservation> **findByAdminIdAndMemberId**(Long adminId, Long memberId){
return reservationRepository.findByAdminIdAndMemberIdAndIsDeletedFalse(adminId, memberId);
}
// 오늘 날짜의 예약 목록을 조회하는 메서드 (당일 예약)
// 관리자 정보를 매개변수로 받아 해당 관리자의 오늘 예약 목록을 가져옵니다.
@Override
@Transactional(readOnly = true)
public List<Reservation> **getTodayReservation**(Admin admin){
return reservationRepository.getTodayReservation(admin);
}
// 관리자 ID와 회원 ID로 삭제되지 않은 예약을 조회하는 메서드
@Override
@Transactional(readOnly = true)
public Optional<Reservation> **findReservationByAdminIdAndMemberIdAndIsDeletedFalse**(Long adminId, Long memberId){
return reservationRepository.findReservationByAdminIdAndMemberIdAndIsDeletedFalse(adminId, memberId);
}
// 예약 생성 메서드
// 주어진 병원 ID, 진료 과목 ID, 선택된 날짜 및 시간으로 새로운 예약을 생성합니다.
// 이때, 현재 사용자 정보를 가져와 예약을 생성합니다. 중복된 시간이나 이미 예약된 시간인지 확인한 후 예약을 저장합니다.
@Override
@Transactional
public Reservation **createReservation**(Long hospitalId, Long subjectId, String selectedDate, String selectedTime) {
Member currentUser = getCurrentUser();
Admin admin = getAdmin(hospitalId, subjectId);
LocalDateTime dateTime = parseDateTime(selectedDate, selectedTime);
validateReservation(dateTime, admin.getId());
return saveReservation(dateTime, admin, currentUser);
}
// 병원 ID와 진료 과목 ID로 관리자를 조회하는 메서드
private Admin getAdmin(Long hospitalId, Long subjectId) {
return adminService.findByHospitalIdAndSubjectId(hospitalId, subjectId)
.orElseThrow(() -> new RuntimeException("해당 병원과 진료 과목에 해당하는 관리자를 찾을 수 없습니다."));
}
// 문자열 형태의 날짜와 시간을 LocalDateTime으로 파싱하는 메서드
private LocalDateTime parseDateTime(String selectedDate, String selectedTime) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
return LocalDateTime.parse(selectedDate + " " + selectedTime, formatter);
}
// 예약 가능 여부를 확인하는 메서드
private void validateReservation(LocalDateTime dateTime, Long adminId) {
RsData<String> checkResult = checkDuplicateTime(dateTime, adminId);
if (!checkResult.isSuccess()) {
throw new RuntimeException(checkResult.getMsg());
}
}
// 이미 예약된 시간인지 확인하는 메서드
private RsData<String> checkDuplicateTime(LocalDateTime dateTime, Long adminId) {
boolean isTimeBooked = reservationRepository.existsByDateAndAdminIdAndIsDeletedFalse(dateTime, adminId);
if (isTimeBooked) {
return RsData.of("F-5", "이미 예약된 시간대입니다.");
}
return RsData.of("S-3", "예약 가능한 시간대입니다.");
}
// 예약 생성 후 RsData로 반환하는 메서드
@Override
@Transactional
public RsData<Reservation> insert(Long hospitalId, Long subjectId, String selectedDate, String selectedTime) {
Reservation reservation = createReservation(hospitalId, subjectId, selectedDate, selectedTime);
return RsData.of("S-1", "예약이 성공적으로 등록되었습니다.", reservation);
}
// 예약을 생성하고, 성공 시 리디렉션 URL을 생성하는 메서드
@Override
@Transactional
public String createReservationWithCheckAndReturnRedirectUrl(Long hospitalId, Long subjectId, String selectedDate, String selectedTime) {
Member currentUser = getCurrentUser();
Admin admin = getAdmin(hospitalId, subjectId);
LocalDateTime dateTime = parseDateTime(selectedDate, selectedTime);
checkReservationExists(admin.getId(), currentUser.getId());
checkTimeIsBooked(dateTime, admin.getId());
Reservation reservation = createReservation(hospitalId, subjectId, selectedDate, selectedTime);
return generateRedirectUrl(subjectId, hospitalId, reservation.getId());
}
// 이미 예약된 병원인지 확인하는 메서드
private void **checkReservationExists**(Long adminId, Long memberId) {
RsData<String> reservationStatus = checkDuplicateReservation(adminId, memberId);
if (!reservationStatus.isSuccess()) {
throw new ReservationNotFoundException(reservationStatus.getMsg());
}
}
// 이미 예약된 시간인지 확인하는 메서드
private void **checkTimeIsBooked**(LocalDateTime dateTime, Long adminId) {
RsData<String> duplicateTimeStatus = checkDuplicateTime(dateTime, adminId);
if (!duplicateTimeStatus.isSuccess()) {
throw new ReservationNotFoundException(duplicateTimeStatus.getMsg());
}
}
// 이미 예약된 병원인지 확인하는 메서드
@Override
@Transactional(readOnly = true)
public RsData<String> **checkDuplicateReservation**(Long adminId, Long memberId) {
boolean isDuplicateActiveReservation = reservationRepository.existsByAdminIdAndMemberIdAndIsDeletedFalse(adminId, memberId);
if (isDuplicateActiveReservation) {
return RsData.of("F-4", "이미 예약한 병원입니다.");
}
return RsData.of("S-3", "예약 가능한 병원입니다.");
}
// 예약 정보 삭제 메서드
@Override
@Transactional
public RsData<String> **deleteReservation**(Long id) {
Reservation reservation = reservationRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("예약 정보를 찾을 수 없습니다."));
reservation.markAsDeleted(true);
reservation.setStatus(ReservationStatus.CANCELLED);
reservation.setRegisterStatus(RegisterChartStatus.CANCELLED);
reservationRepository.save(reservation);
return RsData.of("S-2", "예약 정보가 삭제되었습니다.");
}
// 예약 상태 업데이트 메서드
@Override
@Transactional
public RsData<Reservation> **updateStatus**(Reservation reservation, ReservationStatus status){
reservation.setStatus(status);
return saveAndReturnRsData(reservation, "예약 상태가 업데이트 되었습니다.");
}
// 관리자와 회원에 의한 진료 상태 업데이트 메서드
// 진료가 완료되거나 취소될 경우 해당 예약을 삭제합니다.
@Override
@Transactional
public RsData<Reservation> **updateStatusByAdminAndMember**(Admin admin, Long memberId, RegisterChartStatus status) {
Reservation reservation = reservationRepository.findReservationByAdminIdAndMemberIdAndIsDeletedFalse(admin.getId(), memberId)
.orElseThrow(() -> new IllegalArgumentException("예약 정보를 찾을 수 없습니다."));
if(status.equals(RegisterChartStatus.ENTER)){
reservation.setRegisterStatus(RegisterChartStatus.ENTER);
} else if(status.equals(RegisterChartStatus.COMPLETE)){
reservation.setRegisterStatus(RegisterChartStatus.COMPLETE);
reservation.markAsDeleted(true);
} else if(status.equals(RegisterChartStatus.CANCELLED)){
reservation.setRegisterStatus(RegisterChartStatus.CANCELLED);
reservation.markAsDeleted(true);
}
return saveAndReturnRsData(reservation, "진료 상태가 업데이트 되었습니다.");
}
// 리디렉션 URL 생성 메서드
private String generateRedirectUrl(Long subjectId, Long hospitalId, Long reservationId) {
return "/members/subjects/" + subjectId + "/hospitals/" + hospitalId + "/reservations/" + reservationId;
}
// 예약 정보를 저장하고 RsData로 반환하는 메서드
private Reservation saveReservation(LocalDateTime dateTime, Admin admin, Member member) {
Reservation reservation = Reservation.builder()
.date(dateTime)
.status(ReservationStatus.PENDING)
.registerStatus(RegisterChartStatus.WAITING)
.admin(admin)
.member(member)
.isDeleted(false)
.build();
return reservationRepository.save(reservation);
}
// 예약 정보를 저장하고 RsData로 반환하는 메서드
private RsData<Reservation> saveAndReturnRsData(Reservation reservation, String msg){
reservationRepository.save(reservation);
return RsData.of("S-1", msg, reservation);
}
// 회원 ID로 완료된 진료 목록을 조회하는 메서드
@Transactional(readOnly = true)
public List<RegisterChartDto> getReservationsByMemberIdAndRegisterStatus(Long memberId){
List<Reservation> reservations = reservationRepository.findByMemberIdAndRegisterStatus(memberId, RegisterChartStatus.COMPLETE);
return reservations.stream().map(Reservation::toResponse).collect(Collectors.toList());
}
}