Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 21 additions & 9 deletions back/src/main/java/com/back/global/initdata/InitData.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.back.domain.comment.entity.Comment;
import com.back.domain.comment.repository.CommentRepository;
import com.back.domain.node.dto.PivotListDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Profile;
import com.back.domain.node.dto.base.BaseLineBulkCreateRequest;
import com.back.domain.node.dto.base.BaseLineBulkCreateResponse;
import com.back.domain.node.dto.decision.DecNodeDto;
Expand Down Expand Up @@ -42,8 +44,13 @@
* [요약] 기동 시 admin·user1 생성 → user1에 베이스라인(총7: 헤더+피벗5+테일) 1개와 결정라인 2개(첫 번째 6노드, 두 번째 2노드) 시드 주입.
* 게시글 30개(일반20 + 투표10)과 댓글 14개(마지막 2개 글에 각 7개) 생성.
* 시나리오 3개(베이스 1개 + 완성 1개 + 처리중 1개)와 지표 10개(완성된 시나리오 2개에 각 5개씩) 생성.
*
* [주의] 프로덕션 환경에서는 실행되지 않습니다. (@Profile 설정)
* 프로덕션 필수 데이터는 Flyway 마이그레이션(db/migration)을 통해 관리됩니다.
*/
@Slf4j
@Component
@Profile("!prod") // prod 프로파일에서는 실행 안 함
@RequiredArgsConstructor
public class InitData implements CommandLineRunner {

Expand All @@ -62,6 +69,14 @@ public class InitData implements CommandLineRunner {
// user1을 만들고 베이스라인(7)과 결정라인(5)을 시드로 주입한다
@Override
public void run(String... args) {
// 전체 InitData 실행 여부 체크 (user1 존재 여부로 판단)
if (userRepository.findByEmail("[email protected]").isPresent()) {
log.info("[InitData] 이미 초기화 데이터가 존재합니다. 스킵합니다.");
return;
}

log.info("[InitData] 초기화 데이터 생성을 시작합니다...");

if (userRepository.findByEmail("[email protected]").isEmpty()) {
var admin = User.builder()
.email("[email protected]")
Expand Down Expand Up @@ -221,10 +236,6 @@ public void run(String... args) {
)
);

if (postRepository.count() > 0) {
return;
}

// 잠담 게시글 20개 생성
List<Post> posts = new ArrayList<>();
for (int i = 1; i <= 20; i++) {
Expand Down Expand Up @@ -287,11 +298,6 @@ public void run(String... args) {

// ========== Scenario InitData 생성 ==========

// 시나리오 데이터가 이미 있으면 스킵
if (scenarioRepository.count() > 0) {
return;
}

// BaseLine 조회 (위에서 생성한 baseLineId 사용)
BaseLine baseLine = baseLineRepository.findById(baseLineId)
.orElseThrow(() -> new IllegalStateException("BaseLine not found"));
Expand Down Expand Up @@ -370,6 +376,12 @@ public void run(String... args) {
.status(ScenarioStatus.PROCESSING)
.build();
scenarioRepository.save(processingScenario);

log.info("[InitData] 초기화 데이터 생성 완료!");
log.info("[InitData] - 사용자: admin, user1");
log.info("[InitData] - 게시글: {} 개", postRepository.count());
log.info("[InitData] - 댓글: {} 개", commentRepository.count());
log.info("[InitData] - 시나리오: {} 개", scenarioRepository.count());
}

/**
Expand Down
3 changes: 3 additions & 0 deletions back/src/main/resources/application-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ spring:
username: sa
password:
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create-drop
h2:
console:
enabled: true
Expand Down
82 changes: 82 additions & 0 deletions back/src/main/resources/db/migration/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Flyway 마이그레이션 가이드

## 개요
이 디렉토리는 Flyway 데이터베이스 마이그레이션 파일을 관리합니다.

## 파일 구조
```
db/migration/
├── V1__init_schema.sql # 초기 스키마 생성
├── V2__init_prod_essential.sql # 프로덕션 필수 데이터 (admin 계정)
└── README.md # 이 파일
```

## 마이그레이션 파일 명명 규칙
- **형식**: `V{버전}__{설명}.sql`
- **예시**: `V1__init_schema.sql`, `V2__add_user_table.sql`
- **주의**: 언더스코어 2개 (`__`) 사용 필수

## V2__init_prod_essential.sql 사용 전 필수 작업

### 1. BCrypt 패스워드 생성
```bash
# 테스트 실행
./gradlew test --tests "com.back.util.PasswordEncoderTest"

# 콘솔에서 출력된 BCrypt 해시 복사
# 예: $2a$10$N9qo8uLOickgx2ZMRZoMye...
```

### 2. SQL 파일 수정
`V2__init_prod_essential.sql` 파일의 33번째 줄을 실제 BCrypt 해시로 교체:
```sql
-- 수정 전
'$2a$10$YourActualBCryptHashHere', -- TODO: 실제 BCrypt 해시로 교체

-- 수정 후 (실제 값으로)
'$2a$10$N9qo8uLOickgx2ZMRZoMye...', -- PasswordEncoderTest 실행 결과
```

## 프로파일별 동작

### dev / test 프로파일
- Flyway: **비활성화** (`enabled: false`)
- InitData.java: **활성화** (풍부한 테스트 데이터 자동 생성)

### prod 프로파일
- Flyway: **활성화** (`enabled: true`)
- InitData.java: **비활성화** (`@Profile("!prod")`)

## 멱등성 보장
모든 마이그레이션 파일은 `ON CONFLICT DO NOTHING` 구문을 사용하여:
- Blue-Green 배포 시 중복 실행 방지
- 재배포 시 안전성 보장
- 429 API 에러 방지

## 실행 이력 확인
```sql
SELECT * FROM flyway_schema_history ORDER BY installed_rank;
```

## 주의사항
1. **마이그레이션 파일은 절대 수정 금지** (체크섬 검증 실패)
2. **새로운 변경사항은 새 파일로 추가** (V3, V4...)
3. **프로덕션 배포 전 반드시 로컬에서 테스트**

## 문제 해결

### 체크섬 오류 발생 시
```sql
-- 마이그레이션 이력 초기화 (주의: 개발 환경에서만!)
DELETE FROM flyway_schema_history WHERE version = '2';
```

### 마이그레이션 실패 시
```bash
# Flyway repair 명령 (Spring Boot Actuator 필요)
./gradlew flywayRepair
```

## 참고 문서
- [Flyway Documentation](https://documentation.red-gate.com/fd)
- [프로젝트 CLAUDE.md](../../../CLAUDE.md)
3 changes: 3 additions & 0 deletions back/src/main/resources/db/migration/V1__init_schema.sql
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
--liquibase formatted sql
--dialect postgresql

-- ========================================
-- 사용자 테이블
-- ========================================
Expand Down
46 changes: 46 additions & 0 deletions back/src/main/resources/db/migration/V2__init_prod_essential.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
--liquibase formatted sql
--dialect postgresql

-- ============================================
-- V2__init_prod_essential.sql
-- 프로덕션 환경 필수 초기 데이터
-- ============================================
-- 설명:
-- - Blue-Green 배포 시에도 멱등성을 보장합니다
-- - ON CONFLICT DO NOTHING 구문으로 중복 실행 방지
-- - admin 계정만 생성 (테스트 데이터는 제외)
-- ============================================

-- Admin 사용자 생성 (멱등하게)
INSERT INTO users (
email,
password,
role,
username,
nickname,
birthday_at,
gender,
mbti,
beliefs,
created_date,
updated_date
)
VALUES (
'[email protected]',
-- 주의: 실제 프로덕션에서는 PasswordEncoderTest 실행 결과로 교체하세요
-- 현재 값은 'admin1234!' 인코딩 결과 (매번 다를 수 있음)
'$2a$10$3d.SEWYbRSu6Wett4Txv9OxIL2AccoJP76Hcja5b/T.e4nB9nluIS',
'ADMIN',
'관리자',
'관리자',
'1990-01-01 00:00:00',
'M',
'INTJ',
'합리주의',
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
)
ON CONFLICT (email) DO NOTHING;

-- 초기화 완료 로그
-- Flyway는 실행 성공 시 flyway_schema_history 테이블에 자동 기록됩니다.
30 changes: 30 additions & 0 deletions back/src/test/java/com/back/util/PasswordEncoderTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.back.util;

import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
* 프로덕션 Flyway 마이그레이션용 패스워드 인코딩 값을 생성합니다.
* 테스트 실행 후 출력된 값을 V2__init_prod_essential.sql에 사용하세요.
*/
public class PasswordEncoderTest {

private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

@Test
public void generateEncodedPasswords() {
String adminPassword = "admin1234!";
String encodedAdmin = passwordEncoder.encode(adminPassword);

System.out.println("=================================================");
System.out.println("Flyway 마이그레이션용 인코딩된 패스워드");
System.out.println("=================================================");
System.out.println("Admin Password (admin1234!):");
System.out.println(encodedAdmin);
System.out.println("=================================================");

// 검증
System.out.println("\n검증: " + passwordEncoder.matches(adminPassword, encodedAdmin));
}
}