From cc34db40f21fca7c4291d1edfce41452df4a2fa8 Mon Sep 17 00:00:00 2001 From: zenn Date: Wed, 20 May 2026 14:39:44 +0900 Subject: [PATCH] =?UTF-8?q?v1.3.7:=20NAS=EC=9A=A9=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98=20=EC=85=B8=20=EB=AA=85?= =?UTF-8?q?=EB=A0=B9=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 운영 호스트에 npm이 없어도 Docker Compose와 DB 컨테이너 psql만으로 상태 확인, baseline, 미적용 SQL 실행을 처리한다. Co-authored-by: Cursor --- docs/deploy.md | 10 +- docs/history.md | 6 ++ docs/map.md | 1 + docs/spec.md | 6 +- docs/update.md | 5 + package-lock.json | 4 +- package.json | 8 +- scripts/migrate-production-db.sh | 171 +++++++++++++++++++++++++++++++ 8 files changed, 197 insertions(+), 14 deletions(-) create mode 100644 scripts/migrate-production-db.sh diff --git a/docs/deploy.md b/docs/deploy.md index 2fc035a..947b71d 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -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` 적용 후 정상 동작한다. diff --git a/docs/history.md b/docs/history.md index b3d4c81..b7ed46f 100644 --- a/docs/history.md +++ b/docs/history.md @@ -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 운영 마이그레이션 적용 이력 도입 diff --git a/docs/map.md b/docs/map.md index c7104fd..7b99ac7 100644 --- a/docs/map.md +++ b/docs/map.md @@ -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 컨테이너 실행 초안 | diff --git a/docs/spec.md b/docs/spec.md index 9f1ed30..e8e836e 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -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 (블로그 글) diff --git a/docs/update.md b/docs/update.md index 86722d8..18abda8 100644 --- a/docs/update.md +++ b/docs/update.md @@ -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` 명령 추가. diff --git a/package-lock.json b/package-lock.json index cace365..4fbfe4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 2190986..06182a5 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/scripts/migrate-production-db.sh b/scripts/migrate-production-db.sh new file mode 100644 index 0000000..4e5cc39 --- /dev/null +++ b/scripts/migrate-production-db.sh @@ -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