From fcc89261727b5d3f1918257014ae7b961a2d2d85 Mon Sep 17 00:00:00 2001 From: jisangs Date: Mon, 7 Sep 2026 22:18:08 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A7=88=EC=97=90=EC=8A=A4=ED=8A=B8=EB=A1=9C?= =?UTF-8?q?=20DB=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=B0=B1=EC=97=85+?= =?UTF-8?q?=EC=82=AD=EC=A0=9C,=20=EB=B3=B5=EC=9B=90=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A6=BD=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backup-maestro.sh | 308 ++++++++++++++++++ .../restore-maestro.sh | 149 +++++++++ 2 files changed, 457 insertions(+) create mode 100644 doc/db/260907-backup-delete-maestro/backup-maestro.sh create mode 100644 doc/db/260907-backup-delete-maestro/restore-maestro.sh diff --git a/doc/db/260907-backup-delete-maestro/backup-maestro.sh b/doc/db/260907-backup-delete-maestro/backup-maestro.sh new file mode 100644 index 0000000..0e300b1 --- /dev/null +++ b/doc/db/260907-backup-delete-maestro/backup-maestro.sh @@ -0,0 +1,308 @@ +#!/usr/bin/env bash +# +# backup_maestro.sh +# ------------------------------------------------------------ +# 지정한 MaestroID 와 관련된 12개 테이블의 데이터를 +# 1) mariadb-dump(mysqldump) 로 백업하고 +# 2) (재확인 후) 운영 DB 에서 실제 삭제까지 수행하는 스크립트. +# +# 실행 위치: Synology NAS (운영 서버가 아님) +# - NAS에 설치된 MariaDB client 로 AWS 운영 DB 서버에 +# 원격 접속해서 덤프/삭제를 수행합니다. +# - 운영 DB 서버의 보안 그룹/방화벽에서 NAS 의 공인 IP +# (또는 VPN 대역)에 대해 3306 포트가 열려 있어야 합니다. +# +# 결과물은 "복원 가능한 INSERT 문(.sql)" 한 개 파일로 생성됩니다. +# (테이블 구조는 포함하지 않음 — 복원 대상 DB에 스키마가 이미 +# 존재한다고 가정. 스키마까지 필요하면 아래 USE_NO_CREATE_INFO +# 를 false 로 바꾸세요.) +# +# 삭제 단계는: +# - 백업이 모든 테이블에서 성공했을 때만 진행합니다. +# - MaestroID 를 다시 한번 입력해야만 실행됩니다. +# - 자식 테이블 → 부모(maestro) 순으로 하나의 트랜잭션에서 삭제합니다. +# - 문제 발생 시 방금 만든 백업 파일로 restore_maestro.sh 로 복원 가능. +# +# 사용법: +# chmod +x backup_maestro.sh +# ./backup_maestro.sh # 운영 DB (기본) +# ./backup_maestro.sh --test # 테스트 DB +# 예) ./backup_maestro.sh 317 +# ./backup_maestro.sh --test 317 +# ------------------------------------------------------------ + +set -euo pipefail + +# Synology NAS 의 MariaDB10 패키지 바이너리 경로. +# 기본 PATH 에 포함되지 않는 경우가 많아 명시적으로 추가합니다. +export PATH="$PATH:/var/packages/MariaDB10/target/usr/local/mariadb10.11/bin" + +# ===================== DB 접속 정보 ===================== +# 운영 DB (기본) +PROD_DB_HOST="chocomae.jinaju.com" +PROD_DB_PORT="3306" +PROD_DB_USER="chocomae" +PROD_DB_NAME="chocomae" + +# 테스트 DB (--test 옵션으로 활성화) +TEST_DB_HOST="mariadb.jisangs.com" +TEST_DB_PORT="30001" +TEST_DB_USER="chocomae" +TEST_DB_NAME="chocomae" + +# true : 데이터(INSERT)만 백업 (기본, 스키마는 이미 있다고 가정) +# false : CREATE TABLE 구조까지 포함해서 백업 +USE_NO_CREATE_INFO=true + +OUTPUT_DIR="./backup" +# ========================================================= + +# ---- 인자 파싱 (순서 자유: --test 는 앞/뒤 어디에 와도 됨) ---- +USE_TEST=false +MAESTRO_ID="" +while [ $# -gt 0 ]; do + case "$1" in + -t|--test) + USE_TEST=true + shift + ;; + -h|--help) + echo "사용법: $0 [--test] " + exit 0 + ;; + -*) + echo "알 수 없는 옵션: $1" + exit 1 + ;; + *) + if [ -z "$MAESTRO_ID" ]; then + MAESTRO_ID="$1" + else + echo "인자가 너무 많습니다: $1" + exit 1 + fi + shift + ;; + esac +done + +if [ -z "$MAESTRO_ID" ]; then + echo "사용법: $0 [--test] " + echo "예) $0 317" + echo " $0 --test 317" + exit 1 +fi +if ! [[ "$MAESTRO_ID" =~ ^[0-9]+$ ]]; then + echo "MaestroID 는 정수여야 합니다: '${MAESTRO_ID}'" + exit 1 +fi + +# ---- 대상 DB 확정 ---- +if [ "$USE_TEST" = true ]; then + DB_HOST="$TEST_DB_HOST" + DB_PORT="$TEST_DB_PORT" + DB_USER="$TEST_DB_USER" + DB_NAME="$TEST_DB_NAME" + TARGET_LABEL="TEST" +else + DB_HOST="$PROD_DB_HOST" + DB_PORT="$PROD_DB_PORT" + DB_USER="$PROD_DB_USER" + DB_NAME="$PROD_DB_NAME" + TARGET_LABEL="PROD" +fi + +echo "접속 대상: [${TARGET_LABEL}] ${DB_USER}@${DB_HOST}:${DB_PORT}/${DB_NAME}" + +# 운영 서버 대상일 경우 실수 방지용 재확인. +if [ "$TARGET_LABEL" = "PROD" ]; then + echo + echo "!! 대상은 운영(PROD) DB 입니다. 백업 후 삭제까지 실제로 수행됩니다." + read -r -p "계속하려면 'PROD' 를 입력하세요: " PROD_CONFIRM + if [ "$PROD_CONFIRM" != "PROD" ]; then + echo "취소되었습니다." + exit 0 + fi +fi + +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +OUTPUT_FILE="${OUTPUT_DIR}/maestro_${MAESTRO_ID}_backup_${TIMESTAMP}.sql" + +# 복원 편의를 위해 부모 테이블(maestro)을 가장 먼저 나열 +TABLES=( + maestro + app_highest_record + best_record + license_maestro_password + license_score + license_time + maestro_extension + maestro_log + maestro_upgrade + player + typing_exam_highest_record + typing_exam_record +) + +# Synology NAS 에는 mariadb-dump / mariadb 로 설치되는 경우가 많고, +# 배포판에 따라 mysqldump / mysql 심볼릭 링크만 있는 경우도 있어 둘 다 지원합니다. +if command -v mariadb-dump >/dev/null 2>&1; then + DUMP_CMD="mariadb-dump" +elif command -v mysqldump >/dev/null 2>&1; then + DUMP_CMD="mysqldump" +else + echo "mariadb-dump / mysqldump 명령을 찾을 수 없습니다. NAS 에 MariaDB client 를 먼저 설치하세요." + exit 1 +fi +if command -v mariadb >/dev/null 2>&1; then + MYSQL_CMD="mariadb" +elif command -v mysql >/dev/null 2>&1; then + MYSQL_CMD="mysql" +else + echo "mariadb / mysql 명령을 찾을 수 없습니다. NAS 에 MariaDB client 를 먼저 설치하세요." + exit 1 +fi + +mkdir -p "$OUTPUT_DIR" + +# 비밀번호는 명령행 인자로 넘기지 않고(ps 로 노출되는 것을 방지) +# 프롬프트로 입력받아 MYSQL_PWD 환경변수로 전달합니다. +read -r -s -p "DB(${DB_USER}@${DB_HOST}) 비밀번호: " DB_PASS +echo +export MYSQL_PWD="$DB_PASS" + +# ---- MaestroID → Name 조회로 대상 계정 확인 ---- +if ! MAESTRO_NAME=$("$MYSQL_CMD" --protocol=TCP --default-character-set=utf8mb4 \ + -h"$DB_HOST" -P"$DB_PORT" -u"$DB_USER" "$DB_NAME" \ + -sN -e "SELECT \`Name\` FROM \`maestro\` WHERE MaestroID=${MAESTRO_ID};" \ + 2>/tmp/mysql_err.$$); then + echo "DB 조회에 실패했습니다: $(cat /tmp/mysql_err.$$)" + rm -f /tmp/mysql_err.$$ + unset MYSQL_PWD + exit 1 +fi +rm -f /tmp/mysql_err.$$ + +if [ -z "$MAESTRO_NAME" ]; then + echo "MaestroID=${MAESTRO_ID} 에 해당하는 계정이 [${TARGET_LABEL}] DB 에 없습니다." + unset MYSQL_PWD + exit 1 +fi + +echo +echo "대상 계정: MaestroID=${MAESTRO_ID} / Name='${MAESTRO_NAME}'" +read -r -p "이 계정이 맞으면 진행합니다. 계속하시겠습니까? (y/N) " NAME_CONFIRM +if [[ "$NAME_CONFIRM" != "y" && "$NAME_CONFIRM" != "Y" ]]; then + echo "취소되었습니다." + unset MYSQL_PWD + exit 0 +fi + +# 원격(AWS) DB 로 접속하므로 네트워크 관련 옵션을 추가합니다. +# --protocol=TCP : 로컬 소켓이 아닌 TCP 로 강제 접속 +# --default-character-set : 한글 데이터 안전하게 덤프 +# --set-gtid-purged=OFF : (MySQL 계열에서만 유효, MariaDB 는 무시) +DUMP_OPTS=( + --protocol=TCP + --default-character-set=utf8mb4 + --no-create-info + --compact + --complete-insert + --skip-lock-tables +) +if [ "$USE_NO_CREATE_INFO" = false ]; then + DUMP_OPTS=( + --protocol=TCP + --default-character-set=utf8mb4 + --compact + --complete-insert + --skip-lock-tables + ) +fi + +{ + echo "-- MaestroID=${MAESTRO_ID} 데이터 백업" + echo "-- 생성 시각: $(date '+%Y-%m-%d %H:%M:%S')" + echo "SET FOREIGN_KEY_CHECKS=0;" + echo "SET AUTOCOMMIT=0;" + echo "START TRANSACTION;" + echo +} > "$OUTPUT_FILE" + +FAILED_TABLES=() + +for TABLE in "${TABLES[@]}"; do + echo ">> ${TABLE} 백업 중..." + { + echo "-- ---------------------------------------------" + echo "-- ${TABLE}" + echo "-- ---------------------------------------------" + } >> "$OUTPUT_FILE" + if ! "$DUMP_CMD" -h"$DB_HOST" -P"$DB_PORT" -u"$DB_USER" \ + "${DUMP_OPTS[@]}" \ + --where="MaestroID=${MAESTRO_ID}" \ + "$DB_NAME" "$TABLE" >> "$OUTPUT_FILE" 2>/tmp/mysqldump_err.$$; then + echo " 경고: ${TABLE} 덤프 실패 - $(cat /tmp/mysqldump_err.$$)" + FAILED_TABLES+=("$TABLE") + fi + rm -f /tmp/mysqldump_err.$$ + echo >> "$OUTPUT_FILE" +done + +{ + echo "COMMIT;" + echo "SET FOREIGN_KEY_CHECKS=1;" +} >> "$OUTPUT_FILE" + +echo +echo "백업 완료: ${OUTPUT_FILE}" +echo "총 라인 수: $(wc -l < "$OUTPUT_FILE")" + +# ---------------- 원본 데이터 삭제 ---------------- +# 백업 도중 실패가 있었다면 삭제를 절대 진행하지 않습니다. +if [ ${#FAILED_TABLES[@]} -gt 0 ]; then + echo "다음 테이블에서 문제가 발생했습니다 (확인 필요): ${FAILED_TABLES[*]}" + echo "백업이 불완전하므로 원본 데이터 삭제를 건너뜁니다." + unset MYSQL_PWD + exit 1 +fi + +echo +echo "====================================================" +echo " WARNING: 이제 [${TARGET_LABEL}] DB(${DB_HOST}) 에서" +echo " MaestroID=${MAESTRO_ID} 관련 모든 데이터를 삭제합니다." +echo " 되돌리려면 방금 만든 백업 파일로 restore 해야 합니다." +echo "====================================================" +read -r -p "정말 삭제하려면 MaestroID 값(${MAESTRO_ID}) 숫자를 다시 입력하세요: " CONFIRM_ID +if [ "$CONFIRM_ID" != "$MAESTRO_ID" ]; then + echo "취소되었습니다. (원본 데이터는 그대로 유지됨)" + unset MYSQL_PWD + exit 0 +fi + +# 자식 테이블부터 삭제 → 마지막에 maestro(부모). +# 단일 트랜잭션으로 원자성 보장, FK 는 잠시 꺼두어 순서 의존성 회피. +DELETE_SQL="SET FOREIGN_KEY_CHECKS=0; +START TRANSACTION; +" +for (( i=${#TABLES[@]}-1; i>=0; i-- )); do + DELETE_SQL+="DELETE FROM \`${TABLES[$i]}\` WHERE MaestroID=${MAESTRO_ID}; +SELECT '${TABLES[$i]}' AS tbl, ROW_COUNT() AS deleted; +" +done +DELETE_SQL+="COMMIT; +SET FOREIGN_KEY_CHECKS=1;" + +echo +echo ">> 삭제 실행 중..." +if printf '%s\n' "$DELETE_SQL" | "$MYSQL_CMD" \ + --protocol=TCP --default-character-set=utf8mb4 \ + -h"$DB_HOST" -P"$DB_PORT" -u"$DB_USER" "$DB_NAME"; then + echo "삭제 완료." +else + echo "삭제 중 오류가 발생했습니다. 트랜잭션이 롤백되었을 수 있으니 DB 상태를 반드시 확인하세요." + unset MYSQL_PWD + exit 1 +fi + +unset MYSQL_PWD diff --git a/doc/db/260907-backup-delete-maestro/restore-maestro.sh b/doc/db/260907-backup-delete-maestro/restore-maestro.sh new file mode 100644 index 0000000..e4716b3 --- /dev/null +++ b/doc/db/260907-backup-delete-maestro/restore-maestro.sh @@ -0,0 +1,149 @@ +#!/usr/bin/env bash +# +# restore_maestro.sh +# ------------------------------------------------------------ +# backup_maestro.sh 로 만든 백업(.sql) 파일을 지정한 DB에 +# 복원하는 스크립트. +# +# 사용법: +# chmod +x restore_maestro.sh +# ./restore_maestro.sh # 운영 DB (기본) +# ./restore_maestro.sh --test # 테스트 DB +# ------------------------------------------------------------ + +set -euo pipefail + +# Synology NAS 의 MariaDB10 패키지 바이너리 경로. +# 기본 PATH 에 포함되지 않는 경우가 많아 명시적으로 추가합니다. +export PATH="$PATH:/var/packages/MariaDB10/target/usr/local/mariadb10.11/bin" + +# ===================== DB 접속 정보 ===================== +# 운영 DB (기본) +PROD_DB_HOST="chocomae.jinaju.com" +PROD_DB_PORT="3306" +PROD_DB_USER="chocomae" +PROD_DB_NAME="chocomae" + +# 테스트 DB (--test 옵션으로 활성화) +TEST_DB_HOST="mariadb.jisangs.com" +TEST_DB_PORT="30001" +TEST_DB_USER="chocomae" +TEST_DB_NAME="chocomae" +# ========================================================= + +# ---- 인자 파싱 (순서 자유: --test 는 앞/뒤 어디에 와도 됨) ---- +USE_TEST=false +BACKUP_FILE="" +while [ $# -gt 0 ]; do + case "$1" in + -t|--test) + USE_TEST=true + shift + ;; + -h|--help) + echo "사용법: $0 [--test] " + exit 0 + ;; + -*) + echo "알 수 없는 옵션: $1" + exit 1 + ;; + *) + if [ -z "$BACKUP_FILE" ]; then + BACKUP_FILE="$1" + else + echo "인자가 너무 많습니다: $1" + exit 1 + fi + shift + ;; + esac +done + +if [ -z "$BACKUP_FILE" ]; then + echo "사용법: $0 [--test] " + exit 1 +fi + +if [ ! -f "$BACKUP_FILE" ]; then + echo "파일을 찾을 수 없습니다: $BACKUP_FILE" + exit 1 +fi + +# ---- 대상 DB 확정 ---- +if [ "$USE_TEST" = true ]; then + DB_HOST="$TEST_DB_HOST" + DB_PORT="$TEST_DB_PORT" + DB_USER="$TEST_DB_USER" + DB_NAME="$TEST_DB_NAME" + TARGET_LABEL="TEST" +else + DB_HOST="$PROD_DB_HOST" + DB_PORT="$PROD_DB_PORT" + DB_USER="$PROD_DB_USER" + DB_NAME="$PROD_DB_NAME" + TARGET_LABEL="PROD" +fi + +# Synology NAS 에서는 mariadb CLI 로 설치되는 경우가 많습니다. +if command -v mariadb >/dev/null 2>&1; then + MYSQL_CMD="mariadb" +elif command -v mysql >/dev/null 2>&1; then + MYSQL_CMD="mysql" +else + echo "mariadb / mysql 명령을 찾을 수 없습니다. NAS 에 MariaDB client 를 먼저 설치하세요." + exit 1 +fi + +# 파일명 규칙: maestro__backup_<타임스탬프>.sql +# 규칙에 맞으면 MaestroID 를 뽑아 안내에 노출합니다. (없어도 복원은 정상 동작) +BACKUP_BASENAME=$(basename "$BACKUP_FILE") +DETECTED_MAESTRO_ID="" +if [[ "$BACKUP_BASENAME" =~ ^maestro_([0-9]+)_backup_ ]]; then + DETECTED_MAESTRO_ID="${BASH_REMATCH[1]}" +fi + +echo "복원 대상 : [${TARGET_LABEL}] ${DB_USER}@${DB_HOST}:${DB_PORT}/${DB_NAME}" +echo "백업 파일 : ${BACKUP_FILE}" +if [ -n "$DETECTED_MAESTRO_ID" ]; then + echo "MaestroID : ${DETECTED_MAESTRO_ID} (파일명에서 추출)" +else + echo "MaestroID : (파일명 규칙과 달라 확인 불가)" +fi +echo +echo "주의: 이미 동일한 MaestroID 데이터가 대상 DB에 존재하면" +echo " Primary/Unique Key 충돌로 실패할 수 있습니다." +echo " (신규 서버 또는 데이터가 없는 상태에서 복원을 권장)" +echo +read -r -p "계속 진행하시겠습니까? (y/N) " CONFIRM +if [[ "$CONFIRM" != "y" && "$CONFIRM" != "Y" ]]; then + echo "취소되었습니다." + exit 0 +fi + +# 운영 서버 대상일 경우 실수 방지용 재확인. +if [ "$TARGET_LABEL" = "PROD" ]; then + echo + echo "!! 대상은 운영(PROD) DB 입니다. 실제 데이터가 변경됩니다." + read -r -p "계속하려면 'PROD' 를 입력하세요: " PROD_CONFIRM + if [ "$PROD_CONFIRM" != "PROD" ]; then + echo "취소되었습니다." + exit 0 + fi +fi + +read -r -s -p "DB(${DB_USER}@${DB_HOST}) 비밀번호: " DB_PASS +echo +export MYSQL_PWD="$DB_PASS" + +echo ">> 복원을 시작합니다..." +if "$MYSQL_CMD" --protocol=TCP --default-character-set=utf8mb4 \ + -h"$DB_HOST" -P"$DB_PORT" -u"$DB_USER" "$DB_NAME" < "$BACKUP_FILE"; then + echo "복원이 완료되었습니다." +else + echo "복원 중 오류가 발생했습니다. 백업 파일 내용과 대상 DB 스키마를 확인하세요." + unset MYSQL_PWD + exit 1 +fi + +unset MYSQL_PWD