feat(dev): dev 프로파일 테스트 데이터 시더
스팟6·유저/강아지(견종·성향)·요일/시간대 분산 체크인 이력(자주오는견종용)· 현재 활성 체크인(메이트/AI, Redis)·한줄평 시드. idempotent(스팟 있으면 skip), 실패해도 기동 막지 않음(try/catch). prod 는 미실행이라 DB 깨끗. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
package com.dog.dognation.config;
|
||||
|
||||
import com.dog.dognation.domain.spot.SpotService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* dev 프로파일 전용 테스트 데이터 시더 (PostgreSQL).
|
||||
* 스팟/유저/강아지/성향, 요일·시간대에 분산된 체크인 이력(통계·자주오는견종용),
|
||||
* 현재 활성 체크인(메이트/AI), 한줄평을 채운다.
|
||||
* - idempotent: 스팟이 이미 있으면 건너뜀
|
||||
* - 시더 실패가 앱 기동을 막지 않도록 전체 try/catch
|
||||
* (prod 는 이 시더가 실행되지 않아 DB 가 깨끗하게 유지된다.)
|
||||
*/
|
||||
@Component
|
||||
@Profile("dev")
|
||||
@Order(100)
|
||||
public class DevSeedData implements CommandLineRunner {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(DevSeedData.class);
|
||||
|
||||
private final JdbcTemplate jdbc;
|
||||
private final SpotService spotService;
|
||||
|
||||
public DevSeedData(JdbcTemplate jdbc, SpotService spotService) {
|
||||
this.jdbc = jdbc;
|
||||
this.spotService = spotService;
|
||||
}
|
||||
|
||||
private record SpotDef(String name, String type, double lat, double lng, String[] pref) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(String... args) {
|
||||
try {
|
||||
Long spotCount = jdbc.queryForObject("SELECT count(*) FROM spots", Long.class);
|
||||
if (spotCount != null && spotCount > 0) {
|
||||
log.info("[dev seed] 스팟 {}개 이미 존재 — 시드 건너뜀", spotCount);
|
||||
return;
|
||||
}
|
||||
seed();
|
||||
} catch (Exception e) {
|
||||
log.warn("[dev seed] 실패(무시하고 기동): {}", e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private void seed() {
|
||||
Random rnd = new Random(20260711L);
|
||||
|
||||
// 1) 스팟 (스팟별 단골 견종 pref → 자주오는견종 패턴 형성)
|
||||
List<SpotDef> spotDefs = List.of(
|
||||
new SpotDef("여의도 한강공원", "PARK", 37.5285, 126.9327,
|
||||
new String[]{"말티즈", "푸들", "포메라니안", "비숑프리제"}),
|
||||
new SpotDef("서울숲 반려견 놀이터", "PLAYGROUND", 37.5445, 127.0374,
|
||||
new String[]{"리트리버", "웰시코기", "시바견", "보더콜리"}),
|
||||
new SpotDef("올림픽공원", "PARK", 37.5202, 127.1214,
|
||||
new String[]{"진돗개", "리트리버", "셰퍼드", "시바견"}),
|
||||
new SpotDef("양재천 산책로", "TRAIL", 37.4844, 127.0430,
|
||||
new String[]{"푸들", "시츄", "치와와", "말티즈"}),
|
||||
new SpotDef("경의선숲길", "PARK", 37.5590, 126.9250,
|
||||
new String[]{"비숑프리제", "포메라니안", "웰시코기", "푸들"}),
|
||||
new SpotDef("보라매공원", "PARK", 37.4926, 126.9200,
|
||||
new String[]{"진돗개", "리트리버", "시바견", "웰시코기"})
|
||||
);
|
||||
long[] spotIds = new long[spotDefs.size()];
|
||||
for (int i = 0; i < spotDefs.size(); i++) {
|
||||
SpotDef s = spotDefs.get(i);
|
||||
spotIds[i] = insertSpot(s.name(), s.type(), s.lat(), s.lng());
|
||||
}
|
||||
|
||||
// 2) 견종 → 크기
|
||||
Map<String, String> sizeByBreed = new HashMap<>();
|
||||
sizeByBreed.put("말티즈", "SMALL");
|
||||
sizeByBreed.put("푸들", "SMALL");
|
||||
sizeByBreed.put("포메라니안", "SMALL");
|
||||
sizeByBreed.put("치와와", "SMALL");
|
||||
sizeByBreed.put("시츄", "SMALL");
|
||||
sizeByBreed.put("비숑프리제", "SMALL");
|
||||
sizeByBreed.put("웰시코기", "MEDIUM");
|
||||
sizeByBreed.put("시바견", "MEDIUM");
|
||||
sizeByBreed.put("보더콜리", "MEDIUM");
|
||||
sizeByBreed.put("진돗개", "MEDIUM");
|
||||
sizeByBreed.put("리트리버", "LARGE");
|
||||
sizeByBreed.put("셰퍼드", "LARGE");
|
||||
String[] allBreeds = sizeByBreed.keySet().toArray(new String[0]);
|
||||
|
||||
String[] dogNames = {"뭉치", "초코", "콩이", "보리", "별이", "구름", "두부", "코코", "모카", "루비",
|
||||
"탄이", "감자", "밤이", "호두", "장군", "복실", "나비", "단비", "봄이", "여름"};
|
||||
List<Long> traitIds = jdbc.queryForList("SELECT id FROM trait_tags", Long.class);
|
||||
|
||||
// 3) 유저 + 강아지 (견종별 3~5마리) → 견종별 dogId 풀
|
||||
Map<String, List<Long>> dogsByBreed = new HashMap<>();
|
||||
Map<Long, Long> ownerByDog = new HashMap<>();
|
||||
List<Long> allUserIds = new ArrayList<>();
|
||||
int idx = 0;
|
||||
for (String breed : allBreeds) {
|
||||
int per = 3 + rnd.nextInt(3);
|
||||
for (int k = 0; k < per; k++) {
|
||||
String tier = idx % 6 == 0 ? "PREMIUM" : (idx % 6 == 1 ? "AD_FREE" : "FREE");
|
||||
long uid = insertUser("dev" + idx + "@dog.com", "이웃" + idx, tier);
|
||||
long dogId = insertDog(uid, dogNames[idx % dogNames.length], breed,
|
||||
sizeByBreed.get(breed), rnd.nextBoolean() ? "MALE" : "FEMALE");
|
||||
if (traitIds.size() >= 2) {
|
||||
int a = rnd.nextInt(traitIds.size());
|
||||
int b = (a + 1 + rnd.nextInt(traitIds.size() - 1)) % traitIds.size();
|
||||
linkTrait(dogId, traitIds.get(a));
|
||||
linkTrait(dogId, traitIds.get(b));
|
||||
}
|
||||
dogsByBreed.computeIfAbsent(breed, x -> new ArrayList<>()).add(dogId);
|
||||
ownerByDog.put(dogId, uid);
|
||||
allUserIds.add(uid);
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
|
||||
// 4) 체크인 이력 (통계용) — 최근 75일, 산책 시간대(6~21시) 분산 배치
|
||||
OffsetDateTime now = OffsetDateTime.now();
|
||||
List<Object[]> history = new ArrayList<>();
|
||||
for (int si = 0; si < spotIds.length; si++) {
|
||||
long spot = spotIds[si];
|
||||
String[] pref = spotDefs.get(si).pref();
|
||||
for (int d = 0; d < 75; d++) {
|
||||
int perDay = 4 + rnd.nextInt(5); // 하루 4~8건
|
||||
for (int k = 0; k < perDay; k++) {
|
||||
int hour = 6 + rnd.nextInt(16);
|
||||
OffsetDateTime t = now.minusDays(d)
|
||||
.withHour(hour).withMinute(rnd.nextInt(60)).withSecond(0).withNano(0);
|
||||
long dogId = pickDog(rnd, dogsByBreed, pref, allBreeds);
|
||||
history.add(new Object[]{spot, ownerByDog.get(dogId), dogId, t, t.plusHours(2)});
|
||||
}
|
||||
}
|
||||
}
|
||||
jdbc.batchUpdate(
|
||||
"INSERT INTO check_ins(spot_id, user_id, dog_id, checked_in_at, expires_at) VALUES (?, ?, ?, ?, ?)",
|
||||
history);
|
||||
|
||||
// 5) 현재 활성 체크인 (Redis + Postgres) — 스팟별 2~4마리. spotService.checkIn 이 Redis 도 채움
|
||||
for (int si = 0; si < spotIds.length; si++) {
|
||||
long spot = spotIds[si];
|
||||
String[] pref = spotDefs.get(si).pref();
|
||||
int n = 2 + rnd.nextInt(3);
|
||||
Set<Long> used = new HashSet<>();
|
||||
for (int k = 0; k < n; k++) {
|
||||
long dogId = pickDog(rnd, dogsByBreed, pref, allBreeds);
|
||||
if (!used.add(dogId)) {
|
||||
continue;
|
||||
}
|
||||
spotService.checkIn(spot, ownerByDog.get(dogId), dogId);
|
||||
}
|
||||
}
|
||||
|
||||
// 6) 한줄평
|
||||
String[] tags = {"물그릇 있음", "그늘 시원해요", "진드기 많아요", "배변봉투함 있음",
|
||||
"야간 조명 밝음", "잔디 상태 좋음", "주차 편함", "사람 많아요"};
|
||||
for (long spot : spotIds) {
|
||||
int rn = 3 + rnd.nextInt(3);
|
||||
Set<String> usedTags = new HashSet<>();
|
||||
for (int k = 0; k < rn; k++) {
|
||||
String tag = tags[rnd.nextInt(tags.length)];
|
||||
if (!usedTags.add(tag)) {
|
||||
continue;
|
||||
}
|
||||
long uid = allUserIds.get(rnd.nextInt(allUserIds.size()));
|
||||
spotService.addReview(spot, uid, tag, null);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("[dev seed] 완료 — 스팟 {}, 강아지 {}, 체크인이력 {}건",
|
||||
spotIds.length, ownerByDog.size(), history.size());
|
||||
}
|
||||
|
||||
/** pref 견종에서 75% 확률로, 나머지는 전체에서 랜덤 선택. */
|
||||
private long pickDog(Random rnd, Map<String, List<Long>> byBreed, String[] pref, String[] all) {
|
||||
String breed = rnd.nextInt(4) < 3 ? pref[rnd.nextInt(pref.length)] : all[rnd.nextInt(all.length)];
|
||||
List<Long> pool = byBreed.get(breed);
|
||||
if (pool == null || pool.isEmpty()) {
|
||||
for (List<Long> v : byBreed.values()) {
|
||||
if (!v.isEmpty()) {
|
||||
return v.get(rnd.nextInt(v.size()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return pool.get(rnd.nextInt(pool.size()));
|
||||
}
|
||||
|
||||
private long insertSpot(String name, String type, double lat, double lng) {
|
||||
return jdbc.queryForObject("""
|
||||
INSERT INTO spots(name, type, latitude, longitude, created_at)
|
||||
VALUES (?, ?, ?, ?, now()) RETURNING id
|
||||
""", Long.class, name, type, lat, lng);
|
||||
}
|
||||
|
||||
private long insertUser(String email, String nickname, String tier) {
|
||||
return jdbc.queryForObject("""
|
||||
INSERT INTO users(email, nickname, subscription_tier, status, role, provider, created_at, updated_at)
|
||||
VALUES (?, ?, ?, 'ACTIVE', 'USER', 'GOOGLE', now(), now()) RETURNING id
|
||||
""", Long.class, email, nickname, tier);
|
||||
}
|
||||
|
||||
private long insertDog(long userId, String name, String breed, String size, String gender) {
|
||||
return jdbc.queryForObject("""
|
||||
INSERT INTO dogs(user_id, name, breed, size, gender, neutered, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, false, now()) RETURNING id
|
||||
""", Long.class, userId, name, breed, size, gender);
|
||||
}
|
||||
|
||||
private void linkTrait(long dogId, long tagId) {
|
||||
jdbc.update("INSERT INTO dog_trait_tags(dog_id, tag_id) VALUES (?, ?) ON CONFLICT DO NOTHING",
|
||||
dogId, tagId);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user