DummyBigDataFactory.java

package qwerty.chaekit.global.init;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import qwerty.chaekit.domain.ebook.Ebook;
import qwerty.chaekit.domain.ebook.credit.usage.CreditUsageTransaction;
import qwerty.chaekit.domain.ebook.credit.usage.CreditUsageTransactionRepository;
import qwerty.chaekit.domain.ebook.credit.usage.CreditUsageTransactionType;
import qwerty.chaekit.domain.ebook.credit.wallet.CreditWallet;
import qwerty.chaekit.domain.ebook.credit.wallet.CreditWalletRepository;
import qwerty.chaekit.domain.ebook.purchase.EbookPurchase;
import qwerty.chaekit.domain.ebook.purchase.repository.EbookPurchaseRepository;
import qwerty.chaekit.domain.ebook.repository.EbookRepository;
import qwerty.chaekit.domain.group.ReadingGroup;
import qwerty.chaekit.domain.group.activity.Activity;
import qwerty.chaekit.domain.group.activity.repository.ActivityRepository;
import qwerty.chaekit.domain.group.repository.GroupRepository;
import qwerty.chaekit.domain.member.Member;
import qwerty.chaekit.domain.member.enums.Role;
import qwerty.chaekit.domain.member.publisher.PublisherProfile;
import qwerty.chaekit.domain.member.user.UserProfile;
import qwerty.chaekit.domain.member.user.UserProfileRepository;
import qwerty.chaekit.service.member.admin.AdminService;
import qwerty.chaekit.service.util.EntityFinder;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.stream.IntStream;

@Slf4j
@Component
@RequiredArgsConstructor
public class DummyBigDataFactory {
    private final GroupRepository groupRepository;
    private final UserProfileRepository userProfileRepository;
    private final EbookRepository ebookRepository;
    private final AdminService adminService;
    private final EntityFinder entityFinder;
    private final EbookPurchaseRepository ebookPurchaseRepository;
    private final CreditWalletRepository creditWalletRepository;
    private final ActivityRepository activityRepository;

    private final LocalDate startDate = LocalDate.of(2024, 6, 1);
    private final LocalDate endDate = LocalDate.of(2025, 5, 31);
    private final CreditUsageTransactionRepository creditUsageTransactionRepository;

    @Transactional
    public void generateDummyDataForTest() {
        // 1. 출판물 5권 조회 (더미 도서)
        // 출판사 ID는 관리자 서비스에서 가져옵니다.
        Long publisherId = adminService.getAdminPublisherId();
        PublisherProfile publisher = entityFinder.findPublisher(publisherId);
        Random random = new Random();
        List<Ebook> ebooks = ebookRepository.findAllByPublisher(publisher, PageRequest.of(0, 5))
                .stream()
                .toList();
        log.info("더미 도서 데이터가 {}개 조회되었습니다.", ebooks.size());
        ebooks.forEach(ebook -> ebook.resetViewCount(1000L + random.nextInt(2001)));

        // 2. 일반 사용자 100명 생성
        if (userProfileRepository.findByMember_Email("test1@dummy.com").isPresent()) {
            log.info("더미 사용자 데이터가 이미 존재합니다. 생성을 건너뜁니다.");
            return;
        }
        List<UserProfile> users = IntStream.range(0, 100)
                .mapToObj(i -> userProfileRepository.save(generateDummyUser("test" + i)))
                .toList();

        // 3. 사용자별 책 3권 무작위 구매
        CreditWallet savedWallet = creditWalletRepository.save(CreditWallet.builder()
                .user(users.get(0)) // 더미로 첫 번째 사용자에게 지갑 생성
                .build());
        savedWallet.addCredit(10000000L); // 더미 금액 추가

        List<EbookPurchase> purchases = new ArrayList<>();
        for (UserProfile user : users) {
            List<Ebook> randomBooks = getRandomSubset(ebooks, 3);
            for (Ebook book : randomBooks) {
                CreditUsageTransaction transaction = CreditUsageTransaction.builder()
                        .wallet(savedWallet) // 더미 금액
                        .transactionType(CreditUsageTransactionType.PURCHASE)
                        .creditAmount(book.getPrice()) // 책 가격
                        .build();
                creditUsageTransactionRepository.save(transaction);
                EbookPurchase ep = EbookPurchase.builder()
                        .ebook(book)
                        .user(user)
                        .transaction(transaction)
                        .build();
                ep.resetCreatedAt(biasedRandomDate());
                purchases.add(ep);
            }
        }
        ebookPurchaseRepository.saveAll(purchases);

        // 4. 사용자 중 30명을 랜덤으로 뽑아 모임 생성
        List<Activity> activities = new ArrayList<>();
        List<UserProfile> leaders = getRandomSubset(users, 30);
        for (UserProfile leader : leaders) {
            ReadingGroup group = groupRepository.save(ReadingGroup.builder()
                    .name(leader.getNickname() + "의 모임")
                    .description("더미 모임입니다.")
                    .groupLeader(leader)
                    .build());

            // 5. 모임장 본인이 구매한 책 중 1권으로 활동 생성
            List<Ebook> leaderBooks = getRandomSubset(ebooks, 3);
            for( Ebook selectedBook : leaderBooks) {
                Activity ac = activityRepository.save(Activity.builder()
                        .group(group)
                        .description("이 활동은 " + selectedBook.getTitle() + "을 읽는 것입니다.")
                        .book(selectedBook)
                        .startTime(LocalDate.now())
                        .endTime(LocalDate.now().plusWeeks(1))
                        .build());
                ac.resetCreatedAt(biasedRandomDate());
                activities.add(ac);
            }
        }
        activityRepository.saveAll(activities);
    }
    
    private UserProfile generateDummyUser(String username) {
        Member account = Member.builder()
                .email(username + "@dummy.com")
                .password("password") // 비밀번호는 실제로는 암호화되어야 합니다.
                .role(Role.ROLE_USER)
                .build();
        return UserProfile.builder()
                .member(account)
                .nickname(username)
                .build();
    }

    private <T> List<T> getRandomSubset(List<T> list, int count) {
        List<T> mutable = new ArrayList<>(list);
        Collections.shuffle(mutable);
        return mutable.stream().limit(count).toList();
    }
    
    private LocalDateTime biasedRandomDate() {
        long days = ChronoUnit.DAYS.between(startDate, endDate);

        // 선형 증가 분포를 위한 역변환 샘플링
        double u = Math.random(); // uniform(0, 1)
        double biased = 0.3 * u + 0.7 * Math.sqrt(u); // 오른쪽으로 치우친 분포

        long offsetDays = (long) (biased * days);
        return startDate.plusDays(offsetDays).atStartOfDay();
    }
    
}