From 943c89064f064d6a846f3b17806a61617c2e5e4f Mon Sep 17 00:00:00 2001 From: Jieun Kim <83564946+iamjieunkim@users.noreply.github.com> Date: Thu, 17 Jul 2025 11:45:01 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:profile=EC=97=90=EC=84=9C=20=EB=A0=88?= =?UTF-8?q?=EC=8A=A8=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../profile/controller/ProfileController.java | 28 ++++++++++ .../profile/dto/ProfileCreatedLessonDto.java | 25 +++++++++ .../ProfileCreatedLessonListResponseDto.java | 15 +++++ .../ProfileCreatedLessonListWrapperDto.java | 8 +++ .../profile/mapper/ProfileLessonMapper.java | 51 +++++++++++++++++ .../profile/service/ProfileLessonService.java | 56 +++++++++++++++++++ 6 files changed, 183 insertions(+) create mode 100644 src/main/java/com/threestar/trainus/domain/profile/dto/ProfileCreatedLessonDto.java create mode 100644 src/main/java/com/threestar/trainus/domain/profile/dto/ProfileCreatedLessonListResponseDto.java create mode 100644 src/main/java/com/threestar/trainus/domain/profile/dto/ProfileCreatedLessonListWrapperDto.java create mode 100644 src/main/java/com/threestar/trainus/domain/profile/mapper/ProfileLessonMapper.java create mode 100644 src/main/java/com/threestar/trainus/domain/profile/service/ProfileLessonService.java diff --git a/src/main/java/com/threestar/trainus/domain/profile/controller/ProfileController.java b/src/main/java/com/threestar/trainus/domain/profile/controller/ProfileController.java index 4cd0738..0fae815 100644 --- a/src/main/java/com/threestar/trainus/domain/profile/controller/ProfileController.java +++ b/src/main/java/com/threestar/trainus/domain/profile/controller/ProfileController.java @@ -3,20 +3,29 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import com.threestar.trainus.domain.lesson.admin.entity.LessonStatus; import com.threestar.trainus.domain.profile.dto.ImageUpdateRequestDto; import com.threestar.trainus.domain.profile.dto.ImageUpdateResponseDto; import com.threestar.trainus.domain.profile.dto.IntroUpdateRequestDto; import com.threestar.trainus.domain.profile.dto.IntroUpdateResponseDto; +import com.threestar.trainus.domain.profile.dto.ProfileCreatedLessonListResponseDto; +import com.threestar.trainus.domain.profile.dto.ProfileCreatedLessonListWrapperDto; import com.threestar.trainus.domain.profile.dto.ProfileDetailResponseDto; +import com.threestar.trainus.domain.profile.mapper.ProfileLessonMapper; import com.threestar.trainus.domain.profile.service.ProfileFacadeService; +import com.threestar.trainus.domain.profile.service.ProfileLessonService; import com.threestar.trainus.global.annotation.LoginUser; +import com.threestar.trainus.global.dto.PageRequestDto; import com.threestar.trainus.global.unit.BaseResponse; +import com.threestar.trainus.global.unit.PagedResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -30,6 +39,7 @@ public class ProfileController { private final ProfileFacadeService facadeService; + private final ProfileLessonService profileLessonService; @GetMapping("{userId}") @Operation(summary = "유저 프로필 상세 조회 api") @@ -59,4 +69,22 @@ public ResponseEntity> updateProfileIntro( IntroUpdateResponseDto response = facadeService.updateProfileIntro(loginUserId, requestDto); return BaseResponse.ok("프로필 자기소개 수정이 완료되었습니다.", response, HttpStatus.OK); } + + @GetMapping("/{userId}/created-lessons") + @Operation(summary = "프로필유저의 개설한 레슨 목록 조회 api", + description = "특정 유저가 개설한 레슨 목록을 조회 -> 누구나 조회가능") + public ResponseEntity> getUserCreatedLessons( + @PathVariable Long userId, + @Valid @ModelAttribute PageRequestDto pageRequestDto, + @RequestParam(required = false) LessonStatus status + ) { + // 개설한 레슨 목록 조회 + ProfileCreatedLessonListResponseDto responseDto = profileLessonService + .getUserCreatedLessons(userId, pageRequestDto.getPage(), pageRequestDto.getLimit(), status); + + ProfileCreatedLessonListWrapperDto wrapperDto = ProfileLessonMapper + .toProfileCreatedLessonListWrapperDto(responseDto); + + return PagedResponse.ok("개설한 레슨 목록 조회 완료.", wrapperDto, responseDto.count(), HttpStatus.OK); + } } diff --git a/src/main/java/com/threestar/trainus/domain/profile/dto/ProfileCreatedLessonDto.java b/src/main/java/com/threestar/trainus/domain/profile/dto/ProfileCreatedLessonDto.java new file mode 100644 index 0000000..d8c129a --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/profile/dto/ProfileCreatedLessonDto.java @@ -0,0 +1,25 @@ +package com.threestar.trainus.domain.profile.dto; + +import java.time.LocalDateTime; + +import com.threestar.trainus.domain.lesson.admin.entity.LessonStatus; + +import lombok.Builder; + +/** + * 프로필에서 보여줄 개설한 레슨 정보 + */ +@Builder +public record ProfileCreatedLessonDto( + Long id, + String lessonName, + Integer maxParticipants, + Integer currentParticipants, + Integer price, + LessonStatus status, + LocalDateTime startAt, + LocalDateTime endAt, + Boolean openRun, + String addressDetail +) { +} diff --git a/src/main/java/com/threestar/trainus/domain/profile/dto/ProfileCreatedLessonListResponseDto.java b/src/main/java/com/threestar/trainus/domain/profile/dto/ProfileCreatedLessonListResponseDto.java new file mode 100644 index 0000000..4f854da --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/profile/dto/ProfileCreatedLessonListResponseDto.java @@ -0,0 +1,15 @@ +package com.threestar.trainus.domain.profile.dto; + +import java.util.List; + +import lombok.Builder; + +/** + * 프로필에서 개설한 레슨 목록 응답 + */ +@Builder +public record ProfileCreatedLessonListResponseDto( + List lessons, + Integer count +) { +} diff --git a/src/main/java/com/threestar/trainus/domain/profile/dto/ProfileCreatedLessonListWrapperDto.java b/src/main/java/com/threestar/trainus/domain/profile/dto/ProfileCreatedLessonListWrapperDto.java new file mode 100644 index 0000000..6d62138 --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/profile/dto/ProfileCreatedLessonListWrapperDto.java @@ -0,0 +1,8 @@ +package com.threestar.trainus.domain.profile.dto; + +import java.util.List; + +public record ProfileCreatedLessonListWrapperDto( + List lessons +) { +} diff --git a/src/main/java/com/threestar/trainus/domain/profile/mapper/ProfileLessonMapper.java b/src/main/java/com/threestar/trainus/domain/profile/mapper/ProfileLessonMapper.java new file mode 100644 index 0000000..77fcbbb --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/profile/mapper/ProfileLessonMapper.java @@ -0,0 +1,51 @@ +package com.threestar.trainus.domain.profile.mapper; + +import java.util.List; + +import com.threestar.trainus.domain.lesson.admin.entity.Lesson; +import com.threestar.trainus.domain.profile.dto.ProfileCreatedLessonDto; +import com.threestar.trainus.domain.profile.dto.ProfileCreatedLessonListResponseDto; +import com.threestar.trainus.domain.profile.dto.ProfileCreatedLessonListWrapperDto; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ProfileLessonMapper { + + // Lesson 엔티티를 ProfileCreatedLessonDto로 변환 + public static ProfileCreatedLessonDto toProfileCreatedLessonDto(Lesson lesson) { + return ProfileCreatedLessonDto.builder() + .id(lesson.getId()) + .lessonName(lesson.getLessonName()) + .maxParticipants(lesson.getMaxParticipants()) + .currentParticipants(lesson.getParticipantCount()) + .price(lesson.getPrice()) + .status(lesson.getStatus()) + .startAt(lesson.getStartAt()) + .endAt(lesson.getEndAt()) + .openRun(lesson.getOpenRun()) + .addressDetail(lesson.getAddressDetail()) + .build(); + } + + // 개설한 레슨 목록과 총 레슨의 수를 응답 DTO로 변환 + public static ProfileCreatedLessonListResponseDto toProfileCreatedLessonListResponseDto( + List lessons, Long totalCount) { + + // 각 레슨을 DTO로 변환 + List lessonDtos = lessons.stream() + .map(ProfileLessonMapper::toProfileCreatedLessonDto) + .toList(); + + return ProfileCreatedLessonListResponseDto.builder() + .lessons(lessonDtos) + .count(totalCount.intValue()) + .build(); + } + + public static ProfileCreatedLessonListWrapperDto toProfileCreatedLessonListWrapperDto( + ProfileCreatedLessonListResponseDto responseDto) { + return new ProfileCreatedLessonListWrapperDto(responseDto.lessons()); + } +} diff --git a/src/main/java/com/threestar/trainus/domain/profile/service/ProfileLessonService.java b/src/main/java/com/threestar/trainus/domain/profile/service/ProfileLessonService.java new file mode 100644 index 0000000..0ed6193 --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/profile/service/ProfileLessonService.java @@ -0,0 +1,56 @@ +package com.threestar.trainus.domain.profile.service; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.threestar.trainus.domain.lesson.admin.entity.Lesson; +import com.threestar.trainus.domain.lesson.admin.entity.LessonStatus; +import com.threestar.trainus.domain.lesson.admin.repository.LessonRepository; +import com.threestar.trainus.domain.profile.dto.ProfileCreatedLessonListResponseDto; +import com.threestar.trainus.domain.profile.mapper.ProfileLessonMapper; +import com.threestar.trainus.domain.user.entity.User; +import com.threestar.trainus.domain.user.repository.UserRepository; +import com.threestar.trainus.domain.user.service.UserService; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class ProfileLessonService { + + private final LessonRepository lessonRepository; + private final UserRepository userRepository; + private final UserService userService; + + // 특정 유저가 개설한 레슨 목록 조회 + @Transactional(readOnly = true) + public ProfileCreatedLessonListResponseDto getUserCreatedLessons( + Long userId, int page, int limit, LessonStatus status) { + + // User 존재 확인 + User user = userService.getUserById(userId); + + // 페이징 설정 -> 내림차순!! + Pageable pageable = PageRequest.of(page - 1, limit, Sort.by("createdAt").descending()); + + // 레슨 상태에 따른 조회 + Page lessonPage; + if (status != null) { + // 상태에 따라 조회 가능 + lessonPage = lessonRepository.findByLessonLeaderAndStatusAndDeletedAtIsNull(userId, status, pageable); + } else { + // 삭제되지 않은 레슨만 조회 + lessonPage = lessonRepository.findByLessonLeaderAndDeletedAtIsNull(userId, pageable); + } + + // DTO 변환 + return ProfileLessonMapper.toProfileCreatedLessonListResponseDto( + lessonPage.getContent(), + lessonPage.getTotalElements() + ); + } +} \ No newline at end of file From d4265b07c8bb4f7e3d0f622ea5ceb7d29d9f107e Mon Sep 17 00:00:00 2001 From: Jieun Kim <83564946+iamjieunkim@users.noreply.github.com> Date: Thu, 17 Jul 2025 12:27:58 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=EB=A0=88=EC=8A=A8=EB=AA=A9=EB=A1=9D?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=ED=94=84=EB=A1=9C=ED=95=84=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/config/MockDataInitializer.java | 627 +++++++++++------- 1 file changed, 393 insertions(+), 234 deletions(-) diff --git a/src/main/java/com/threestar/trainus/global/config/MockDataInitializer.java b/src/main/java/com/threestar/trainus/global/config/MockDataInitializer.java index 4977310..561812b 100644 --- a/src/main/java/com/threestar/trainus/global/config/MockDataInitializer.java +++ b/src/main/java/com/threestar/trainus/global/config/MockDataInitializer.java @@ -1,234 +1,393 @@ -// package com.threestar.trainus.global.config; -// -// import java.time.LocalDateTime; -// import java.util.ArrayList; -// import java.util.List; -// import java.util.Random; -// -// import org.springframework.boot.CommandLineRunner; -// import org.springframework.security.crypto.password.PasswordEncoder; -// import org.springframework.stereotype.Component; -// import org.springframework.transaction.annotation.Transactional; -// -// import com.threestar.trainus.domain.lesson.admin.entity.Category; -// import com.threestar.trainus.domain.lesson.admin.entity.Lesson; -// import com.threestar.trainus.domain.lesson.admin.repository.LessonRepository; -// import com.threestar.trainus.domain.metadata.entity.ProfileMetadata; -// import com.threestar.trainus.domain.metadata.mapper.ProfileMetadataMapper; -// import com.threestar.trainus.domain.metadata.repository.ProfileMetadataRepository; -// import com.threestar.trainus.domain.profile.entity.Profile; -// import com.threestar.trainus.domain.profile.mapper.ProfileMapper; -// import com.threestar.trainus.domain.profile.repository.ProfileRepository; -// import com.threestar.trainus.domain.review.entity.Review; -// import com.threestar.trainus.domain.review.repository.ReviewRepository; -// 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 lombok.extern.slf4j.Slf4j; -// -// @Slf4j -// @Component -// @org.springframework.context.annotation.Profile("dev") // dev 프로필에서만 실행 -// @RequiredArgsConstructor -// public class MockDataInitializer implements CommandLineRunner { -// -// private final UserRepository userRepository; -// private final ProfileRepository profileRepository; -// private final ProfileMetadataRepository profileMetadataRepository; -// private final LessonRepository lessonRepository; -// private final ReviewRepository reviewRepository; -// private final PasswordEncoder passwordEncoder; -// -// private final Random random = new Random(); -// -// @Override -// @Transactional -// public void run(String... args) throws Exception { -// if (userRepository.count() > 0) { -// log.info("데이터가 이미 존재합니다. Mock 데이터 생성을 건너뜁니다."); -// return; -// } -// -// log.info("Mock 데이터 생성 시작..."); -// -// // 1. 강사 유저 생성 (15명) -// List instructors = createInstructors(); -// -// // 2. 수강생 유저 생성 (50명) -// List students = createStudents(); -// -// // 3. 각 강사별로 레슨 생성 -// List lessons = createLessons(instructors); -// -// // 4. 리뷰 생성 (ProfileMetadata 업데이트) -// createReviews(instructors, students, lessons); -// -// log.info("Mock 데이터 생성 완료!"); -// log.info("생성된 데이터: 강사 {}명, 수강생 {}명, 레슨 {}개", -// instructors.size(), students.size(), lessons.size()); -// } -// -// private List createInstructors() { -// List instructors = new ArrayList<>(); -// String[] instructorNames = { -// "헬스왕김철수", "요가마스터", "필라테스여신", "러닝코치박", "수영선수이", -// "테니스프로", "복싱챔피언", "클라이밍킹", "골프레슨프로", "댄스퀸", -// "크로스핏코치", "배드민턴고수", "탁구선수", "농구코치", "축구감독" -// }; -// -// for (int i = 0; i < instructorNames.length; i++) { -// User instructor = User.builder() -// .email("instructor" + (i + 1) + "@test.com") -// .password(passwordEncoder.encode("password123")) -// .nickname(instructorNames[i]) -// .role(UserRole.USER) -// .build(); -// -// User savedInstructor = userRepository.save(instructor); -// -// // Profile 생성 -// Profile profile = ProfileMapper.toDefaultEntity(savedInstructor); -// profile.updateProfileImage( -// "https://example.com/instructor" + (i + 1) + ".jpg", -// instructorNames[i] + "입니다. 최고의 레슨을 제공합니다!" -// ); -// profileRepository.save(profile); -// -// // ProfileMetadata 생성 (랭킹용 데이터) -// ProfileMetadata metadata = ProfileMetadata.builder() -// .user(savedInstructor) -// .reviewCount(generateReviewCount()) -// .rating(generateRating()) -// .build(); -// profileMetadataRepository.save(metadata); -// -// instructors.add(savedInstructor); -// } -// -// return instructors; -// } -// -// private List createStudents() { -// List students = new ArrayList<>(); -// -// for (int i = 0; i < 50; i++) { -// User student = User.builder() -// .email("student" + (i + 1) + "@test.com") -// .password(passwordEncoder.encode("password123")) -// .nickname("수강생" + (i + 1)) -// .role(UserRole.USER) -// .build(); -// -// User savedStudent = userRepository.save(student); -// -// // Profile 생성 -// Profile profile = ProfileMapper.toDefaultEntity(savedStudent); -// profile.updateProfileImage( -// "https://example.com/student" + (i + 1) + ".jpg", -// "운동을 열심히 하는 수강생입니다!" -// ); -// profileRepository.save(profile); -// -// // 수강생은 메타데이터 기본값 -// ProfileMetadata metadata = ProfileMetadataMapper.toDefaultEntity(savedStudent); -// profileMetadataRepository.save(metadata); -// -// students.add(savedStudent); -// } -// -// return students; -// } -// -// private List createLessons(List instructors) { -// List lessons = new ArrayList<>(); -// Category[] categories = Category.values(); -// -// for (User instructor : instructors) { -// // 각 강사당 2-4개의 레슨 생성 -// int lessonCount = random.nextInt(3) + 2; -// -// for (int i = 0; i < lessonCount; i++) { -// Category category = categories[random.nextInt(categories.length)]; -// -// Lesson lesson = Lesson.builder() -// .lessonLeader(instructor.getId()) -// .lessonName(category.name() + " 레슨" + (i + 1)) -// .description("최고의 " + category.name() + " 레슨입니다!") -// .maxParticipants(random.nextInt(10) + 5) // 5-14명 -// .startAt(LocalDateTime.now().plusDays(random.nextInt(30) + 1)) -// .endAt(LocalDateTime.now().plusDays(random.nextInt(30) + 1).plusHours(2)) -// .price(random.nextInt(50000) + 10000) // 10,000-60,000원 -// .category(category) -// .openTime(null) -// .openRun(false) -// .city("서울시") -// .district("강남구") -// .dong("역삼동") -// .addressDetail("테스트 주소 " + random.nextInt(100)) -// .build(); -// -// lessons.add(lessonRepository.save(lesson)); -// } -// } -// -// return lessons; -// } -// -// private void createReviews(List instructors, List students, List lessons) { -// for (User instructor : instructors) { -// ProfileMetadata metadata = profileMetadataRepository.findByUserId(instructor.getId()) -// .orElseThrow(); -// -// // 해당 강사의 레슨들 찾기 -// List instructorLessons = lessons.stream() -// .filter(lesson -> lesson.getLessonLeader().equals(instructor.getId())) -// .toList(); -// -// if (instructorLessons.isEmpty()) { -// continue; -// } -// -// // reviewCount만큼 실제 리뷰 생성 -// int reviewCount = metadata.getReviewCount(); -// -// for (int i = 0; i < reviewCount; i++) { -// User randomStudent = students.get(random.nextInt(students.size())); -// Lesson randomLesson = instructorLessons.get(random.nextInt(instructorLessons.size())); -// -// // 평점은 metadata의 rating 주변으로 생성 -// double baseRating = metadata.getRating(); -// double reviewRating = Math.max(1.0d, Math.min(5.0d, -// baseRating + (random.nextDouble() - 0.5d) * 2)); // ±1점 범위 -// -// Review review = Review.builder() -// .reviewer(randomStudent) -// .reviewee(instructor) -// .lesson(randomLesson) -// .rating(reviewRating) -// .content("좋은 레슨이었습니다! 추천해요.") -// .image(random.nextBoolean() ? "https://example.com/review" + i + ".jpg" : null) -// .build(); -// -// reviewRepository.save(review); -// } -// } -// } -// -// private int generateReviewCount() { -// // 랭킹에 들어갈 강사들(20개 이상)과 그렇지 않은 강사들 섞어서 생성 -// int rand = random.nextInt(100); -// if (rand < 60) { // 60% 확률로 랭킹 대상 -// return random.nextInt(50) + 20; // 20-69개 -// } else { // 40% 확률로 랭킹 비대상 -// return random.nextInt(20); // 0-19개 -// } -// } -// -// private double generateRating() { -// // 3.0 ~ 5.0 사이의 평점 생성 (소수점 1자리) -// double rating = 3.0d + random.nextDouble() * 2.0d; -// return Math.round(rating * 10) / 10.0d; -// } -// } +package com.threestar.trainus.global.config; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import com.threestar.trainus.domain.coupon.user.entity.Coupon; +import com.threestar.trainus.domain.coupon.user.entity.CouponCategory; +import com.threestar.trainus.domain.coupon.user.entity.CouponStatus; +import com.threestar.trainus.domain.coupon.user.entity.UserCoupon; +import com.threestar.trainus.domain.coupon.user.repository.CouponRepository; +import com.threestar.trainus.domain.coupon.user.repository.UserCouponRepository; +import com.threestar.trainus.domain.lesson.admin.entity.Category; +import com.threestar.trainus.domain.lesson.admin.entity.Lesson; +import com.threestar.trainus.domain.lesson.admin.repository.LessonRepository; +import com.threestar.trainus.domain.metadata.entity.ProfileMetadata; +import com.threestar.trainus.domain.metadata.mapper.ProfileMetadataMapper; +import com.threestar.trainus.domain.metadata.repository.ProfileMetadataRepository; +import com.threestar.trainus.domain.profile.entity.Profile; +import com.threestar.trainus.domain.profile.mapper.ProfileMapper; +import com.threestar.trainus.domain.profile.repository.ProfileRepository; +import com.threestar.trainus.domain.review.entity.Review; +import com.threestar.trainus.domain.review.repository.ReviewRepository; +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 lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +@org.springframework.context.annotation.Profile("dev") // dev 프로필에서만 실행 +@RequiredArgsConstructor +public class MockDataInitializer implements CommandLineRunner { + + private final UserRepository userRepository; + private final ProfileRepository profileRepository; + private final ProfileMetadataRepository profileMetadataRepository; + private final LessonRepository lessonRepository; + private final ReviewRepository reviewRepository; + private final PasswordEncoder passwordEncoder; + private final CouponRepository couponRepository; + private final UserCouponRepository userCouponRepository; + + private final Random random = new Random(); + + @Override + @Transactional + public void run(String... args) throws Exception { + if (userRepository.count() > 0) { + log.info("데이터가 이미 존재합니다. Mock 데이터 생성을 건너뜁니다."); + return; + } + + log.info("Mock 데이터 생성 시작..."); + + // 1. 강사 유저 생성 (15명) + List instructors = createInstructors(); + + // 2. 수강생 유저 생성 (50명) + List students = createStudents(); + + // 3. 각 강사별로 레슨 생성 + List lessons = createLessons(instructors); + + // 4. 리뷰 생성 (ProfileMetadata 업데이트) + createReviews(instructors, students, lessons); + + // 5. 쿠폰 생성 (관리자가 생성) + List coupons = createCoupons(); + + // 6. 유저쿠폰 생성 (사용자들이 쿠폰 발급받은 데이터) + createUserCoupons(students, coupons); + + log.info("Mock 데이터 생성 완료!"); + log.info("생성된 데이터: 강사 {}명, 수강생 {}명, 레슨 {}개", + instructors.size(), students.size(), lessons.size()); + } + + private List createInstructors() { + List instructors = new ArrayList<>(); + String[] instructorNames = { + "헬스왕김철수", "요가마스터", "필라테스여신", "러닝코치박", "수영선수이", + "테니스프로", "복싱챔피언", "클라이밍킹", "골프레슨프로", "댄스퀸", + "크로스핏코치", "배드민턴고수", "탁구선수", "농구코치", "축구감독" + }; + + for (int i = 0; i < instructorNames.length; i++) { + User instructor = User.builder() + .email("instructor" + (i + 1) + "@test.com") + .password(passwordEncoder.encode("password123")) + .nickname(instructorNames[i]) + .role(UserRole.USER) + .build(); + + User savedInstructor = userRepository.save(instructor); + + // Profile 생성 + Profile profile = ProfileMapper.toDefaultEntity(savedInstructor); + profile.updateProfileImage("https://example.com/instructor" + (i + 1) + ".jpg"); + profile.updateProfileIntro(instructorNames[i] + "입니다. 최고의 레슨을 제공합니다!"); + profileRepository.save(profile); + + // ProfileMetadata 생성 (랭킹용 데이터) + ProfileMetadata metadata = ProfileMetadata.builder() + .user(savedInstructor) + .reviewCount(generateReviewCount()) + .rating(generateRating()) + .build(); + profileMetadataRepository.save(metadata); + + instructors.add(savedInstructor); + } + + // 관리자 계정 추가 + User admin = User.builder() + .email("admin@test.com") + .password(passwordEncoder.encode("admin123")) + .nickname("관리자") + .role(UserRole.ADMIN) //관리자 + .build(); + + User savedAdmin = userRepository.save(admin); + + // 관리자 Profile 생성 + Profile adminProfile = ProfileMapper.toDefaultEntity(savedAdmin); + adminProfile.updateProfileImage("https://example.com/admin.jpg"); + profileRepository.save(adminProfile); + + // 관리자 ProfileMetadata 생성 + ProfileMetadata adminMetadata = ProfileMetadataMapper.toDefaultEntity(savedAdmin); + profileMetadataRepository.save(adminMetadata); + + instructors.add(savedAdmin); + + return instructors; + } + + private List createStudents() { + List students = new ArrayList<>(); + + for (int i = 0; i < 50; i++) { + User student = User.builder() + .email("student" + (i + 1) + "@test.com") + .password(passwordEncoder.encode("password123")) + .nickname("수강생" + (i + 1)) + .role(UserRole.USER) + .build(); + + User savedStudent = userRepository.save(student); + + // Profile 생성 + Profile profile = ProfileMapper.toDefaultEntity(savedStudent); + profile.updateProfileImage("https://example.com/student" + (i + 1) + ".jpg"); + profileRepository.save(profile); + + // 수강생은 메타데이터 기본값 + ProfileMetadata metadata = ProfileMetadataMapper.toDefaultEntity(savedStudent); + profileMetadataRepository.save(metadata); + + students.add(savedStudent); + } + + return students; + } + + private List createLessons(List instructors) { + List lessons = new ArrayList<>(); + Category[] categories = Category.values(); + + for (User instructor : instructors) { + + // 각 강사당 2-4개의 레슨 생성 + int lessonCount = random.nextInt(3) + 2; + + for (int i = 0; i < lessonCount; i++) { + Category category = categories[random.nextInt(categories.length)]; + + Lesson lesson = Lesson.builder() + .lessonLeader(instructor.getId()) + .lessonName(category.name() + " 레슨" + (i + 1)) + .description("최고의 " + category.name() + " 레슨입니다!") + .maxParticipants(random.nextInt(10) + 5) // 5-14명 + .startAt(LocalDateTime.now().plusDays(random.nextInt(30) + 1)) + .endAt(LocalDateTime.now().plusDays(random.nextInt(30) + 1).plusHours(2)) + .price(random.nextInt(50000) + 10000) // 10,000-60,000원 + .category(category) + .openTime(null) + .openRun(false) + .city("서울시") + .district("강남구") + .dong("역삼동") + .addressDetail("테스트 주소 " + random.nextInt(100)) + .build(); + + lessons.add(lessonRepository.save(lesson)); + } + } + + return lessons; + } + + private void createReviews(List instructors, List students, List lessons) { + for (User instructor : instructors) { + + ProfileMetadata metadata = profileMetadataRepository.findByUserId(instructor.getId()) + .orElseThrow(); + + // 해당 강사의 레슨들 찾기 + List instructorLessons = lessons.stream() + .filter(lesson -> lesson.getLessonLeader().equals(instructor.getId())) + .toList(); + + if (instructorLessons.isEmpty()) { + continue; + } + + // reviewCount만큼 실제 리뷰 생성 + int reviewCount = metadata.getReviewCount(); + + for (int i = 0; i < reviewCount; i++) { + User randomStudent = students.get(random.nextInt(students.size())); + Lesson randomLesson = instructorLessons.get(random.nextInt(instructorLessons.size())); + + // 평점은 metadata의 rating 주변으로 생성 + double baseRating = metadata.getRating(); + double reviewRating = Math.max(1.0d, Math.min(5.0d, + baseRating + (random.nextDouble() - 0.5d) * 2)); // ±1점 범위 + + Review review = Review.builder() + .reviewer(randomStudent) + .reviewee(instructor) + .lesson(randomLesson) + .rating(reviewRating) + .content("좋은 레슨이었습니다! 추천해요.") + .image(random.nextBoolean() ? "https://example.com/review" + i + ".jpg" : null) + .build(); + + reviewRepository.save(review); + } + } + } + + //쿠폰 생성 메서드 + private List createCoupons() { + List coupons = new ArrayList<>(); + + // 다양한 쿠폰 생성 + String[] couponNames = { + "신규 회원 환영 쿠폰", + "여름 시즌 할인 쿠폰", + "헬스 전용 할인권", + "요가 클래스 특가 쿠폰", + "VIP 회원 전용 쿠폰", + "주말 특가 쿠폰", + "첫 레슨 체험 쿠폰", + "단체 할인 쿠폰", + "생일 축하 쿠폰", + "연말 감사 쿠폰" + }; + + CouponCategory[] categories = CouponCategory.values(); + CouponStatus[] statuses = {CouponStatus.ACTIVE, CouponStatus.ACTIVE, CouponStatus.INACTIVE}; // 대부분 활성화 + + for (int i = 0; i < couponNames.length; i++) { + LocalDateTime now = LocalDateTime.now(); + + // 발급 기간 설정 (현재부터 30일간) + LocalDateTime openAt = now.minusDays(random.nextInt(10)); // 이미 시작된 쿠폰들도 있게 + LocalDateTime closeAt = openAt.plusDays(30 + random.nextInt(30)); // 30-60일간 + + // 사용 기한 설정 (발급 마감일로부터 추가 30일) + LocalDateTime expirationDate = closeAt.plusDays(30); + + String discountPriceStr; + if (random.nextBoolean()) { + // 50% 확률로 고정 금액 할인 (1000-10000원) + int discountAmount = (random.nextInt(10) + 1) * 1000; + discountPriceStr = discountAmount + "원"; + } else { + // 50% 확률로 비율 할인 (5%-50%) + int discountPercent = (random.nextInt(10) + 1) * 5; // 5%, 10%, 15%, ..., 50% + discountPriceStr = discountPercent + "%"; + } + + Coupon coupon = Coupon.builder() + .name(couponNames[i]) + .expirationDate(expirationDate) + .discountPrice(discountPriceStr) // 1000-10000원 + .minOrderPrice(generateMinOrderPrice()) // 10000-50000원 + .status(statuses[random.nextInt(statuses.length)]) + .quantity(generateCouponQuantity()) // 50-500개 + .category(categories[random.nextInt(categories.length)]) + .openAt(openAt) + .closeAt(closeAt) + .build(); + + coupons.add(couponRepository.save(coupon)); + } + + log.info("쿠폰 {}개 생성 완료", coupons.size()); + return coupons; + } + + // 유저쿠폰 생성 메서드 (사용자들이 쿠폰을 발급받은 데이터) + private void createUserCoupons(List students, List coupons) { + List userCoupons = new ArrayList<>(); + + // 활성화된 쿠폰들만 필터링 + List activeCoupons = coupons.stream() + .filter(coupon -> coupon.getStatus() == CouponStatus.ACTIVE) + .toList(); + + if (activeCoupons.isEmpty()) { + log.info("활성화된 쿠폰이 없어서 유저쿠폰을 생성하지 않습니다."); + return; + } + + // 각 학생이 몇 개의 쿠폰을 가질지 결정 (0-5개) + for (User student : students) { + int couponCount = random.nextInt(6); // 0-5개 + + if (couponCount == 0) { + continue; // 쿠폰이 없는 사용자도 있게 + } + + // 중복 방지를 위한 Set + Set issuedCouponIds = new HashSet<>(); + + for (int i = 0; i < couponCount; i++) { + Coupon randomCoupon = activeCoupons.get(random.nextInt(activeCoupons.size())); + + // 이미 발급받은 쿠폰은 제외 + if (issuedCouponIds.contains(randomCoupon.getId())) { + continue; + } + + issuedCouponIds.add(randomCoupon.getId()); + + // 쿠폰 만료일 설정 (원본 쿠폰의 만료일과 동일) + LocalDateTime expirationDate = randomCoupon.getExpirationDate(); + + // UserCoupon 생성 (기본적으로 ACTIVE 상태로 생성됨) + UserCoupon userCoupon = new UserCoupon(student, randomCoupon, expirationDate); + + // 20% 확률로 이미 사용된 쿠폰으로 설정 + if (random.nextInt(100) < 20) { + userCoupon.use(); // 상태를 INACTIVE로 변경하고 useDate 설정 + } + + userCoupons.add(userCoupon); + } + } + + userCouponRepository.saveAll(userCoupons); + log.info("유저쿠폰 {}개 생성 완료", userCoupons.size()); + } + + // 최소 주문 금액 생성 (10000-50000원, 5000원 단위) + private Integer generateMinOrderPrice() { + return (random.nextInt(9) + 2) * 5000; // 10000-50000원 + } + + // 쿠폰 수량 생성 (50-500개, 50개 단위) + private Integer generateCouponQuantity() { + return (random.nextInt(10) + 1) * 50; // 50-500개 + } + + private int generateReviewCount() { + // 랭킹에 들어갈 강사들(20개 이상)과 그렇지 않은 강사들 섞어서 생성 + int rand = random.nextInt(100); + if (rand < 60) { // 60% 확률로 랭킹 대상 + return random.nextInt(50) + 20; // 20-69개 + } else { // 40% 확률로 랭킹 비대상 + return random.nextInt(20); // 0-19개 + } + } + + private double generateRating() { + // 3.0 ~ 5.0 사이의 평점 생성 (소수점 1자리) + double rating = 3.0d + random.nextDouble() * 2.0d; + return Math.round(rating * 10) / 10.0d; + } +}