Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -146,7 +150,7 @@ public ParticipantListResponseDto getLessonParticipants(

Page<LessonApplication> participantPage = lessonApplicationRepository
.findApprovedParticipantsWithUserAndProfile(lesson, pageable);

return LessonParticipantMapper.toParticipantsResponseDto(
participantPage.getContent(),
participantPage.getTotalElements()
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, String> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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, "레슨 삭제 권한이 없습니다. 강사만 삭제할 수 있습니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, String> redisTemplate;

@Mock
private ValueOperations<String, String> 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();
}
}
Loading