Skip to content

Commit

Permalink
[BE] feat: 목 데이터 스케쥴러 구현(#725) (#821)
Browse files Browse the repository at this point in the history
* 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: 주석 수정
  • Loading branch information
BGuga authored Mar 31, 2024
1 parent c96da1c commit a2e3beb
Show file tree
Hide file tree
Showing 16 changed files with 754 additions and 0 deletions.
10 changes: 10 additions & 0 deletions backend/src/main/java/com/festago/config/SchedulerConfig.java
Original file line number Diff line number Diff line change
@@ -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 {

}
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
40 changes: 40 additions & 0 deletions backend/src/main/java/com/festago/mock/MockArtist.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
216 changes: 216 additions & 0 deletions backend/src/main/java/com/festago/mock/MockDataService.java
Original file line number Diff line number Diff line change
@@ -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<School> allSchool = schoolRepository.findAll();
List<Artist> 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<Artist> 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<Artist> makeRandomArtists(List<Artist> artists) {
List<Artist> randomArtists = new ArrayList<>(artists);
Collections.shuffle(randomArtists);
return new ArrayDeque<>(randomArtists);
}

/**
* 축제 기간 동안 축제를 채웁니다. 에를 들어 Festival 이 23~25일 이라면 23, 24, 25 날짜의 stage 를 생성합니다.
*/
private void makeStages(Long festivalId, Queue<Artist> 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<Artist> 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<Long> makeStageArtists(Queue<Artist> 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<Artist> makeStageArtistsByArtistCount(Queue<Artist> artists) {
if (artists.size() < STAGE_ARTIST_COUNT * 2) {
return makeDuplicateStageArtists(artists);
}
return makeUniqueStageArtists(artists);
}

private List<Artist> makeDuplicateStageArtists(Queue<Artist> artists) {
List<Artist> 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<Artist> makeUniqueStageArtists(Queue<Artist> artists) {
List<Artist> result = new ArrayList<>();
for (int i = 0; i < STAGE_ARTIST_COUNT; i++) {
Artist artist = artists.poll();
result.add(artist);
}
return result;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
21 changes: 21 additions & 0 deletions backend/src/main/java/com/festago/mock/MockScheduler.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
@@ -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<Artist, Long> {

}
Original file line number Diff line number Diff line change
@@ -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<FestivalQueryInfo, Long> {

}
Original file line number Diff line number Diff line change
@@ -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<Festival, Long> {

}
Original file line number Diff line number Diff line change
@@ -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<School, Long> {

}
Loading

0 comments on commit a2e3beb

Please sign in to comment.