Skip to content

Commit ecb3feb

Browse files
authored
[EC-53] BE/refactor 회의실 예약 생성 api 리팩토링 및 예외처리 (#43)
* [EC-53] chore: WIP * [EC-53] feat: 캠퍼스내 회의실 일치여부 예외 처리 * [EC-53] feat: 예외 처리
1 parent a4eb54a commit ecb3feb

File tree

9 files changed

+65
-113
lines changed

9 files changed

+65
-113
lines changed
Lines changed: 17 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,25 @@
11
package org.example.educheck.domain.meetingroomreservation.service;
22

33
import lombok.RequiredArgsConstructor;
4-
import lombok.extern.slf4j.Slf4j;
54
import org.example.educheck.domain.meetingroom.entity.MeetingRoom;
65
import org.example.educheck.domain.meetingroom.repository.MeetingRoomRepository;
7-
import org.example.educheck.domain.meetingroomreservation.TimeSlot;
86
import org.example.educheck.domain.meetingroomreservation.dto.request.MeetingRoomReservationRequestDto;
97
import org.example.educheck.domain.meetingroomreservation.entity.MeetingRoomReservation;
108
import org.example.educheck.domain.meetingroomreservation.repository.MeetingRoomReservationRepository;
119
import org.example.educheck.domain.member.entity.Member;
1210
import org.example.educheck.domain.member.repository.MemberRepository;
11+
import org.example.educheck.global.common.exception.custom.common.ResourceMismatchException;
1312
import org.example.educheck.global.common.exception.custom.common.ResourceNotFoundException;
1413
import org.example.educheck.global.common.exception.custom.reservation.ReservationConflictException;
15-
import org.springframework.data.redis.core.RedisTemplate;
1614
import org.springframework.security.core.userdetails.UserDetails;
1715
import org.springframework.stereotype.Service;
1816
import org.springframework.transaction.annotation.Transactional;
1917

2018
import java.time.LocalDate;
2119
import java.time.LocalDateTime;
2220
import java.time.LocalTime;
23-
import java.time.format.DateTimeFormatter;
2421
import java.time.temporal.ChronoUnit;
25-
import java.util.List;
2622

27-
@Slf4j
2823
@Service
2924
@Transactional(readOnly = true)
3025
@RequiredArgsConstructor
@@ -33,12 +28,11 @@ public class MeetingRoomReservationService {
3328
private final MeetingRoomReservationRepository meetingRoomReservationRepository;
3429
private final MemberRepository memberRepository;
3530
private final MeetingRoomRepository meetingRoomRepository;
36-
private final RedisTemplate<String, Object> redisTemplate;
3731

3832
@Transactional
3933
public void createReservation(UserDetails user, Long campusId, MeetingRoomReservationRequestDto requestDto) {
4034

41-
Member findMember = memberRepository.findByEmail(user.getUsername()).orElseThrow(() -> new ResourceNotFoundException("존재하지 않는 사용자입니다."));
35+
Member findMember = memberRepository.findByEmail(user.getUsername()).orElseThrow(() -> new IllegalArgumentException("존재하지 않는 member입니다."));
4236

4337
MeetingRoom meetingRoom = meetingRoomRepository.findById(requestDto.getMeetingRoomId())
4438
.orElseThrow(() -> new ResourceNotFoundException("해당 회의실이 존재하지 않습니다."));
@@ -47,45 +41,13 @@ public void createReservation(UserDetails user, Long campusId, MeetingRoomReserv
4741

4842
validateReservationTime(requestDto.getStartTime(), requestDto.getEndTime());
4943

50-
TimeSlot timeSlot = TimeSlot.from(requestDto);
5144

52-
//신버전
53-
if (!isAvailable(meetingRoom.getId(), timeSlot)) {
54-
throw new ReservationConflictException();
55-
}
45+
validateReservableTime(meetingRoom, requestDto.getStartTime(), requestDto.getEndTime());
5646

57-
//RDB에 예약 -> Redis 슬롯 처리
5847
MeetingRoomReservation meetingRoomReservation = requestDto.toEntity(findMember, meetingRoom);
5948
meetingRoomReservationRepository.save(meetingRoomReservation);
60-
61-
updateRedisSlots(meetingRoom.getId(), timeSlot, true);
62-
63-
log.info("createReservation 메서드, 예약 성공");
64-
}
65-
66-
private void updateRedisSlots(Long roomId, TimeSlot timeSlot, boolean isReserved) {
67-
68-
String redisKey = generateSlotKey(roomId, timeSlot.getDate());
69-
70-
Boolean[] slots = (Boolean[]) redisTemplate.opsForValue().get(redisKey);
71-
72-
if (slots == null) {
73-
initDailyReservationsSlots(timeSlot.getDate());
74-
slots = (Boolean[]) redisTemplate.opsForValue().get(redisKey);
75-
}
76-
77-
int startSlotIndex = calculateSlotIndex(timeSlot.getStartTime());
78-
int endSlotIndex = calculateSlotIndex(timeSlot.getEndTime());
79-
80-
for (int i = startSlotIndex; i <= endSlotIndex; i++) {
81-
slots[i] = isReserved;
82-
}
83-
84-
redisTemplate.opsForValue().set(redisKey, slots);
85-
8649
}
8750

88-
8951
/**
9052
* 예약은 9시부터 22시까지 가능
9153
*/
@@ -96,84 +58,38 @@ private void validateReservationTime(LocalDateTime startTime, LocalDateTime endT
9658

9759

9860
if (endTime.isBefore(startTime)) {
99-
throw new IllegalArgumentException("시작 시간이 종료 시간보다 늦을 수 없습니다.");
61+
throw new ReservationConflictException("시작 시간이 종료 시간보다 늦을 수 없습니다.");
10062
}
10163

10264
if (startTime.isAfter(endTime)) {
103-
throw new IllegalArgumentException("종료 시간이 시작 시간보다 빠를 수 없습니다.");
65+
throw new ReservationConflictException("종료 시간이 시작 시간보다 빠를 수 없습니다.");
10466
}
10567

10668
if (ChronoUnit.MINUTES.between(startTime, endTime) < 15) {
107-
throw new IllegalArgumentException("최소 예약 시간은 15분입니다.");
69+
throw new ReservationConflictException("최소 예약 시간은 15분입니다.");
10870
}
10971

11072
if (startTime.toLocalTime().isBefore(startOfDay) || endTime.toLocalTime().isAfter(endOfDay)) {
111-
throw new IllegalArgumentException("예약 가능 시간은 오전 9시부터 오후 10시까지입니다.");
73+
throw new ReservationConflictException("예약 가능 시간은 오전 9시부터 오후 10시까지입니다.");
11274
}
11375

11476
}
11577

78+
private void validateReservableTime(MeetingRoom meetingRoom, LocalDateTime startTime, LocalDateTime endTime) {
79+
LocalDate date = startTime.toLocalDate();
80+
boolean result = meetingRoomReservationRepository.existsOverlappingReservation(meetingRoom,
81+
date, startTime, endTime);
11682

117-
//TODO: 쿼리 발생하는거 확인 후, FETCH JOIN 처리 등 고려 하기
118-
private void validateUserCampusMatchMeetingRoom(Long campusId, MeetingRoom meetingRoom) {
119-
120-
if (!campusId.equals(meetingRoom.getCampusId())) {
121-
throw new IllegalArgumentException("해당 회의실은 캠퍼스내 회의실이 아닙니다.");
122-
}
123-
}
124-
125-
private String generateSlotKey(Long roomId, LocalDate date) {
126-
return String.format("mettingRoom:%d:date:%s", roomId, date.format(DateTimeFormatter.ofPattern("yyyyMMdd")));
127-
}
128-
129-
private int calculateSlots(int startHour, int endHour, int slotDurationMinutes) {
130-
int totalMinutes = (endHour - startHour) * 60;
131-
return totalMinutes / slotDurationMinutes;
132-
133-
}
134-
135-
private void initDailyReservationsSlots(LocalDate date) {
136-
List<MeetingRoom> roomList = meetingRoomRepository.findAll();
137-
138-
for (MeetingRoom room : roomList) {
139-
String redisKey = generateSlotKey(room.getId(), date);
140-
141-
int slotsCount = calculateSlots(9, 22, 15);
142-
Boolean[] slots = new Boolean[slotsCount];
143-
for (int i = 0; i < slotsCount; i++) {
144-
slots[i] = false;
145-
}
146-
147-
redisTemplate.opsForValue().set(redisKey, slots);
83+
if (result) {
84+
throw new ReservationConflictException();
14885
}
14986
}
15087

151-
public boolean isAvailable(Long meetingRoomId, TimeSlot timeSlot) {
152-
String redisKey = generateSlotKey(meetingRoomId, timeSlot.getDate());
153-
Boolean[] slots = (Boolean[]) redisTemplate.opsForValue().get(redisKey);
154-
155-
if (slots == null) {
156-
initDailyReservationsSlots(timeSlot.getDate());
157-
slots = (Boolean[]) redisTemplate.opsForValue().get(redisKey);
158-
}
159-
160-
int startSlotIndex = calculateSlotIndex(timeSlot.getStartTime());
161-
int endSlotIndex = calculateSlotIndex(timeSlot.getStartTime());
88+
//TODO: 쿼리 발생하는거 확인 후, FETCH JOIN 처리 등 고려 하기
89+
private void validateUserCampusMatchMeetingRoom(Long campusId, MeetingRoom meetingRoom) {
16290

163-
for (int i = startSlotIndex; i <= endSlotIndex; i++) {
164-
if (slots[i]) {
165-
return false;
166-
}
91+
if (!campusId.equals(meetingRoom.getCampusId())) {
92+
throw new ResourceMismatchException("해당 회의실은 캠퍼스내 회의실이 아닙니다.");
16793
}
168-
169-
return true;
170-
}
171-
172-
private int calculateSlotIndex(LocalTime localTime) {
173-
int hour = localTime.getHour();
174-
int minute = localTime.getMinute();
175-
176-
return (hour - 9) * 4 + (minute / 15);
177-
17894
}
17995
}

api/src/main/java/org/example/educheck/global/common/config/RedisConfig.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.example.educheck.global.common.config;
22

3+
import com.fasterxml.jackson.databind.ObjectMapper;
34
import org.springframework.beans.factory.annotation.Value;
45
import org.springframework.context.annotation.Bean;
56
import org.springframework.context.annotation.Configuration;
@@ -34,10 +35,19 @@ public RedisTemplate<String, Object> redisTemplate() {
3435
RedisTemplate<String, Object> template = new RedisTemplate<>();
3536
template.setConnectionFactory(connectionFactory);
3637

37-
template.setKeySerializer(new StringRedisSerializer());
38-
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
38+
ObjectMapper objectMapper = new ObjectMapper();
39+
objectMapper.activateDefaultTyping(
40+
objectMapper.getPolymorphicTypeValidator(),
41+
ObjectMapper.DefaultTyping.NON_FINAL
42+
);
43+
44+
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(objectMapper);
45+
46+
template.setValueSerializer(serializer);
47+
template.setHashValueSerializer(serializer);
3948
template.setHashKeySerializer(new StringRedisSerializer());
40-
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
49+
template.setKeySerializer(new StringRedisSerializer());
50+
template.afterPropertiesSet();
4151

4252
return template;
4353
}

api/src/main/java/org/example/educheck/global/common/exception/ErrorCode.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,17 @@ public enum ErrorCode {
1010

1111
//0000 회원
1212
//1000 예약
13+
RESERVATION_CONFLICT(HttpStatus.CONFLICT, "2000", "해당 시간에는 이미 예약이 있습니다. 다른 시간을 선택해주세요."),
14+
1315
//2000
1416

15-
//예시 2개
16-
RESERVATION_CONFLICT(HttpStatus.CONFLICT, "6000", "해당 시간에는 이미 예약이 있습니다."),
17+
//예시
1718
MEETINGROOM_NOT_FOUND(HttpStatus.NOT_FOUND, "3000", "회의실을 찾을 수 없습니다."),
1819

1920
//4000 5000 공통
2021
INVALID_INPUT(HttpStatus.BAD_REQUEST, "4000", "입력값이 올바르지 않습니다."),
2122
RESOURCE_NOT_FOUND(HttpStatus.NOT_FOUND, "4004", "해당 리소스가 존재하지 않습니다."),
23+
MISMATCHED_RESOURCE(HttpStatus.BAD_REQUEST, "4008", "요청한 리소스가 일치하지 않습니다.")
2224
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "4010", "인증에 실패했습니다.");
2325

2426
private final HttpStatus status;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package org.example.educheck.global.common.exception.custom.common;
2+
3+
4+
import org.example.educheck.global.common.exception.ErrorCode;
5+
6+
public class ResourceMismatchException extends GlobalException {
7+
public ResourceMismatchException(ErrorCode errorCode) {
8+
super(errorCode);
9+
}
10+
11+
public ResourceMismatchException(String customMessage) {
12+
super(ErrorCode.MISMATCHED_RESOURCE, customMessage);
13+
}
14+
15+
public ResourceMismatchException() {
16+
super(ErrorCode.MISMATCHED_RESOURCE, ErrorCode.MISMATCHED_RESOURCE.getMessage());
17+
}
18+
}

api/src/main/java/org/example/educheck/global/common/exception/custom/reservation/ReservationConflictException.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ public ReservationConflictException(ErrorCode errorCode) {
99
}
1010

1111
public ReservationConflictException() {
12-
super(ErrorCode.RESERVATION_CONFLICT, ErrorCode.RESERVATION_CONFLICT.getMessage());
12+
super(ErrorCode.RESERVATION_CONFLICT);
13+
}
14+
15+
public ReservationConflictException(String customMessage) {
16+
super(ErrorCode.RESERVATION_CONFLICT, customMessage);
1317
}
1418
}

api/src/main/java/org/example/educheck/global/security/config/SecurityConfig.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
3333
.sessionManagement(session -> session
3434
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
3535
.authorizeHttpRequests(auth -> auth
36-
.requestMatchers("/**").permitAll()
36+
.requestMatchers("/api/auth/login", "/api/auth/signup").permitAll()
37+
.anyRequest().authenticated()
3738
// TODO: 인증 엔드포인트 수정
3839
)
3940
// TODO: 비밀번호 예외처리

api/src/main/java/org/example/educheck/global/security/jwt/JwtTokenUtil.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public String createRefreshToken(Authentication authentication) {
7171
}
7272

7373
public String createAccessToken(Authentication authentication) {
74-
long accessTokenValidityMilliSeconds = 1000L * 60 * 60;
74+
long accessTokenValidityMilliSeconds = 1000L * 60 * 60 * 24 * 7; // 개발 7일
7575

7676
return createToken(authentication, accessTokenValidityMilliSeconds);
7777
}

api/src/main/resources/application.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ spring.docker.compose.lifecycle-management=start-and-stop
1010
spring.jpa.show-sql=true
1111
spring.jpa.hibernate.ddl-auto=update
1212
jwt.secret=${JWT_SECRET}
13+
logging.level.org.springframework.security=DEBUG

api/src/test/java/org/example/educheck/domain/meetingroomreservation/http.http

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ GET https://examples.http-client.intellij.net/get
33
?generated-in=IntelliJ IDEA
44

55
###
6-
POST http://localhost:8080/api/campuses/17/meeting-rooms/reservations
6+
POST http://localhost:8080/api/campuses/1/meeting-rooms/reservations
77
Content-Type: application/json
8-
Authorization: eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3NDI0MDQzNTcsImV4cCI6MTc0MjQwNDQxN30.P2eXpnR_dDaoqRVu2AdZoLmprZru_IXKNGdBispM3BY
8+
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0QG5hdmVyLmNvbSIsInJvbGVzIjpbIlNUVURFTlQiXSwiaWF0IjoxNzQyNDkyMTgxLCJleHAiOjE3NDI0OTU3ODF9.fgPPSxZZMWZvD1CmcIE1sYc4AdbIkXBp6EO
99

1010
{
1111
"startTime": "2025-03-20T09:00:00",
1212
"endTime": "2025-03-20T10:00:00",
13-
"meetingRoomId": 60,
14-
"courseId": 1
13+
"meetingRoomId": 2,
14+
"courseId": 5
1515
}

0 commit comments

Comments
 (0)