From a2e3beb04587e27847242a7f27ffc73809adba90 Mon Sep 17 00:00:00 2001 From: Guga Date: Sun, 31 Mar 2024 17:30:29 +0900 Subject: [PATCH] =?UTF-8?q?[BE]=20feat:=20=EB=AA=A9=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=8A=A4=EC=BC=80=EC=A5=B4=EB=9F=AC=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84(#725)=20(#821)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 스케쥴러 등록 * feat: 앱 실행 시 학교와 축제 데이터를 초기화한다 * chore: 클래스 네이밍 변경 * feat: MockService 구현 * feat: 앱 시작시 스케쥴러 1회 실행 * feat: 대학 축제명 변경 * test: 축제 무대 검증 변경 및 스케쥴러 빈 등록 * chore: 축제 이름 변경 * refactor: Service 기반으로 mock 을 생성한다 * feat: MockDataService cursor 방식 변경 및 queryInfo에 대한 검증 추가 * chore: 개행 추가 * chore: 패키지 변경 * chore: MockDataService profile dev 설정 * refactor: 만약 초기 DB를 초기화하지 않았다면 축제 생성로직은 생략한다 * refactor: mock 전용 서비스, 레포지토리 구분 * chore: 비 상수 네이밍 적용 * refactor: 멀티 쓰레드에 안전한 random 사용 * refactor: 메서드 가독성 개선 및 주석 추가 * chore: 개행 제거 * refactor: 사용하지 않는 Repository 로직 제거 * feat: 만약 artist 가 부족할 경우 예외를 발생시키거나 * chore: 개행 추가 * chore: 주석 수정 --- .../com/festago/config/SchedulerConfig.java | 10 + .../mock/CommandLineAppStartupRunner.java | 22 ++ .../java/com/festago/mock/MockArtist.java | 40 +++ .../com/festago/mock/MockDataService.java | 216 ++++++++++++++ .../mock/MockFestivalDateGenerator.java | 10 + .../java/com/festago/mock/MockScheduler.java | 21 ++ .../mock/RandomMockFestivalDateGenerator.java | 31 ++ .../repository/ForMockArtistRepository.java | 8 + .../ForMockFestivalInfoRepository.java | 8 + .../repository/ForMockFestivalRepository.java | 8 + .../repository/ForMockSchoolRepository.java | 8 + .../ForMockStageArtistRepository.java | 8 + .../ForMockStageQueryInfoRepository.java | 8 + .../repository/ForMockStageRepository.java | 8 + .../mock/application/MockDataServiceTest.java | 280 ++++++++++++++++++ .../festago/mock/config/MockDataConfig.java | 68 +++++ 16 files changed, 754 insertions(+) create mode 100644 backend/src/main/java/com/festago/config/SchedulerConfig.java create mode 100644 backend/src/main/java/com/festago/mock/CommandLineAppStartupRunner.java create mode 100644 backend/src/main/java/com/festago/mock/MockArtist.java create mode 100644 backend/src/main/java/com/festago/mock/MockDataService.java create mode 100644 backend/src/main/java/com/festago/mock/MockFestivalDateGenerator.java create mode 100644 backend/src/main/java/com/festago/mock/MockScheduler.java create mode 100644 backend/src/main/java/com/festago/mock/RandomMockFestivalDateGenerator.java create mode 100644 backend/src/main/java/com/festago/mock/repository/ForMockArtistRepository.java create mode 100644 backend/src/main/java/com/festago/mock/repository/ForMockFestivalInfoRepository.java create mode 100644 backend/src/main/java/com/festago/mock/repository/ForMockFestivalRepository.java create mode 100644 backend/src/main/java/com/festago/mock/repository/ForMockSchoolRepository.java create mode 100644 backend/src/main/java/com/festago/mock/repository/ForMockStageArtistRepository.java create mode 100644 backend/src/main/java/com/festago/mock/repository/ForMockStageQueryInfoRepository.java create mode 100644 backend/src/main/java/com/festago/mock/repository/ForMockStageRepository.java create mode 100644 backend/src/test/java/com/festago/mock/application/MockDataServiceTest.java create mode 100644 backend/src/test/java/com/festago/mock/config/MockDataConfig.java diff --git a/backend/src/main/java/com/festago/config/SchedulerConfig.java b/backend/src/main/java/com/festago/config/SchedulerConfig.java new file mode 100644 index 000000000..3a496b0d8 --- /dev/null +++ b/backend/src/main/java/com/festago/config/SchedulerConfig.java @@ -0,0 +1,10 @@ +package com.festago.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; + +@EnableScheduling +@Configuration +public class SchedulerConfig { + +} diff --git a/backend/src/main/java/com/festago/mock/CommandLineAppStartupRunner.java b/backend/src/main/java/com/festago/mock/CommandLineAppStartupRunner.java new file mode 100644 index 000000000..cde40d7a4 --- /dev/null +++ b/backend/src/main/java/com/festago/mock/CommandLineAppStartupRunner.java @@ -0,0 +1,22 @@ +package com.festago.mock; + +import lombok.RequiredArgsConstructor; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Profile({"dev"}) +@Component +@RequiredArgsConstructor +public class CommandLineAppStartupRunner implements CommandLineRunner { + + private final MockDataService mockDataService; + private final MockScheduler mockScheduler; + + @Override + public void run(String... args) { + if (mockDataService.initialize()) { + mockScheduler.run(); + } + } +} diff --git a/backend/src/main/java/com/festago/mock/MockArtist.java b/backend/src/main/java/com/festago/mock/MockArtist.java new file mode 100644 index 000000000..ede1c5f65 --- /dev/null +++ b/backend/src/main/java/com/festago/mock/MockArtist.java @@ -0,0 +1,40 @@ +package com.festago.mock; + +public enum MockArtist { + 윤하("https://i.namu.wiki/i/GXgnzRzEe4tq0YmevyZdmLNTPmVK2fzRyZtiCMA-0QSO8vbau9tah4rgP3qNokmT33-El0fx2PauysBruWpTk8B6MLu73gDgVnC3tvFeBnm3Lwo9oRreHM6slID3ilDTyFjpV2eKMO80Jf5u5E920g.webp", + "https://i.namu.wiki/i/IBHythQ09eithIwC-Ix3hr0Sh-AohBVBavl0u-2WW28L6f16JM8F8Klm-0A6gDacgy4t7ATbto7wBc9xt5gwsj0IVG1y9EphSkJun-op4O9OGwvOvX8ES8aTUejOsKPFX5Iat_ubwgGWAYudo5q-Yw.webp"), + 뉴진스("https://i.namu.wiki/i/l1co7IV1KFVJ9HBgzxCXkMbgMfuZp_MJhjhqgB7e76DLuokabw6CNlltqr7HGzAMFqt42JfXF94Cflw5XdDuXTS2QkvomS7WYpiiJbuAn5MAjBxOA_zT93dsgyLO-gJXtV0JN-jEQ4tQ-MWtqbHJyA.webp", + "https://i.namu.wiki/i/j5Fs-OjRcQsjfrrFFUVumAWauv-47tj5WPrfyIcCMuBrV5UeStJwaFK17HKcaKxvME2NVpo5PuxVgRpED1huULNxCYBydqsOs-HCLRD-kMztnZdaMJJvi1VefVB1RN0MnwMdxS7xKzxJa11qem0LMg.svg"), + 아이유("https://i.namu.wiki/i/k-to3_lfqjjcdnXtWMu3aLtZAArBM1nDpDP6cCWz5iJYm3HjJZ3b7i2H-4-KFSkQ6HOeftXIilMOQXvkdp83hu1FdBv5GE_PyYuacNUSygQ2cnT8vfNHqQVUReYdEYY3ob1BWoyGBE6BQRaHmnGPLw.webp", + "https://i.namu.wiki/i/bJDL9DWmKsrHvvMcLOSKMlVv_E62CX6brjhiddhFuLrGPVYN6-bYcJxUHnE_KP04Ok-8PqezYob8OCepRWFBw6CTDE5Jvde2iJOZEqGgYVt6Gdbub0s9pBIOqzI1DQYZvJbAezbh-8xns_ZugPW68g.webp"), + 방탄소년단( + "https://i.namu.wiki/i/EvnNG2DchyHHYmFtyWWzVHhPKkURdc6kdoiRVYisSKHE6BDE8itzfhfYvIMdoX0-6wvum0UgELIowRGR6cuwfNsR1OHrLamq-Rpg0F4XzFMSJHJ_xchPwFBBurNR45kOUYk2ueOKasd-xZ0g9Z14dg.webp", + "https://i.namu.wiki/i/PmFR4AjifhcmdLRXQUPPce9Z7BXVWc6mVX4N22fPUKOzK5ZfjNTfo9e1M7HPa2jiEmG_tuhm43aJMGWLylyQqw.svg"), + 푸우("https://i.namu.wiki/i/aj4JXZR4P-ZiY6Gf0EsoPMK3XFHHsSmx0b8ysKnUDpEd0ET1BVQHZIEAGvZGHCNJrn7Nkz7N5zeYzKh3WPSTGdCdOPpj1LnlAceeLWTmMSsiXvl2fyGaEZfRjm7i6DiBTW6_7pDqIRCWfYRQFKUsdg.webp", + "https://i.namu.wiki/i/_EIBF52MTqZAj5wtmY86jsU4fs7aYsdns4guDgLKYWQGoauVdCyZZiFcxc5qI92HxTUiWRRRrK0qk4Ot0qCRGpIc1GUTjUaYz1Y20IKkDuIo9472InXbpNMxcsE8PmP-taYj-7-Ql3_P557yYOk3EA.webp"), + 장범준("https://i.namu.wiki/i/6VAPyai_C0lBvsGytiMDu3moDOzS9UH9TDHqxzkjPWFymhQV-vcyH4q884nf1KKH2lVzLqMndBnCOTlUh4ZJE6QeB1oUQEH23d_FwMa3CFsyj8mkn3nG2DQMmJ31TN0cvCrxk6II8-IWq6C-d883zA.webp", + "https://i.namu.wiki/i/WPzIZvkNW_-q9UZmEHLKMtrvAQ2g2Oo7MwbrbzWnWEkRYYAdc2cyougS-n8-BwWsLuo3knt9aaHYEGiyhd7jtg.webp"), + 세븐틴("https://i.namu.wiki/i/55JrvWZKaTo_Vik4Wim6-PfXiEmWqwYnAwL5_KmNg9FWiM31U5VV-lmMl76TgtON5hpGP9dIEBhub70rAQvbvOVWt7dQ6GLqvrnpbQSV3Vr1vEKRXjk_RSqCpz5a_7nupUqhB8sBJExEDHf7WGKQDw.webp", + "https://i.namu.wiki/i/l4c_545au41xOK_9XRJyDh1PoNU1k9v-T5NF1UhLHxgCBQJzahV-ra8kP87FLmVFhey_OaWJcrBDJs0RmqbBB3lziiAgbM9lkDUAkENQBv4GweS2MSglXGGno9XQfnzisf5e-Z7185_U4jTqIqTiQA.svg"), + 아이들("https://i.namu.wiki/i/LTu_7r5vrTyZgjGb0pV79BoSI_CZr3hwLMnj2s1-ShPb-A07Nc0Gh_rGn8dic1_JwcJlB-pnSunyqmmIP-UhKRw33PlPO5GECFE2u4I5EtKIXN3c5u8_Wln6U22-Ofyjf90PxLLG1BLQziOoQ0d-pg.webp", + "https://i.namu.wiki/i/KwThwv_MdMM3O7mCr-WyXHXCZhfwKtLgAof5i-wIkkgp10izoSGyTKwCgMgBcoAaIP7VocBS-D36nHI6pkiPy3E8ncWJqsqrghC8bwoaM5dOEs32E9QSxk12CZUKCzGg9AM1bJivIuBBzFBpuc5JBg.webp"), + 에스파("https://i.namu.wiki/i/KJ5Gpz42djU6ZUsFKnkAnpMS1zRFxUOuqzt9plzbjV_mkFlruZcDULsfEjpVw-2vxjsSKbcGflPlOThHE1DgzST-hnm9jmxPqdPMExPkqH_71ZMF6jhQVfQX6QuNZw3Bz0EZ4C1sO5vpZ-OJNfvTyg.webp", + "https://i.namu.wiki/i/yAfAHme6H-HfWWQCvNAje-KInl_XM-xzRHOUmUxvRxh-HLbzk8KbG6zmD9qQXfUAeCenhHM5whZJ2nhQk0lanzT1LVja3BEQCVk1yPWABxy4NygdaLGyNpiRZTwVFkhD_PnCcESdUQ7-oEtK0YptsQ.webp"), + ; + + private final String profileImage; + private final String backgroundImageUrl; + + MockArtist(String profileImage, String backgroundImageUrl) { + this.profileImage = profileImage; + this.backgroundImageUrl = backgroundImageUrl; + } + + public String getProfileImage() { + return profileImage; + } + + public String getBackgroundImageUrl() { + return backgroundImageUrl; + } +} diff --git a/backend/src/main/java/com/festago/mock/MockDataService.java b/backend/src/main/java/com/festago/mock/MockDataService.java new file mode 100644 index 000000000..81081a7cc --- /dev/null +++ b/backend/src/main/java/com/festago/mock/MockDataService.java @@ -0,0 +1,216 @@ +package com.festago.mock; + +import com.festago.artist.application.ArtistCommandService; +import com.festago.artist.domain.Artist; +import com.festago.artist.dto.command.ArtistCreateCommand; +import com.festago.common.exception.ErrorCode; +import com.festago.common.exception.NotFoundException; +import com.festago.festival.application.command.FestivalCommandFacadeService; +import com.festago.festival.domain.Festival; +import com.festago.festival.dto.command.FestivalCreateCommand; +import com.festago.mock.repository.ForMockArtistRepository; +import com.festago.mock.repository.ForMockFestivalRepository; +import com.festago.mock.repository.ForMockSchoolRepository; +import com.festago.school.application.SchoolCommandService; +import com.festago.school.domain.School; +import com.festago.school.domain.SchoolRegion; +import com.festago.school.dto.SchoolCreateCommand; +import com.festago.stage.application.command.StageCommandFacadeService; +import com.festago.stage.dto.command.StageCreateCommand; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.atomic.AtomicLong; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Profile({"dev"}) +@Service +@Transactional +@RequiredArgsConstructor +public class MockDataService { + + public static final int STAGE_ARTIST_COUNT = 3; + private static final AtomicLong festivalSequence = new AtomicLong(); + private static final int STAGE_START_HOUR = 19; + private static final int SCHOOL_PER_REGION = 3; + private static final int DATE_OFFSET = 1; + + private final MockFestivalDateGenerator mockFestivalDateGenerator; + private final ForMockSchoolRepository schoolRepository; + private final ForMockArtistRepository artistRepository; + private final ForMockFestivalRepository festivalRepository; + private final FestivalCommandFacadeService festivalCommandFacadeService; + private final StageCommandFacadeService stageCommandFacadeService; + private final ArtistCommandService artistCommandService; + private final SchoolCommandService schoolCommandService; + + public boolean initialize() { + if (alreadyInitialized()) { + return false; + } + initializeData(); + return true; + } + + private boolean alreadyInitialized() { + return !schoolRepository.findAll().isEmpty(); + } + + private void initializeData() { + initializeSchool(); + initializeArtist(); + } + + private void initializeSchool() { + for (SchoolRegion schoolRegion : SchoolRegion.values()) { + if (SchoolRegion.ANY.equals(schoolRegion)) { + continue; + } + makeRegionSchools(schoolRegion); + } + } + + /** + * 각 지역 별로 3개의 학교를 만듭니다. ex) 서울1대학교 서울2대학교 서울3대학교 + */ + private void makeRegionSchools(SchoolRegion schoolRegion) { + for (int i = 0; i < SCHOOL_PER_REGION; i++) { + String schoolName = String.format("%s%d대학교", schoolRegion.name(), i + 1); + String schoolEmail = String.format("%s%d.com", schoolRegion.name(), i + 1); + crateSchool(schoolRegion, schoolName, schoolEmail); + } + } + + private void crateSchool(SchoolRegion schoolRegion, String schoolName, String schoolEmail) { + schoolCommandService.createSchool(new SchoolCreateCommand( + schoolName, + schoolEmail, + schoolRegion, + null, + null + ) + ); + } + + private void initializeArtist() { + for (MockArtist artist : MockArtist.values()) { + artistCommandService.save(new ArtistCreateCommand( + artist.name(), + artist.getProfileImage(), + artist.getBackgroundImageUrl() + ) + ); + } + } + + public void makeMockFestivals(int availableFestivalDuration) { + List allSchool = schoolRepository.findAll(); + List allArtist = artistRepository.findAll(); + int artistSize = allArtist.size(); + if (STAGE_ARTIST_COUNT > artistSize) { + throw new IllegalArgumentException( + String.format("공연을 구성하기 위한 아티스트의 최소 수를 만족하지 못합니다 최소 수 : %d 현재 수 : %d", STAGE_ARTIST_COUNT, artistSize)); + } + for (School school : allSchool) { + makeFestival(availableFestivalDuration, school, allArtist); + } + } + + /** + * 현재 날짜 + 입력받은 축제 기간 안의 기간을 갖는 축제를 생성합니다. 이때 하나의 축제에 중복된 아티스트가 포함되지 않기 위해서 makeRandomArtists 라는 메서드를 통해 섞인 Artist + * 들의 큐가 생성됩니다. + */ + private void makeFestival(int availableFestivalDuration, School school, List artists) { + LocalDate now = LocalDate.now(); + LocalDate startDate = mockFestivalDateGenerator.makeRandomStartDate(availableFestivalDuration, now); + LocalDate endDate = mockFestivalDateGenerator.makeRandomEndDate(availableFestivalDuration, now, startDate); + + Long newFestivalId = festivalCommandFacadeService.createFestival(new FestivalCreateCommand( + school.getName() + "축제" + festivalSequence.incrementAndGet(), + startDate, + endDate, + "https://picsum.photos/536/354", + school.getId() + )); + + makeStages(newFestivalId, makeRandomArtists(artists)); + } + + private Queue makeRandomArtists(List artists) { + List randomArtists = new ArrayList<>(artists); + Collections.shuffle(randomArtists); + return new ArrayDeque<>(randomArtists); + } + + /** + * 축제 기간 동안 축제를 채웁니다. 에를 들어 Festival 이 23~25일 이라면 23, 24, 25 날짜의 stage 를 생성합니다. + */ + private void makeStages(Long festivalId, Queue artists) { + Festival festival = festivalRepository.findById(festivalId) + .orElseThrow(() -> new NotFoundException(ErrorCode.FESTIVAL_NOT_FOUND)); + LocalDate endDate = festival.getEndDate(); + LocalDate startDate = festival.getStartDate(); + startDate.datesUntil(endDate.plusDays(DATE_OFFSET)) + .forEach(localDate -> makeStage(festival, artists, localDate)); + } + + /** + * 실질적으로 무대를 만드는 부분으로 이때 하나의 stage 는 랜덤한 아티스트 3명을 갖도록 만듭니다. + * 축제 별로 생성되는 queue 에서 poll 을 통해 stageArtist 를 결정하기 때문에 같은 축제에서 아티스트는 중복되지 않습니다. + */ + private void makeStage(Festival festival, Queue artists, LocalDate localDate) { + LocalDateTime startTime = localDate.atTime(STAGE_START_HOUR, 0); + stageCommandFacadeService.createStage(new StageCreateCommand( + festival.getId(), + startTime, + startTime.minusDays(1L), + makeStageArtists(artists) + )); + } + + private List makeStageArtists(Queue artists) { + return makeStageArtistsByArtistCount(artists).stream() + .map(Artist::getId) + .toList(); + } + + /** + * Stage 는 생성 제약 조건에 의해서 무조건 다른 아티스트로 구성해야합니다. + * 만약 STAGE_ARTIST_COUNT * 2 값보다 큐에 artist 가 작게 들어있으면 poll 연산 이후 artist 는 STAGE_ARTIST_COUNT 보다 적게 들어있습니다. + * 예를 들어 STAGE_ARTIST_COUNT = 3 일떄 6개의 아티스트에 대해서 poll 를 한다면 3개가 남아 나머지 3개로 중복 없는Stage 를 구성할 수 있지만 + * 5개의 아티스트에 대해서 poll 한 후에 2개의 artist 로는 Stage 에 중복이 생길 수 밖에 없습니다. + * 따라서 STAGE_ARTIST_COUNT * 2 artists 가 크다면 poll, 아닐 경우 poll 이후 다시 insert 해주는 로직을 진행합니다. + */ + private List makeStageArtistsByArtistCount(Queue artists) { + if (artists.size() < STAGE_ARTIST_COUNT * 2) { + return makeDuplicateStageArtists(artists); + } + return makeUniqueStageArtists(artists); + } + + private List makeDuplicateStageArtists(Queue artists) { + List result = new ArrayList<>(); + for (int i = 0; i < STAGE_ARTIST_COUNT; i++) { + Artist artist = artists.poll(); + result.add(artist); + artists.add(artist); + } + return result; + } + + private List makeUniqueStageArtists(Queue artists) { + List result = new ArrayList<>(); + for (int i = 0; i < STAGE_ARTIST_COUNT; i++) { + Artist artist = artists.poll(); + result.add(artist); + } + return result; + } +} diff --git a/backend/src/main/java/com/festago/mock/MockFestivalDateGenerator.java b/backend/src/main/java/com/festago/mock/MockFestivalDateGenerator.java new file mode 100644 index 000000000..51b72e243 --- /dev/null +++ b/backend/src/main/java/com/festago/mock/MockFestivalDateGenerator.java @@ -0,0 +1,10 @@ +package com.festago.mock; + +import java.time.LocalDate; + +public interface MockFestivalDateGenerator { + + LocalDate makeRandomStartDate(int festivalDuration, LocalDate now); + + LocalDate makeRandomEndDate(int festivalDuration, LocalDate now, LocalDate startDate); +} diff --git a/backend/src/main/java/com/festago/mock/MockScheduler.java b/backend/src/main/java/com/festago/mock/MockScheduler.java new file mode 100644 index 000000000..98d171068 --- /dev/null +++ b/backend/src/main/java/com/festago/mock/MockScheduler.java @@ -0,0 +1,21 @@ +package com.festago.mock; + +import java.util.concurrent.TimeUnit; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Profile; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Profile("dev") +@Component +@RequiredArgsConstructor +public class MockScheduler { + + private static final long SCHEDULER_CYCLE = 7; + private final MockDataService mockDataService; + + @Scheduled(fixedDelay = SCHEDULER_CYCLE, timeUnit = TimeUnit.DAYS) + public void run() { + mockDataService.makeMockFestivals((int) SCHEDULER_CYCLE); + } +} diff --git a/backend/src/main/java/com/festago/mock/RandomMockFestivalDateGenerator.java b/backend/src/main/java/com/festago/mock/RandomMockFestivalDateGenerator.java new file mode 100644 index 000000000..7ec66a5b5 --- /dev/null +++ b/backend/src/main/java/com/festago/mock/RandomMockFestivalDateGenerator.java @@ -0,0 +1,31 @@ +package com.festago.mock; + +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Profile("dev") +@Component +public class RandomMockFestivalDateGenerator implements MockFestivalDateGenerator { + + private static final int COUNT_FIRST_DAY_AS_DURATION_ONE = 1; + private static final int RANDOM_OFFSET = 1; + private static final int MAX_END_DATE_FROM_START_DATE = 2; + private final Random random = ThreadLocalRandom.current(); + + @Override + public LocalDate makeRandomStartDate(int festivalDuration, LocalDate now) { + return now.plusDays(random.nextInt(festivalDuration)); + } + + @Override + public LocalDate makeRandomEndDate(int festivalDuration, LocalDate now, LocalDate startDate) { + long timeUntilFestivalStart = startDate.until(now, ChronoUnit.DAYS); + long maxAvailableEndDateFromStartDate = festivalDuration - (timeUntilFestivalStart + COUNT_FIRST_DAY_AS_DURATION_ONE); + int randomEndDate = random.nextInt((int) (maxAvailableEndDateFromStartDate + RANDOM_OFFSET)); + return startDate.plusDays(Math.min(randomEndDate, MAX_END_DATE_FROM_START_DATE)); + } +} diff --git a/backend/src/main/java/com/festago/mock/repository/ForMockArtistRepository.java b/backend/src/main/java/com/festago/mock/repository/ForMockArtistRepository.java new file mode 100644 index 000000000..15970e2c6 --- /dev/null +++ b/backend/src/main/java/com/festago/mock/repository/ForMockArtistRepository.java @@ -0,0 +1,8 @@ +package com.festago.mock.repository; + +import com.festago.artist.domain.Artist; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ForMockArtistRepository extends JpaRepository { + +} diff --git a/backend/src/main/java/com/festago/mock/repository/ForMockFestivalInfoRepository.java b/backend/src/main/java/com/festago/mock/repository/ForMockFestivalInfoRepository.java new file mode 100644 index 000000000..37f78f296 --- /dev/null +++ b/backend/src/main/java/com/festago/mock/repository/ForMockFestivalInfoRepository.java @@ -0,0 +1,8 @@ +package com.festago.mock.repository; + +import com.festago.festival.domain.FestivalQueryInfo; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ForMockFestivalInfoRepository extends JpaRepository { + +} diff --git a/backend/src/main/java/com/festago/mock/repository/ForMockFestivalRepository.java b/backend/src/main/java/com/festago/mock/repository/ForMockFestivalRepository.java new file mode 100644 index 000000000..a18224397 --- /dev/null +++ b/backend/src/main/java/com/festago/mock/repository/ForMockFestivalRepository.java @@ -0,0 +1,8 @@ +package com.festago.mock.repository; + +import com.festago.festival.domain.Festival; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ForMockFestivalRepository extends JpaRepository { + +} diff --git a/backend/src/main/java/com/festago/mock/repository/ForMockSchoolRepository.java b/backend/src/main/java/com/festago/mock/repository/ForMockSchoolRepository.java new file mode 100644 index 000000000..312f55dbc --- /dev/null +++ b/backend/src/main/java/com/festago/mock/repository/ForMockSchoolRepository.java @@ -0,0 +1,8 @@ +package com.festago.mock.repository; + +import com.festago.school.domain.School; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ForMockSchoolRepository extends JpaRepository { + +} diff --git a/backend/src/main/java/com/festago/mock/repository/ForMockStageArtistRepository.java b/backend/src/main/java/com/festago/mock/repository/ForMockStageArtistRepository.java new file mode 100644 index 000000000..cc666e420 --- /dev/null +++ b/backend/src/main/java/com/festago/mock/repository/ForMockStageArtistRepository.java @@ -0,0 +1,8 @@ +package com.festago.mock.repository; + +import com.festago.stage.domain.StageArtist; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ForMockStageArtistRepository extends JpaRepository { + +} diff --git a/backend/src/main/java/com/festago/mock/repository/ForMockStageQueryInfoRepository.java b/backend/src/main/java/com/festago/mock/repository/ForMockStageQueryInfoRepository.java new file mode 100644 index 000000000..6a6480c63 --- /dev/null +++ b/backend/src/main/java/com/festago/mock/repository/ForMockStageQueryInfoRepository.java @@ -0,0 +1,8 @@ +package com.festago.mock.repository; + +import com.festago.stage.domain.StageQueryInfo; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ForMockStageQueryInfoRepository extends JpaRepository { + +} diff --git a/backend/src/main/java/com/festago/mock/repository/ForMockStageRepository.java b/backend/src/main/java/com/festago/mock/repository/ForMockStageRepository.java new file mode 100644 index 000000000..e606c76d5 --- /dev/null +++ b/backend/src/main/java/com/festago/mock/repository/ForMockStageRepository.java @@ -0,0 +1,8 @@ +package com.festago.mock.repository; + +import com.festago.stage.domain.Stage; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ForMockStageRepository extends JpaRepository { + +} diff --git a/backend/src/test/java/com/festago/mock/application/MockDataServiceTest.java b/backend/src/test/java/com/festago/mock/application/MockDataServiceTest.java new file mode 100644 index 000000000..7acf0249c --- /dev/null +++ b/backend/src/test/java/com/festago/mock/application/MockDataServiceTest.java @@ -0,0 +1,280 @@ +package com.festago.mock.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doReturn; + +import com.festago.artist.domain.Artist; +import com.festago.festival.domain.Festival; +import com.festago.festival.domain.FestivalQueryInfo; +import com.festago.mock.MockArtist; +import com.festago.mock.MockDataService; +import com.festago.mock.MockFestivalDateGenerator; +import com.festago.mock.config.MockDataConfig; +import com.festago.mock.repository.ForMockArtistRepository; +import com.festago.mock.repository.ForMockFestivalInfoRepository; +import com.festago.mock.repository.ForMockFestivalRepository; +import com.festago.mock.repository.ForMockSchoolRepository; +import com.festago.mock.repository.ForMockStageArtistRepository; +import com.festago.mock.repository.ForMockStageQueryInfoRepository; +import com.festago.mock.repository.ForMockStageRepository; +import com.festago.school.domain.School; +import com.festago.school.domain.SchoolRegion; +import com.festago.stage.domain.Stage; +import com.festago.stage.domain.StageArtist; +import com.festago.stage.domain.StageQueryInfo; +import com.festago.support.ApplicationIntegrationTest; +import com.festago.support.fixture.SchoolFixture; +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.stream.Collectors; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.context.annotation.Import; +import org.springframework.transaction.annotation.Transactional; + +@DisplayNameGeneration(ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +@Import(MockDataConfig.class) +class MockDataServiceTest extends ApplicationIntegrationTest { + + private static final int INCLUDE_FIRST_DATE = 1; + @Autowired + ForMockArtistRepository artistRepository; + + @Autowired + ForMockSchoolRepository schoolRepository; + + @Autowired + ForMockFestivalRepository festivalRepository; + + @Autowired + ForMockStageRepository stageRepository; + + @Autowired + ForMockStageArtistRepository stageArtistRepository; + + @Autowired + ForMockStageQueryInfoRepository stageQueryInfoRepository; + + @Autowired + ForMockFestivalInfoRepository festivalInfoRepository; + + @SpyBean + MockFestivalDateGenerator mockFestivalDateGenerator; + + @Autowired + MockDataService mockDataService; + + @Nested + class 목_데이터_초기화는 { + + @Test + void 만약_하나의_학교라도_존재하면_초기화_된_상태로_판단한다() { + // given + schoolRepository.save(SchoolFixture.builder().build()); + + // when + mockDataService.initialize(); + List allSchool = schoolRepository.findAll(); + + // then + assertThat(allSchool).hasSize(1); + } + + @Test + void 학교가_없다면_ANY_를_제외한_지역_곱하기_3개만큼의_학교와_MOCK_ARTIST_만큼의_아티스트를_생성한다() { + // given + mockDataService.initialize(); + int expectGeneratedSchoolSize = (SchoolRegion.values().length - 1) * 3; + int expectArtistSize = MockArtist.values().length; + + // when + List allSchool = schoolRepository.findAll(); + List allArtist = artistRepository.findAll(); + + // then + assertSoftly(softly -> { + softly.assertThat(allSchool).hasSize(expectGeneratedSchoolSize); + softly.assertThat(allArtist).hasSize(expectArtistSize); + }); + } + } + + @Nested + class 목_축제_생성_요청은 { + + @Test + @Transactional + void 존재하는_학교수_만큼의_축제를_만들어_낸다() { + // given + mockDataService.initialize(); + List allSchool = schoolRepository.findAll(); + List beforeFestivals = festivalRepository.findAll(); + + // when + mockDataService.makeMockFestivals(7); + List afterFestivals = festivalRepository.findAll(); + List festivalSchools = afterFestivals.stream() + .map(festival -> festival.getSchool()) + .toList(); + + // then + assertSoftly(softly -> { + softly.assertThat(beforeFestivals).hasSize(0); + softly.assertThat(afterFestivals).hasSize(allSchool.size()); + softly.assertThat(festivalSchools).containsAll(allSchool); + }); + } + + @Test + void 쿼리_최적화_정보들을_생성한다() { + // given + mockDataService.initialize(); + mockDataService.makeMockFestivals(7); + + // when + List stageQueryInfos = stageQueryInfoRepository.findAll(); + List festivalQueryInfos = festivalInfoRepository.findAll(); + + // then + assertSoftly(softly -> { + assertThat(stageQueryInfos).isNotEmpty(); + assertThat(festivalQueryInfos).isNotEmpty(); + }); + } + + @Test + void 생성된_모든_축제는_기간은_전달_받은_기간_이내_이다() { + // given + mockDataService.initialize(); + mockDataService.makeMockFestivals(1); + + // when + List allFestival = festivalRepository.findAll(); + + // then + assertThat(allFestival).allMatch( + festival -> festival.getStartDate().until(festival.getStartDate(), ChronoUnit.DAYS) == 0); + } + + @Test + @Transactional + void 무대는_생성된_축제_기간동안_전부_존재한다() { + // given + LocalDate now = LocalDate.now(); + + mockDataService.initialize(); + mockDataService.makeMockFestivals(7); + List allFestival = festivalRepository.findAll(); + + // when + List allStage = stageRepository.findAll(); + Map> stageByFestival = allStage.stream() + .collect(Collectors.groupingBy(Stage::getFestival)); + + // then + assertThat(stageByFestival.entrySet()).allMatch(festivalListEntry -> { + Festival festival = festivalListEntry.getKey(); + long festivalDuration = + festival.getStartDate().until(festival.getEndDate(), ChronoUnit.DAYS) + INCLUDE_FIRST_DATE; + return festivalListEntry.getValue().size() == festivalDuration; + }); + } + + @Test + void 같은_축제_속_무대_아티스트_들은_겹치지_않는다() { + // given + mockDataService.initialize(); + mockDataService.makeMockFestivals(7); + + List stageArtists = stageArtistRepository.findAll(); + List allStage = stageRepository.findAll(); + + Map> stageArtistByStageId = stageArtists.stream() + .collect(Collectors.groupingBy(StageArtist::getStageId)); + Map> stageByFestival = allStage.stream() + .collect(Collectors.groupingBy(Stage::getFestival)); + + // when + Map> stageArtistsByFestival = new HashMap<>(); + + stageByFestival.forEach((festival, stages) -> { + List artistsForFestival = stages.stream() + .map(stage -> stageArtistByStageId.getOrDefault(stage.getId(), Collections.emptyList())) + .flatMap(List::stream) + .collect(Collectors.toList()); + + stageArtistsByFestival.put(festival, artistsForFestival); + }); + + // then + assertThat(stageArtistsByFestival.keySet()) + .allMatch(festival -> { + List stageArtistsValue = stageArtistsByFestival.get(festival); + long uniqueStageArtists = stageArtistsValue.stream() + .map(stageArtist -> stageArtist.getArtistId()) + .distinct() + .count(); + return stageArtistsValue.size() == uniqueStageArtists; + }); + } + + @Test + void 만약_아티스트가_중복없이_무대를_구성하기_부족하다면_중복을_허용한다() { + + // given + LocalDate now = LocalDate.now(); + int availableUniqueStageCount = MockArtist.values().length / MockDataService.STAGE_ARTIST_COUNT; + doReturn(now) + .when(mockFestivalDateGenerator) + .makeRandomStartDate(anyInt(), any(LocalDate.class)); + doReturn(now.plusDays(availableUniqueStageCount + 1)) + .when(mockFestivalDateGenerator) + .makeRandomEndDate(anyInt(), any(LocalDate.class), any(LocalDate.class)); + + mockDataService.initialize(); + mockDataService.makeMockFestivals(10); + + List stageArtists = stageArtistRepository.findAll(); + List allStage = stageRepository.findAll(); + + Map> stageArtistByStageId = stageArtists.stream() + .collect(Collectors.groupingBy(StageArtist::getStageId)); + Map> stageByFestival = allStage.stream() + .collect(Collectors.groupingBy(Stage::getFestival)); + + // when + Map> stageArtistsByFestival = new HashMap<>(); + + stageByFestival.forEach((festival, stages) -> { + List artistsForFestival = stages.stream() + .map(stage -> stageArtistByStageId.getOrDefault(stage.getId(), Collections.emptyList())) + .flatMap(List::stream) + .collect(Collectors.toList()); + + stageArtistsByFestival.put(festival, artistsForFestival); + }); + + List artistIds = stageArtistsByFestival.values().stream() + .flatMap(List::stream) + .map(stageArtist -> stageArtist.getArtistId()) + .toList(); + + // then + assertThat(artistIds.size()).isNotEqualTo(new HashSet<>(artistIds).size()); + } + } +} diff --git a/backend/src/test/java/com/festago/mock/config/MockDataConfig.java b/backend/src/test/java/com/festago/mock/config/MockDataConfig.java new file mode 100644 index 000000000..ba4632540 --- /dev/null +++ b/backend/src/test/java/com/festago/mock/config/MockDataConfig.java @@ -0,0 +1,68 @@ +package com.festago.mock.config; + +import com.festago.artist.application.ArtistCommandService; +import com.festago.artist.repository.ArtistRepository; +import com.festago.festival.application.command.FestivalCommandFacadeService; +import com.festago.festival.repository.FestivalRepository; +import com.festago.mock.CommandLineAppStartupRunner; +import com.festago.mock.MockScheduler; +import com.festago.mock.MockFestivalDateGenerator; +import com.festago.mock.MockDataService; +import com.festago.mock.RandomMockFestivalDateGenerator; +import com.festago.mock.repository.ForMockArtistRepository; +import com.festago.mock.repository.ForMockFestivalRepository; +import com.festago.mock.repository.ForMockSchoolRepository; +import com.festago.school.application.SchoolCommandService; +import com.festago.school.repository.SchoolRepository; +import com.festago.stage.application.command.StageCommandFacadeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +@TestConfiguration +public class MockDataConfig { + + @Autowired + private ForMockSchoolRepository schoolRepository; + + @Autowired + private ForMockArtistRepository artistRepository; + + @Autowired + private ForMockFestivalRepository festivalRepository; + + @Autowired + private FestivalCommandFacadeService festivalCommandFacadeService; + + @Autowired + private StageCommandFacadeService stageCommandFacadeService; + + @Autowired + private ArtistCommandService artistCommandService; + + @Autowired + private SchoolCommandService schoolCommandService; + + + @Bean + public MockFestivalDateGenerator festivalDateGenerator() { + return new RandomMockFestivalDateGenerator(); + } + + @Bean + public MockDataService mockDataService() { + return new MockDataService(festivalDateGenerator(), schoolRepository, artistRepository, festivalRepository, + festivalCommandFacadeService, stageCommandFacadeService, artistCommandService, schoolCommandService); + } + + @Bean + public MockScheduler mockScheduler() { + return new MockScheduler(mockDataService()); + + } + + @Bean + public CommandLineAppStartupRunner commandLineAppStartupRunner() { + return new CommandLineAppStartupRunner(mockDataService(), mockScheduler()); + } +}