From 0c32a0cfa2507a0c6b63128d44ac5047a4d3711f Mon Sep 17 00:00:00 2001 From: Jieun Kim <83564946+iamjieunkim@users.noreply.github.com> Date: Mon, 28 Jul 2025 16:09:48 +0900 Subject: [PATCH] =?UTF-8?q?feat:=EB=A0=88=EC=8A=A8=EC=83=9D=EC=84=B1?= =?UTF-8?q?=EC=8B=9C=EC=BF=A8=ED=83=80=EC=9E=84=EC=A0=9C=ED=95=9C=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../teacher/service/AdminLessonService.java | 6 +- .../service/LessonCreationLimitService.java | 51 +++++++++++++++ .../global/exception/domain/ErrorCode.java | 1 + .../LessonCreationLimitServiceTest.java | 64 +++++++++++++++++++ 4 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/threestar/trainus/domain/lesson/teacher/service/LessonCreationLimitService.java create mode 100644 src/test/java/com/threestar/trainus/domain/lesson/teacher/service/LessonCreationLimitServiceTest.java diff --git a/src/main/java/com/threestar/trainus/domain/lesson/teacher/service/AdminLessonService.java b/src/main/java/com/threestar/trainus/domain/lesson/teacher/service/AdminLessonService.java index 6fd457a3..e9b3bbfc 100644 --- a/src/main/java/com/threestar/trainus/domain/lesson/teacher/service/AdminLessonService.java +++ b/src/main/java/com/threestar/trainus/domain/lesson/teacher/service/AdminLessonService.java @@ -51,11 +51,15 @@ public class AdminLessonService { private final LessonImageRepository lessonImageRepository; private final LessonApplicationRepository lessonApplicationRepository; private final UserService userService; + private final LessonCreationLimitService lessonCreationLimitService; //레슨 생성 public LessonResponseDto createLesson(LessonCreateRequestDto requestDto, Long userId) { User user = userService.getUserById(userId); + //레슨 생성 제한 확인 및 쿨타임 설정 + lessonCreationLimitService.checkAndSetCreationLimit(userId); + // 생성시에 필요한 검증을 진행 validateLessonCreation(requestDto, userId); @@ -146,7 +150,7 @@ public ParticipantListResponseDto getLessonParticipants( Page participantPage = lessonApplicationRepository .findApprovedParticipantsWithUserAndProfile(lesson, pageable); - + return LessonParticipantMapper.toParticipantsResponseDto( participantPage.getContent(), participantPage.getTotalElements() diff --git a/src/main/java/com/threestar/trainus/domain/lesson/teacher/service/LessonCreationLimitService.java b/src/main/java/com/threestar/trainus/domain/lesson/teacher/service/LessonCreationLimitService.java new file mode 100644 index 00000000..457fcf3e --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/lesson/teacher/service/LessonCreationLimitService.java @@ -0,0 +1,51 @@ +package com.threestar.trainus.domain.lesson.teacher.service; + +import java.time.Duration; + +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import com.threestar.trainus.global.exception.domain.ErrorCode; +import com.threestar.trainus.global.exception.handler.BusinessException; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * 레슨 생성 제한 서비스 + * Redis를 사용해 강사의 레슨 생성에 쿨타임을 적용 ->1분 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class LessonCreationLimitService { + + private final RedisTemplate redisTemplate; + + // 레슨 생성 쿨타임 (1분) + private static final Duration CREATION_COOLTIME = Duration.ofMinutes(1); + + //redis 키 접두사 + private static final String LESSON_CREATION_KEY_PREFIX = "lesson_creation_limit:"; + + //레슨 생성 가능 여부 확인 + 쿨타임 설정 + public void checkAndSetCreationLimit(Long userId) { + String key = generateRedisKey(userId); + + // 이미 쿨타임이 설정되어 있는지 확인 + if (redisTemplate.hasKey(key)) { + Long remainingTtl = redisTemplate.getExpire(key); + log.info("레슨 생성 제한 - 사용자 ID: {}, 남은 시간: {}초", userId, remainingTtl); + throw new BusinessException(ErrorCode.LESSON_CREATION_TOO_FREQUENT); + } + + // 쿨타임 설정 + redisTemplate.opsForValue().set(key, "restricted", CREATION_COOLTIME); + log.info("레슨 생성 쿨타임 설정 - 사용자 ID: {}, 지속시간: {}분", userId, CREATION_COOLTIME.toMinutes()); + } + + // Redis 키를 생성 + private String generateRedisKey(Long userId) { + return LESSON_CREATION_KEY_PREFIX + userId; + } +} diff --git a/src/main/java/com/threestar/trainus/global/exception/domain/ErrorCode.java b/src/main/java/com/threestar/trainus/global/exception/domain/ErrorCode.java index ed10dae6..c2999d0a 100644 --- a/src/main/java/com/threestar/trainus/global/exception/domain/ErrorCode.java +++ b/src/main/java/com/threestar/trainus/global/exception/domain/ErrorCode.java @@ -77,6 +77,7 @@ public enum ErrorCode { LESSON_DELETE_STATUS_INVALID(HttpStatus.BAD_REQUEST, "모집중인 레슨만 삭제할 수 있습니다."), LESSON_DELETE_HAS_PARTICIPANTS(HttpStatus.BAD_REQUEST, "참가자가 있는 레슨은 삭제할 수 없습니다."), LESSON_TIME_LIMIT_EXCEEDED(HttpStatus.BAD_REQUEST, "레슨 시작 12시간 전이므로 수정/삭제할 수 없습니다."), + LESSON_CREATION_TOO_FREQUENT(HttpStatus.TOO_MANY_REQUESTS, "레슨 생성은 1분에 1번만 가능합니다. 잠시 후 다시 시도해주세요."), // 403 Forbidden LESSON_DELETE_FORBIDDEN(HttpStatus.FORBIDDEN, "레슨 삭제 권한이 없습니다. 강사만 삭제할 수 있습니다."), diff --git a/src/test/java/com/threestar/trainus/domain/lesson/teacher/service/LessonCreationLimitServiceTest.java b/src/test/java/com/threestar/trainus/domain/lesson/teacher/service/LessonCreationLimitServiceTest.java new file mode 100644 index 00000000..6f99448c --- /dev/null +++ b/src/test/java/com/threestar/trainus/domain/lesson/teacher/service/LessonCreationLimitServiceTest.java @@ -0,0 +1,64 @@ +package com.threestar.trainus.domain.lesson.teacher.service; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.time.Duration; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; + +import com.threestar.trainus.global.exception.domain.ErrorCode; +import com.threestar.trainus.global.exception.handler.BusinessException; + +@ExtendWith(MockitoExtension.class) +class LessonCreationLimitServiceTest { + @Mock + private RedisTemplate redisTemplate; + + @Mock + private ValueOperations valueOperations; + + @InjectMocks + private LessonCreationLimitService lessonCreationLimitService; + + @Test + @DisplayName("첫 레슨 생성 시 제한 없이 성공한다") + void checkAndSetCreationLimit_FirstTime_Success() { + Long userId = 1L; + String expectedKey = "lesson_creation_limit:" + userId; + + when(redisTemplate.hasKey(expectedKey)).thenReturn(false); + when(redisTemplate.opsForValue()).thenReturn(valueOperations); + + assertThatCode(() -> lessonCreationLimitService.checkAndSetCreationLimit(userId)) + .doesNotThrowAnyException(); + + verify(redisTemplate).hasKey(expectedKey); + verify(valueOperations).set(expectedKey, "restricted", Duration.ofMinutes(1)); + } + + @Test + @DisplayName("쿨타임이 남아있을 때 레슨 생성을 제한한다") + void checkAndSetCreationLimit_WithinCooltime_ThrowsException() { + Long userId = 1L; + String expectedKey = "lesson_creation_limit:" + userId; + + when(redisTemplate.hasKey(expectedKey)).thenReturn(true); + when(redisTemplate.getExpire(expectedKey)).thenReturn(30L); // 30초 남음 + + assertThatThrownBy(() -> lessonCreationLimitService.checkAndSetCreationLimit(userId)) + .isInstanceOf(BusinessException.class) + .extracting(e -> ((BusinessException)e).getErrorCode()) + .isEqualTo(ErrorCode.LESSON_CREATION_TOO_FREQUENT); + + verify(redisTemplate).hasKey(expectedKey); + verify(redisTemplate, never()).opsForValue(); + } +}