diff --git a/src/main/java/com/threestar/trainus/domain/test/controller/TestConcurrencyController.java b/src/main/java/com/threestar/trainus/domain/test/controller/TestConcurrencyController.java new file mode 100644 index 0000000..2fd627d --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/test/controller/TestConcurrencyController.java @@ -0,0 +1,51 @@ +package com.threestar.trainus.domain.test.controller; + +import com.threestar.trainus.domain.coupon.user.dto.CreateUserCouponResponseDto; +import com.threestar.trainus.domain.coupon.user.service.CouponService; +import com.threestar.trainus.domain.lesson.student.dto.LessonApplicationResponseDto; +import com.threestar.trainus.domain.lesson.student.service.StudentLessonService; +import com.threestar.trainus.domain.test.dto.TestRequestDto; +import com.threestar.trainus.domain.test.service.TestUserService; +import com.threestar.trainus.domain.user.entity.User; +import com.threestar.trainus.global.unit.BaseResponse; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "동시성 테스트 API", description = "선착순 기능 테스트를 위한 API") +@RestController +@RequestMapping("/test") +@RequiredArgsConstructor +public class TestConcurrencyController { + + private final CouponService couponService; + private final StudentLessonService studentLessonService; + private final TestUserService testUserService; + + @PostMapping("/coupons/{couponId}") + @Operation(summary = "쿠폰 발급 동시성 테스트", description = "쿠폰을 발급받는 테스트 API") + public ResponseEntity> issueCouponForTest( + @PathVariable Long couponId, + @RequestBody TestRequestDto testRequestDto + ) { + User user = testUserService.findOrCreateUser(testRequestDto.getUserId()); + CreateUserCouponResponseDto responseDto = couponService.createUserCoupon(user.getId(), couponId); + return BaseResponse.ok("쿠폰 발급 완료", responseDto, HttpStatus.CREATED); + } + + @PostMapping("/lessons/{lessonId}/application") + @Operation(summary = "레슨 신청 동시성 테스트", description = "레슨을 신청하는 테스트 API") + public ResponseEntity> applyToLessonForTest( + @PathVariable Long lessonId, + @RequestBody TestRequestDto testRequestDto + ) { + User user = testUserService.findOrCreateUser(testRequestDto.getUserId()); + LessonApplicationResponseDto response = studentLessonService.applyToLessonWithLock(lessonId, user.getId()); + return BaseResponse.ok("레슨 신청 완료", response, HttpStatus.OK); + } +} diff --git a/src/main/java/com/threestar/trainus/domain/test/dto/TestRequestDto.java b/src/main/java/com/threestar/trainus/domain/test/dto/TestRequestDto.java new file mode 100644 index 0000000..99b24d8 --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/test/dto/TestRequestDto.java @@ -0,0 +1,10 @@ +package com.threestar.trainus.domain.test.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class TestRequestDto { + private Long userId; +} diff --git a/src/main/java/com/threestar/trainus/domain/test/service/TestUserService.java b/src/main/java/com/threestar/trainus/domain/test/service/TestUserService.java new file mode 100644 index 0000000..ffee861 --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/test/service/TestUserService.java @@ -0,0 +1,51 @@ +package com.threestar.trainus.domain.test.service; + +import com.threestar.trainus.domain.profile.service.ProfileFacadeService; +import com.threestar.trainus.domain.user.entity.User; +import com.threestar.trainus.domain.user.entity.UserRole; +import com.threestar.trainus.domain.user.repository.UserRepository; + +import lombok.RequiredArgsConstructor; + +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class TestUserService { + + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final ProfileFacadeService profileFacadeService; + + @Transactional + public User findOrCreateUser(Long userId) { + Optional existingUser = userRepository.findById(userId); + if (existingUser.isPresent()) { + return existingUser.get(); + } + String email = "testuser" + userId + "@example.com"; + String nickname = "testuser" + userId; + + if (userRepository.existsByEmail(email) || userRepository.existsByNickname(nickname)) { + return userRepository.findByEmail(email) + .orElseThrow(() -> new IllegalStateException("테스트 유저 생성 실패")); + } + + String encodedPassword = passwordEncoder.encode("password"); + User newUser = User.builder() + .email(email) + .password(encodedPassword) + .nickname(nickname) + .role(UserRole.USER) + .build(); + + User savedUser = userRepository.save(newUser); + profileFacadeService.createDefaultProfile(savedUser); + + return savedUser; + } +} diff --git a/src/main/java/com/threestar/trainus/global/config/security/SecurityConfig.java b/src/main/java/com/threestar/trainus/global/config/security/SecurityConfig.java index 796ae3c..2a4f0c3 100644 --- a/src/main/java/com/threestar/trainus/global/config/security/SecurityConfig.java +++ b/src/main/java/com/threestar/trainus/global/config/security/SecurityConfig.java @@ -27,7 +27,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .authorizeHttpRequests(auth -> auth .requestMatchers("/api/v1/users/**", "/api/lessons/test-auth", "/swagger-ui/**", "/v3/api-docs/**", "/api/v1/profiles/**", "/api/v1/lessons/**", "/api/v1/comments/**", "/api/v1/reviews/**", - "/api/v1/rankings/**", "/api/v1/payments/**") + "/api/v1/rankings/**", "/api/v1/payments/**", "/test/**") .permitAll() .requestMatchers("/api/v1/admin/**") .hasRole("ADMIN")