회원 예약 조회 및 취소
회원은 자신의 예약을 조회할 수 있습니다.
자신의 예약을 취소할 수 있습니다.
‘예약조회’를 누르면, 예약 조회 페이지로 이동하고,
자신의 모든 예약을 볼 수 있습니다.
각 예약에는 ‘예약 취소’ 버튼이 있어 이것을 누르면 예약이 취소 됩니다.
- 프론트엔드 -
UserReservations.vue
<!-- src/views/UserReservations.vue -->
<template>
<v-container>
<h2 class="text-h5 mb-4">내 예약 목록</h2>
<v-card
v-for="reservation in reservations"
:key="reservation.id"
class="mb-4"
>
<v-card-title>
예약 ID: {{ reservation.id }} - {{ reservation.date }} - 상태: {{ statusText(reservation.status) }}
</v-card-title>
<v-card-text class="memo-text">
<b>메모</b>: {{ reservation.memo }}
<br />
<b>시간 슬롯: </b>
<ul>
<li v-for="slot in reservation.timeSlots" :key="slot.id">{{ slot.timeSlot }}</li>
</ul>
</v-card-text>
<v-card-actions>
<v-btn
color="red"
@click="cancelReservation(reservation.id)"
:disabled="reservation.status !== 'PENDING'"
>
예약 취소
</v-btn>
</v-card-actions>
</v-card>
</v-container>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useReservationStore } from '@/stores/reservationStore'
const reservationStore = useReservationStore()
const reservations = ref([])
const statusText = (status) => {
const map = {
PENDING: '대기 중',
APPROVED: '승인됨',
REJECTED: '거부됨',
CANCELLED: '취소됨'
}
return map[status] || '알 수 없음'
}
const fetchMyReservations = async () => {
try {
const res = await reservationStore.fetchMyReservations() // 사용자 본인 예약 목록 가져오기
reservations.value = res
} catch (err) {
console.error('내 예약 목록 조회 실패', err)
}
}
onMounted(() => {
fetchMyReservations()
})
const cancelReservation = async (reservationId) => {
try {
await reservationStore.cancelReservation(reservationId)
await fetchMyReservations()
} catch (err) {
console.error('예약 취소 실패', err)
alert('예약 취소 중 오류가 발생했습니다.')
}
}
</script>
<style scoped>
.memo-text {
white-space: pre-wrap; /* 줄바꿈과 공백을 유지 */
word-break: break-word; /* 단어가 너무 길면 줄바꿈 */
}
</style>
stores - reservationStore.js
// src/stores/reservationStore.js
import { defineStore } from 'pinia'
import axios from 'axios'
const API_URL = 'http://localhost:8080/api/auth'
const API_ADMIN_URL = 'http://localhost:8080/api/admin'
export const useReservationStore = defineStore('reservation', () => {
const getToken = () => localStorage.getItem('token')
// 예약 제출
const submitReservation = async (formData) => {
const token = getToken()
try {
const response = await axios.post(
`${API_URL}/reservation`,
formData,
{
headers: { Authorization: `Bearer ${token}` },
}
)
return response.data
} catch (err) {
throw err
}
}
// 관리자 - 예약 전체 조회
const fetchReservations = async (date) => {
try {
const token = getToken()
if (!token) throw new Error('인증 토큰이 없습니다.')
const params = date ? { date } : {}
const response = await axios.get(`${API_ADMIN_URL}/reservations`, {
headers: { Authorization: `Bearer ${token}` },
params,
})
return response.data
} catch (err) {
console.error('예약 목록 조회 실패:', err)
throw err
}
}
// 관리자 - 상태 변경
const updateStatus = async (id, status) => {
try {
const token = getToken()
const endpoint =
status === 'APPROVED'
? `${API_ADMIN_URL}/reservation/${id}/approve`
: `${API_ADMIN_URL}/reservation/${id}/reject`
await axios.post(endpoint, null, {
headers: { Authorization: `Bearer ${token}` },
})
await fetchReservations()
} catch (err) {
console.error('상태 변경 실패', err)
}
}
// 사용자 - 내 예약 조회
const fetchMyReservations = async () => {
const token = getToken()
try {
const response = await axios.get(`${API_URL}/reservations/my`, {
headers: { Authorization: `Bearer ${token}` },
})
return response.data
} catch (err) {
console.error('내 예약 조회 실패:', err)
throw err
}
}
// 사용자 - 예약 취소
const cancelReservation = async (reservationId) => {
const token = getToken()
try {
await axios.patch(
`${API_URL}/reservations/${reservationId}/cancel`,
null,
{
headers: { Authorization: `Bearer ${token}` },
}
)
} catch (err) {
console.error('예약 취소 실패:', err)
throw err
}
}
return {
submitReservation,
fetchReservations,
updateStatus,
fetchMyReservations,
cancelReservation,
}
})
- 백엔드 -
SecurityConfig.java
package com.example.reservation.config;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.*;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.*;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.*;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import java.util.List;
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {
private static final Logger logger = LoggerFactory.getLogger(JwtFilter.class);
private final JwtFilter jwtFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.cors(cors -> cors.configurationSource(request -> {
CorsConfiguration corsConfig = new CorsConfiguration();
corsConfig.setAllowedOrigins(List.of("http://localhost:5173"));
corsConfig.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
corsConfig.setAllowedHeaders(List.of("Authorization", "Content-Type"));
corsConfig.setAllowCredentials(true);
return corsConfig;
}))
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() // OPTIONS 요청 허용
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
);
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config
) throws Exception {
return config.getAuthenticationManager();
}
}
ReservationService.java
package com.example.reservation.service;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.stereotype.Service;
import com.example.reservation.dto.ReservationRequest;
import com.example.reservation.dto.ReservationResponse;
import com.example.reservation.model.Reservation;
import com.example.reservation.model.ReservationStatus;
import com.example.reservation.model.ReservationTimeSlot;
import com.example.reservation.model.User;
import com.example.reservation.repository.ReservationRepository;
import com.example.reservation.repository.UserRepository;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class ReservationService {
private final ReservationRepository reservationRepository;
private final UserRepository userRepository;
/**
* 사용자: 예약 생성
*/
@Transactional
public ReservationResponse createReservation(String email, ReservationRequest request) {
User user = userRepository.findByEmail(email).orElseThrow();
Reservation reservation = new Reservation();
reservation.setDate(LocalDate.parse(request.getDate()));
reservation.setUser(user);
reservation.setMemo(request.getMemo());
reservation.setStatus(ReservationStatus.PENDING);
reservation.setCreatedAt(LocalDateTime.now());
List<ReservationTimeSlot> slotEntities = request.getTimeSlots().stream()
.map(ts -> {
ReservationTimeSlot slot = new ReservationTimeSlot();
slot.setTimeSlot(ts);
slot.setReservation(reservation); // 필수
return slot;
}).collect(Collectors.toList());
reservation.setTimeSlots(slotEntities);
Reservation saved = reservationRepository.save(reservation);
return new ReservationResponse(
saved.getId(),
saved.getDate().toString(),
saved.getTimeSlots().stream().map(ReservationTimeSlot::getTimeSlot).toList(),
saved.getMemo(),
saved.getStatus().name(),
saved.getCreatedAt().toString()
);
}
/**
* 관리자: 예약 상태 변경 (APPROVED, REJECTED, CANCELLED 등)
*/
@Transactional
public Reservation updateReservationStatus(Long reservationId, ReservationStatus status) {
Reservation reservation = reservationRepository.findById(reservationId)
.orElseThrow(() -> new RuntimeException("Reservation not found"));
reservation.setStatus(status);
return reservationRepository.save(reservation);
}
/**
* 관리자: 전체 예약 목록 조회
*/
public List<Reservation> getAllReservations() {
return reservationRepository.findAll();
}
public List<Reservation> getReservationsByDate(String date) {
// date 형식에 맞게 파싱 후 DB 쿼리 (예: '2025-05-27')
LocalDate localDate = LocalDate.parse(date); // 문자열 → LocalDate 변환
return reservationRepository.findByDate(localDate);
}
/**
* 사용자: 내 예약 목록 조회
*/
public List<Reservation> getReservationsByUser(String email) {
User user = userRepository.findByEmail(email).orElseThrow();
return reservationRepository.findByUser(user);
}
public void cancelReservation(Long reservationId, String email) {
Reservation reservation = reservationRepository.findById(reservationId)
.orElseThrow(() -> new RuntimeException("예약을 찾을 수 없습니다."));
// 클라이언트에서 로그인 후 예약을 취소하므로 본인의 예약만 취소를 한다.
// 승인 또는 거부가 된 예약도 취소할 수 있다.
// if (!reservation.getEmail().equals(email)) {
// throw new SecurityException("본인의 예약만 취소할 수 있습니다.");
// }
// if (reservation.getStatus() != ReservationStatus.PENDING) {
// throw new IllegalStateException("대기 중인 예약만 취소할 수 있습니다.");
// }
reservation.setStatus(ReservationStatus.CANCELLED);
reservationRepository.save(reservation);
}
}
ReservationController.java
package com.example.reservation.controller;
import com.example.reservation.dto.ReservationRequest;
import com.example.reservation.dto.ReservationResponse;
import com.example.reservation.model.Reservation;
import com.example.reservation.model.ReservationStatus;
import com.example.reservation.service.ReservationService;
import com.example.reservation.util.JwtUtil;
import lombok.RequiredArgsConstructor;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/")
@CrossOrigin(origins = "http://localhost:5173")
@RequiredArgsConstructor
public class ReservationController {
private static final Logger logger = LoggerFactory.getLogger(ReservationController.class);
private final ReservationService reservationService;
private final JwtUtil jwtUtil;
@PostMapping("auth/reservation")
public ReservationResponse create(
@RequestBody ReservationRequest request,
@RequestHeader("Authorization") String authHeader
) {
String token = authHeader.replace("Bearer ", "");
String email = jwtUtil.extractUsername(token);
return reservationService.createReservation(email, request);
}
/**
* ✅ 사용자: 자신의 예약 목록 조회
*/
@GetMapping("auth/reservations/my")
@PreAuthorize("hasRole('USER')")
public List<Reservation> getMyReservations(
@RequestHeader("Authorization") String authHeader
) {
String token = authHeader.replace("Bearer ", "");
String email = jwtUtil.extractUsername(token);
return reservationService.getReservationsByUser(email);
}
/**
* ✅ 사용자: 자신의 예약 취소
*/
@PatchMapping("auth/reservations/{id}/cancel")
@PreAuthorize("hasRole('USER')")
public ResponseEntity<?> cancelReservation(
@PathVariable Long id,
@RequestHeader("Authorization") String authHeader
) {
String token = authHeader.replace("Bearer ", "");
String email = jwtUtil.extractUsername(token);
reservationService.cancelReservation(id, email);
return ResponseEntity.ok("예약이 취소되었습니다.");
}
/**
* ✅ 관리자: 전체 예약 목록 조회
*/
@GetMapping("admin/reservations")
@PreAuthorize("hasRole('ADMIN')")
public List<Reservation> getAllReservations(
@RequestParam(required = false) String date
) {
if (date != null && !date.isEmpty()) {
return reservationService.getReservationsByDate(date);
}
return reservationService.getAllReservations();
}
@PreAuthorize("hasRole('ADMIN')")
@PostMapping("admin/reservation/{id}/approve")
public ResponseEntity<?> approveReservation(@PathVariable Long id) {
logger.info("==== approveReservation : " + id);
reservationService.updateReservationStatus(id, ReservationStatus.APPROVED);
return ResponseEntity.ok("Reservation approved");
}
@PreAuthorize("hasRole('ADMIN')")
@PostMapping("admin/reservation/{id}/reject")
public ResponseEntity<?> rejectReservation(@PathVariable Long id) {
logger.info("==== rejectReservation : " + id);
reservationService.updateReservationStatus(id, ReservationStatus.REJECTED);
return ResponseEntity.ok("Reservation rejected");
}
}
'예약 관리 시스템 - Spring + Vue' 카테고리의 다른 글
예약 관리 시스템 완성 - 프론트엔드(Vue 3 + Vuetify) (0) | 2025.05.29 |
---|---|
예약 관리 시스템 완성 - Spring Boot 3 + Vue 3 (0) | 2025.05.29 |
예약 관리 시스템 (Spring Boot 3 + Vue 3) - 예약 관리(관리자) (1) | 2025.05.27 |
예약 관리 시스템 (Spring Boot 3 + Vue 3) - 예약 하기 (백엔드) (0) | 2025.05.26 |
예약 관리 시스템 (Spring Boot 3 + Vue 3) - 예약 하기 (프론트엔드) (0) | 2025.05.26 |