v1.3.7: NAS용 마이그레이션 셸 명령 추가
운영 호스트에 npm이 없어도 Docker Compose와 DB 컨테이너 psql만으로 상태 확인, baseline, 미적용 SQL 실행을 처리한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -138,13 +138,13 @@ cp .env.example .env.production
|
||||
docker compose --env-file .env.production up -d --build
|
||||
|
||||
# 운영 DB 마이그레이션 상태 확인
|
||||
npm run db:migrate:prod:status
|
||||
sh scripts/migrate-production-db.sh status
|
||||
|
||||
# schema_migrations 도입 전 운영 DB가 이미 최신이면 최초 1회 기준점 기록(실제 SQL 실행 없음)
|
||||
npm run db:migrate:prod:baseline
|
||||
sh scripts/migrate-production-db.sh baseline
|
||||
|
||||
# 이후 배포에서는 아직 적용되지 않은 SQL만 순서대로 실행
|
||||
npm run db:migrate:prod
|
||||
sh scripts/migrate-production-db.sh migrate
|
||||
```
|
||||
|
||||
### Docker 네트워크 충돌 대응
|
||||
@@ -198,8 +198,8 @@ docker compose --env-file .env.production up -d --build
|
||||
- NAS Docker 배포 시 PostgreSQL 초기 스키마는 `db/migrations/`의 SQL로 생성
|
||||
- 로컬 개발 Docker Compose 실행 시 `ENV_FILE=.env.development`와 `--env-file .env.development`를 함께 사용
|
||||
- 로컬 개발 DB 마이그레이션은 `npm run db:migrate:dev`로 실행
|
||||
- NAS 운영 DB 마이그레이션은 `npm run db:migrate:prod:status`로 적용 상태를 확인하고, `npm run db:migrate:prod`로 미적용 파일만 실행한다.
|
||||
- `schema_migrations`가 없는 기존 운영 DB에서 `posts` 테이블이 감지되면 `db:migrate:prod`는 001부터 자동 실행하지 않는다. 현재 코드 기준 최신 DB라면 최초 1회 `npm run db:migrate:prod:baseline`으로 기존 파일을 적용 완료로 기록한다. 특정 번호까지만 기록하려면 예: `npm run db:migrate:prod:baseline -- --to=031`.
|
||||
- NAS 운영 DB 마이그레이션은 NAS 호스트에 npm이 없어도 실행할 수 있도록 `sh scripts/migrate-production-db.sh status`로 적용 상태를 확인하고, `sh scripts/migrate-production-db.sh migrate`로 미적용 파일만 실행한다.
|
||||
- `schema_migrations`가 없는 기존 운영 DB에서 `posts` 테이블이 감지되면 `migrate`는 001부터 자동 실행하지 않는다. 현재 코드 기준 최신 DB라면 최초 1회 `sh scripts/migrate-production-db.sh baseline`으로 기존 파일을 적용 완료로 기록한다. 특정 번호까지만 기록하려면 예: `sh scripts/migrate-production-db.sh baseline 031`.
|
||||
- 네비게이션 계층(`parent_id`, `is_folder`)은 `017_navigation_hierarchy.sql` 적용 후 저장 API가 정상 동작한다(미적용 시 `INSERT` 컬럼 불일치).
|
||||
- 회원 마지막 로그인 표시(`previous_last_seen_at`, `previous_last_seen_ip`)는 `021_add_member_previous_login.sql` 적용 후 정상 동작한다.
|
||||
- 사이트 로고와 파비콘 저장(`logo_url`, `favicon_url`)은 `022_add_site_logo_urls.sql` 적용 후 정상 동작한다.
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# 의사결정 이력
|
||||
|
||||
## 2026-05-20 v1.3.7
|
||||
|
||||
### NAS 마이그레이션 명령을 npm 없는 호스트 기준으로 보정
|
||||
|
||||
NAS 운영 호스트는 Docker Compose만 있고 Node/npm이 없을 수 있다. 운영 DB 마이그레이션은 앱 빌드 도구가 아니라 배포 호스트에서 실행하는 운영 절차이므로, `npm run db:migrate:prod:*`만 안내하면 실제 NAS에서 막힌다. Docker Compose와 DB 컨테이너의 `psql`만 사용하는 `scripts/migrate-production-db.sh`를 추가해 호스트 npm 설치 여부와 무관하게 상태 확인, baseline, 미적용 SQL 실행을 처리하도록 했다.
|
||||
|
||||
## 2026-05-20 v1.3.6
|
||||
|
||||
### NAS 운영 마이그레이션 적용 이력 도입
|
||||
|
||||
@@ -293,6 +293,7 @@
|
||||
| scripts/dev-server.js | 로컬 개발 서버 링크 요약 출력 |
|
||||
| scripts/migrate-database.js | 로컬·NAS DB 마이그레이션 적용/상태/baseline 실행 |
|
||||
| scripts/migrate-development-db.js | 기존 로컬 개발 DB 마이그레이션 명령 호환 래퍼 |
|
||||
| scripts/migrate-production-db.sh | npm 없는 NAS 호스트용 운영 DB 마이그레이션 적용/상태/baseline 실행 |
|
||||
| .env.example | 환경 변수 예시 |
|
||||
| Dockerfile | NAS 운영 이미지 빌드 |
|
||||
| docker-compose.yml | NAS 컨테이너 실행 초안 |
|
||||
|
||||
@@ -217,9 +217,9 @@ components/content/
|
||||
|
||||
- `schema_migrations` 테이블은 적용 완료된 SQL 파일명을 `file_name` 기준으로 기록한다.
|
||||
- `npm run db:migrate:dev`와 `npm run db:migrate:prod`는 `db/migrations/*.sql` 중 `schema_migrations`에 없는 파일만 순서대로 실행한다.
|
||||
- `npm run db:migrate:prod:status`는 NAS 운영 DB의 적용/대기 파일 목록을 출력한다.
|
||||
- 기존 운영 DB에 `posts` 테이블은 있지만 `schema_migrations`가 없으면 `npm run db:migrate:prod`는 데이터 보호를 위해 001부터 자동 실행하지 않고 중단한다.
|
||||
- 기존 운영 DB가 현재 코드 기준으로 이미 최신이면 `npm run db:migrate:prod:baseline`으로 현재 마이그레이션 파일들을 실행 없이 적용 완료로 기록한 뒤 이후 새 파일만 적용한다.
|
||||
- `sh scripts/migrate-production-db.sh status`는 npm이 없는 NAS 호스트에서도 운영 DB의 적용/대기 파일 목록을 출력한다.
|
||||
- 기존 운영 DB에 `posts` 테이블은 있지만 `schema_migrations`가 없으면 `sh scripts/migrate-production-db.sh migrate`는 데이터 보호를 위해 001부터 자동 실행하지 않고 중단한다.
|
||||
- 기존 운영 DB가 현재 코드 기준으로 이미 최신이면 `sh scripts/migrate-production-db.sh baseline`으로 현재 마이그레이션 파일들을 실행 없이 적용 완료로 기록한 뒤 이후 새 파일만 적용한다.
|
||||
|
||||
### Posts (블로그 글)
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# 업데이트 이력
|
||||
|
||||
## v1.3.7
|
||||
|
||||
- NAS 마이그레이션: npm 없는 NAS 호스트에서도 실행 가능한 `scripts/migrate-production-db.sh` 추가.
|
||||
- 운영 문서: `db:migrate:prod:*` 안내를 `sh scripts/migrate-production-db.sh` 기준으로 수정.
|
||||
|
||||
## v1.3.6
|
||||
|
||||
- DB 마이그레이션: `schema_migrations` 적용 이력 관리와 `db:migrate:prod`, `db:migrate:prod:status`, `db:migrate:prod:baseline` 명령 추가.
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sori.studio",
|
||||
"version": "1.3.6",
|
||||
"version": "1.3.7",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sori.studio",
|
||||
"version": "1.3.6",
|
||||
"version": "1.3.7",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sori.studio",
|
||||
"version": "1.3.6",
|
||||
"version": "1.3.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"imports": {
|
||||
@@ -19,9 +19,9 @@
|
||||
"db:migrate": "node scripts/migrate-database.js migrate",
|
||||
"db:migrate:dev": "MIGRATION_ENV=development ENV_FILE=.env.development node scripts/migrate-database.js migrate",
|
||||
"db:migrate:dev:status": "MIGRATION_ENV=development ENV_FILE=.env.development node scripts/migrate-database.js status",
|
||||
"db:migrate:prod": "MIGRATION_ENV=production ENV_FILE=.env.production node scripts/migrate-database.js migrate",
|
||||
"db:migrate:prod:status": "MIGRATION_ENV=production ENV_FILE=.env.production node scripts/migrate-database.js status",
|
||||
"db:migrate:prod:baseline": "MIGRATION_ENV=production ENV_FILE=.env.production node scripts/migrate-database.js baseline",
|
||||
"db:migrate:prod": "sh scripts/migrate-production-db.sh migrate",
|
||||
"db:migrate:prod:status": "sh scripts/migrate-production-db.sh status",
|
||||
"db:migrate:prod:baseline": "sh scripts/migrate-production-db.sh baseline",
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
171
scripts/migrate-production-db.sh
Normal file
171
scripts/migrate-production-db.sh
Normal file
@@ -0,0 +1,171 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ENV_FILE=${ENV_FILE:-.env.production}
|
||||
DB_SERVICE=${DB_SERVICE:-sori-studio-db}
|
||||
MIGRATIONS_DIR="$ROOT_DIR/db/migrations"
|
||||
CONTAINER_MIGRATIONS_DIR=${CONTAINER_MIGRATIONS_DIR:-/docker-entrypoint-initdb.d}
|
||||
SCHEMA_MIGRATIONS_TABLE=${SCHEMA_MIGRATIONS_TABLE:-schema_migrations}
|
||||
MODE=${1:-migrate}
|
||||
BASELINE_TARGET=${2:-}
|
||||
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
. "$ENV_FILE"
|
||||
set +a
|
||||
fi
|
||||
|
||||
POSTGRES_DB=${POSTGRES_DB:-sori_studio}
|
||||
POSTGRES_USER=${POSTGRES_USER:-sori_studio}
|
||||
|
||||
compose() {
|
||||
docker compose --env-file "$ENV_FILE" "$@"
|
||||
}
|
||||
|
||||
psql_exec() {
|
||||
compose exec -T "$DB_SERVICE" psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" "$@"
|
||||
}
|
||||
|
||||
query_sql() {
|
||||
psql_exec -At -c "$1" | tr -d '\r'
|
||||
}
|
||||
|
||||
run_sql() {
|
||||
psql_exec -c "$1"
|
||||
}
|
||||
|
||||
ensure_database_ready() {
|
||||
compose up -d "$DB_SERVICE"
|
||||
|
||||
attempt=1
|
||||
while [ "$attempt" -le 20 ]; do
|
||||
if compose exec -T "$DB_SERVICE" pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
attempt=$((attempt + 1))
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "DB가 준비되지 않았습니다." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
has_schema_migrations_table() {
|
||||
[ "$(query_sql "SELECT to_regclass('public.$SCHEMA_MIGRATIONS_TABLE') IS NOT NULL;")" = "t" ]
|
||||
}
|
||||
|
||||
has_application_schema() {
|
||||
[ "$(query_sql "SELECT to_regclass('public.posts') IS NOT NULL;")" = "t" ]
|
||||
}
|
||||
|
||||
ensure_schema_migrations_table() {
|
||||
run_sql "CREATE TABLE IF NOT EXISTS $SCHEMA_MIGRATIONS_TABLE (
|
||||
file_name TEXT PRIMARY KEY,
|
||||
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);"
|
||||
}
|
||||
|
||||
migration_files() {
|
||||
find "$MIGRATIONS_DIR" -maxdepth 1 -type f -name '*.sql' -exec basename {} \; | sort
|
||||
}
|
||||
|
||||
is_applied() {
|
||||
file_name=$1
|
||||
[ "$(query_sql "SELECT EXISTS (SELECT 1 FROM $SCHEMA_MIGRATIONS_TABLE WHERE file_name = '$file_name');")" = "t" ]
|
||||
}
|
||||
|
||||
print_status() {
|
||||
if ! has_schema_migrations_table; then
|
||||
echo "schema_migrations 테이블이 없습니다."
|
||||
echo "기존 운영 DB라면 먼저 sh scripts/migrate-production-db.sh baseline 으로 현재 파일들을 적용 완료로 기록하세요."
|
||||
|
||||
migration_files | while IFS= read -r file_name; do
|
||||
echo "pending $file_name"
|
||||
done
|
||||
return 0
|
||||
fi
|
||||
|
||||
migration_files | while IFS= read -r file_name; do
|
||||
if is_applied "$file_name"; then
|
||||
echo "applied $file_name"
|
||||
else
|
||||
echo "pending $file_name"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
baseline_migrations() {
|
||||
ensure_schema_migrations_table
|
||||
baseline_count=0
|
||||
found_target=0
|
||||
|
||||
migration_files | while IFS= read -r file_name; do
|
||||
if [ -n "$BASELINE_TARGET" ] && [ "$found_target" -eq 1 ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
run_sql "INSERT INTO $SCHEMA_MIGRATIONS_TABLE (file_name) VALUES ('$file_name') ON CONFLICT (file_name) DO NOTHING;"
|
||||
echo "baseline $file_name"
|
||||
baseline_count=$((baseline_count + 1))
|
||||
|
||||
if [ -n "$BASELINE_TARGET" ] && { [ "$file_name" = "$BASELINE_TARGET" ] || echo "$file_name" | grep -q "^${BASELINE_TARGET}_"; }; then
|
||||
found_target=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$BASELINE_TARGET" ] && ! migration_files | grep -q "^${BASELINE_TARGET}_\\|^${BASELINE_TARGET}$"; then
|
||||
echo "기준 마이그레이션을 찾을 수 없습니다: $BASELINE_TARGET" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "baseline 완료"
|
||||
}
|
||||
|
||||
migrate_database() {
|
||||
if ! has_schema_migrations_table && has_application_schema; then
|
||||
echo "운영 DB에 기존 스키마가 있지만 schema_migrations 적용 이력이 없습니다." >&2
|
||||
echo "데이터 보호를 위해 001부터 자동 실행하지 않습니다." >&2
|
||||
echo "현재 운영 DB가 최신 상태라면 sh scripts/migrate-production-db.sh baseline 으로 기준점을 먼저 기록하세요." >&2
|
||||
echo "상태 확인: sh scripts/migrate-production-db.sh status" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ensure_schema_migrations_table
|
||||
applied_count=0
|
||||
|
||||
migration_files | while IFS= read -r file_name; do
|
||||
if is_applied "$file_name"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "apply $file_name"
|
||||
psql_exec -f "$CONTAINER_MIGRATIONS_DIR/$file_name"
|
||||
run_sql "INSERT INTO $SCHEMA_MIGRATIONS_TABLE (file_name) VALUES ('$file_name') ON CONFLICT (file_name) DO NOTHING;"
|
||||
applied_count=$((applied_count + 1))
|
||||
done
|
||||
|
||||
echo "마이그레이션 실행 완료"
|
||||
}
|
||||
|
||||
ensure_database_ready
|
||||
|
||||
case "$MODE" in
|
||||
status)
|
||||
print_status
|
||||
;;
|
||||
baseline)
|
||||
baseline_migrations
|
||||
;;
|
||||
migrate)
|
||||
migrate_database
|
||||
;;
|
||||
*)
|
||||
echo "사용법: sh scripts/migrate-production-db.sh [status|baseline|migrate] [기준번호]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user