From 96b415dd09ce2639d2452188673e50be4ce67983 Mon Sep 17 00:00:00 2001 From: sb Date: Sat, 11 Jul 2026 19:28:27 +0900 Subject: [PATCH] =?UTF-8?q?feat(dev):=20dev=20=ED=94=84=EB=A1=9C=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=8B=9C=EB=8D=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 스팟6·유저/강아지(견종·성향)·요일/시간대 분산 체크인 이력(자주오는견종용)· 현재 활성 체크인(메이트/AI, Redis)·한줄평 시드. idempotent(스팟 있으면 skip), 실패해도 기동 막지 않음(try/catch). prod 는 미실행이라 DB 깨끗. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../com/dog/dognation/config/DevSeedData.java | 225 ++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 src/main/java/com/dog/dognation/config/DevSeedData.java diff --git a/src/main/java/com/dog/dognation/config/DevSeedData.java b/src/main/java/com/dog/dognation/config/DevSeedData.java new file mode 100644 index 0000000..27ea5bf --- /dev/null +++ b/src/main/java/com/dog/dognation/config/DevSeedData.java @@ -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 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 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 traitIds = jdbc.queryForList("SELECT id FROM trait_tags", Long.class); + + // 3) 유저 + 강아지 (견종별 3~5마리) → 견종별 dogId 풀 + Map> dogsByBreed = new HashMap<>(); + Map ownerByDog = new HashMap<>(); + List 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 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 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 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> byBreed, String[] pref, String[] all) { + String breed = rnd.nextInt(4) < 3 ? pref[rnd.nextInt(pref.length)] : all[rnd.nextInt(all.length)]; + List pool = byBreed.get(breed); + if (pool == null || pool.isEmpty()) { + for (List 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); + } +} \ No newline at end of file