Supabase on-prem 설정 및 마이그레이션 작업
This commit is contained in:
@@ -1,10 +1,18 @@
|
||||
|
||||
// This file is automatically generated. Do not edit it directly.
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import type { Database } from './types';
|
||||
|
||||
const SUPABASE_URL = "https://qnerebtvwwfobfzdoftx.supabase.co";
|
||||
const SUPABASE_PUBLISHABLE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InFuZXJlYnR2d3dmb2JmemRvZnR4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDIwNTE0MzgsImV4cCI6MjA1NzYyNzQzOH0.Wm7h2DUhoQbeANuEM3wm2tz22ITrVEW8FizyLgIVmv8";
|
||||
const SUPABASE_URL = (() => {
|
||||
const url = import.meta.env.VITE_SUPABASE_URL;
|
||||
if (!url) throw new Error("VITE_SUPABASE_URL is not set");
|
||||
return url;
|
||||
})();
|
||||
|
||||
const SUPABASE_PUBLISHABLE_KEY = (() => {
|
||||
const key = import.meta.env.VITE_SUPABASE_ANON_KEY;
|
||||
if (!key) throw new Error("VITE_SUPABASE_ANON_KEY is not set");
|
||||
return key;
|
||||
})();
|
||||
|
||||
// Import the supabase client like this:
|
||||
// import { supabase } from "@/integrations/supabase/client";
|
||||
|
||||
80
src/lib/fullMigrate.js
Executable file
80
src/lib/fullMigrate.js
Executable file
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env node
|
||||
// 스키마 및 데이터를 Supabase Cloud -> On-Prem(a11)으로 완전 복제
|
||||
import 'dotenv/config';
|
||||
import { execSync } from 'child_process';
|
||||
import { URL, fileURLToPath } from 'url';
|
||||
import path from 'path';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const CLOUD_DATABASE_URL = process.env.CLOUD_DATABASE_URL;
|
||||
const ONPREM_SSH_HOST = process.env.ONPREM_SSH_HOST || 'a11';
|
||||
const ONPREM_REMOTE_TMP_DIR = process.env.ONPREM_REMOTE_TMP_DIR || '/root';
|
||||
|
||||
if (!CLOUD_DATABASE_URL) {
|
||||
console.error('환경 변수 CLOUD_DATABASE_URL이 설정되지 않았습니다.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Cloud DB 비밀번호 추출
|
||||
const cloudUrlObj = new URL(CLOUD_DATABASE_URL);
|
||||
const CLOUD_DATABASE_PASSWORD = cloudUrlObj.password;
|
||||
if (!CLOUD_DATABASE_PASSWORD) {
|
||||
console.error('Cloud DB URL에서 비밀번호를 찾을 수 없습니다.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 원격 Postgres 컨테이너 이름 조회
|
||||
console.log('원격 Postgres 컨테이너 조회 (ssh a11)...');
|
||||
let containerName = execSync(
|
||||
`ssh ${ONPREM_SSH_HOST} "docker ps --format '{{.Names}} {{.Image}}' | grep supabase/postgres | awk '{print \\$1}'"`,
|
||||
{ encoding: 'utf8' }
|
||||
).trim();
|
||||
if (!containerName) {
|
||||
console.error('원격 Postgres 컨테이너를 찾을 수 없습니다. docker ps 결과를 확인하세요.');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`발견된 컨테이너: ${containerName}`);
|
||||
|
||||
// 1) 원격 a11에서 Cloud DB 스키마 덤프
|
||||
console.log('원격에서 Cloud DB 스키마 덤프 시작...');
|
||||
execSync(
|
||||
`ssh ${ONPREM_SSH_HOST} "docker run --rm --network host -e PGPASSWORD='${CLOUD_DATABASE_PASSWORD}' postgres:15 ` +
|
||||
`pg_dump --schema-only --no-owner --no-privileges ` +
|
||||
`-h ${cloudUrlObj.hostname} ` +
|
||||
`-p ${cloudUrlObj.port} ` +
|
||||
`-U ${cloudUrlObj.username} ` +
|
||||
`${cloudUrlObj.pathname.slice(1)} > ${ONPREM_REMOTE_TMP_DIR}/supabase_schema.sql"`,
|
||||
{ stdio: 'inherit' }
|
||||
);
|
||||
|
||||
// 2) 원격 a11에서 Cloud DB 데이터 덤프
|
||||
console.log('원격에서 Cloud DB 데이터 덤프 시작...');
|
||||
execSync(
|
||||
`ssh ${ONPREM_SSH_HOST} "docker run --rm --network host -e PGPASSWORD='${CLOUD_DATABASE_PASSWORD}' postgres:15 ` +
|
||||
`pg_dump --data-only --column-inserts --no-owner --no-privileges ` +
|
||||
`-h ${cloudUrlObj.hostname} ` +
|
||||
`-p ${cloudUrlObj.port} ` +
|
||||
`-U ${cloudUrlObj.username} ` +
|
||||
`${cloudUrlObj.pathname.slice(1)} > ${ONPREM_REMOTE_TMP_DIR}/supabase_data.sql"`,
|
||||
{ stdio: 'inherit' }
|
||||
);
|
||||
|
||||
// 4) 원격에 복원 (스키마)
|
||||
console.log('원격 스키마 복원...');
|
||||
execSync(
|
||||
`ssh ${ONPREM_SSH_HOST} "docker exec -i ${containerName} ` +
|
||||
`psql -U postgres -d postgres < ${ONPREM_REMOTE_TMP_DIR}/supabase_schema.sql"`,
|
||||
{ stdio: 'inherit' }
|
||||
);
|
||||
|
||||
// 5) 원격에 복원 (데이터)
|
||||
console.log('원격 데이터 복원...');
|
||||
execSync(
|
||||
`ssh ${ONPREM_SSH_HOST} "docker exec -i ${containerName} ` +
|
||||
`psql -U postgres -d postgres < ${ONPREM_REMOTE_TMP_DIR}/supabase_data.sql"`,
|
||||
{ stdio: 'inherit' }
|
||||
);
|
||||
|
||||
console.log('Cloud → On-Prem 전체 마이그레이션 완료.');
|
||||
@@ -15,39 +15,249 @@ if (!cloudUrl || !cloudKey || !onpremUrl || !onpremKey) {
|
||||
|
||||
const cloud = createClient(cloudUrl, cloudKey);
|
||||
const onprem = createClient(onpremUrl, onpremKey);
|
||||
const tables = ['users', 'accounts', 'transactions'];
|
||||
|
||||
async function migrateTable(table) {
|
||||
console.log(`Migrating table: ${table}`);
|
||||
const { data, error } = await cloud.from(table).select('*');
|
||||
if (error) {
|
||||
if (error.code === '42P01') {
|
||||
console.warn(`Table ${table} not found in Cloud DB, skipping.`);
|
||||
return;
|
||||
// 마이그레이션할 테이블 목록
|
||||
const tables = ['transactions', 'budgets', '_tests'];
|
||||
|
||||
// 테이블 스키마 정의
|
||||
const tableSchemas = {
|
||||
transactions: `
|
||||
CREATE TABLE IF NOT EXISTS transactions (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id UUID REFERENCES auth.users(id) NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
amount NUMERIC NOT NULL,
|
||||
category TEXT NOT NULL,
|
||||
date TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
type TEXT NOT NULL,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Row Level Security 설정
|
||||
ALTER TABLE transactions ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- 사용자 정책 설정 (읽기)
|
||||
DROP POLICY IF EXISTS "사용자는 자신의 트랜잭션만 볼 수 있음" ON transactions;
|
||||
CREATE POLICY "사용자는 자신의 트랜잭션만 볼 수 있음"
|
||||
ON transactions FOR SELECT
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- 사용자 정책 설정 (쓰기)
|
||||
DROP POLICY IF EXISTS "사용자는 자신의 트랜잭션만 추가할 수 있음" ON transactions;
|
||||
CREATE POLICY "사용자는自己的 트랜잭션만 추가할 수 있음"
|
||||
ON transactions FOR INSERT
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
-- 사용자 정책 설정 (업데이트)
|
||||
DROP POLICY IF EXISTS "사용자는 자신의 트랜잭션만 업데이트할 수 있음" ON transactions;
|
||||
CREATE POLICY "사용자는自己的 트랜잭션만 업데이트할 수 있음"
|
||||
ON transactions FOR UPDATE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- 사용자 정책 설정 (삭제)
|
||||
DROP POLICY IF EXISTS "사용자는 자신의 트랜잭션만 삭제할 수 있음" ON transactions;
|
||||
CREATE POLICY "사용자는自己的 트랜잭션만 삭제할 수 있음"
|
||||
ON transactions FOR DELETE
|
||||
USING (auth.uid() = user_id);
|
||||
`,
|
||||
budgets: `
|
||||
CREATE TABLE IF NOT EXISTS budgets (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id UUID REFERENCES auth.users(id) NOT NULL,
|
||||
month INTEGER NOT NULL,
|
||||
year INTEGER NOT NULL,
|
||||
total_budget NUMERIC NOT NULL DEFAULT 0,
|
||||
categories JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
UNIQUE (user_id, month, year)
|
||||
);
|
||||
|
||||
-- Row Level Security 설정
|
||||
ALTER TABLE budgets ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- 사용자 정책 설정 (읽기)
|
||||
DROP POLICY IF EXISTS "사용자는自己的 예산만 볼 수 있음" ON budgets;
|
||||
CREATE POLICY "사용자는自己的 예산만 볼 수 있음"
|
||||
ON budgets FOR SELECT
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- 사용자 정책 설정 (쓰기)
|
||||
DROP POLICY IF EXISTS "사용자는自己的 예산만 추가할 수 있음" ON budgets;
|
||||
CREATE POLICY "사용자는自己的 예산만 추가할 수 있음"
|
||||
ON budgets FOR INSERT
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
-- 사용자 정책 설정 (업데이트)
|
||||
DROP POLICY IF EXISTS "사용자는自己的 예산만 업데이트할 수 있음" ON budgets;
|
||||
CREATE POLICY "사용자는自己的 예산만 업데이트할 수 있음"
|
||||
ON budgets FOR UPDATE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- 사용자 정책 설정 (삭제)
|
||||
DROP POLICY IF EXISTS "사용자는自己的 예산만 삭제할 수 있음" ON budgets;
|
||||
CREATE POLICY "사용자는自己的 예산만 삭제할 수 있음"
|
||||
ON budgets FOR DELETE
|
||||
USING (auth.uid() = user_id);
|
||||
`,
|
||||
_tests: `
|
||||
CREATE TABLE IF NOT EXISTS _tests (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
test_name TEXT NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 모든 사용자가 접근 가능하도록 설정
|
||||
ALTER TABLE _tests ENABLE ROW LEVEL SECURITY;
|
||||
DROP POLICY IF EXISTS "모든 사용자가 테스트 테이블에 접근 가능" ON _tests;
|
||||
CREATE POLICY "모든 사용자가 테스트 테이블에 접근 가능"
|
||||
ON _tests FOR SELECT
|
||||
USING (true);
|
||||
`
|
||||
};
|
||||
|
||||
/**
|
||||
* 헬퍼 함수 생성
|
||||
*/
|
||||
async function createHelperFunctions() {
|
||||
console.log('헬퍼 함수 생성 중...');
|
||||
|
||||
// execute_sql 함수 생성
|
||||
const executeSqlSQL = `
|
||||
CREATE OR REPLACE FUNCTION execute_sql(sql_query TEXT)
|
||||
RETURNS VOID AS $$
|
||||
BEGIN
|
||||
EXECUTE sql_query;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
`;
|
||||
|
||||
const { error: execFnError } = await onprem.rpc('execute_sql', { sql_query: executeSqlSQL });
|
||||
if (execFnError) {
|
||||
console.error('execute_sql 함수 생성 실패:', execFnError);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('헬퍼 함수 생성 완료');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 생성
|
||||
*/
|
||||
async function createTable(tableName) {
|
||||
console.log(`테이블 생성 중: ${tableName}`);
|
||||
|
||||
if (!tableSchemas[tableName]) {
|
||||
console.warn(`${tableName} 테이블의 스키마 정보가 없습니다.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 테이블 생성 SQL 실행
|
||||
const { error } = await onprem.rpc('execute_sql', { sql_query: tableSchemas[tableName] });
|
||||
|
||||
if (error) {
|
||||
console.error(`${tableName} 테이블 생성 실패:`, error);
|
||||
return false;
|
||||
}
|
||||
console.error(`Error fetching ${table}:`, error);
|
||||
return;
|
||||
}
|
||||
if (!data.length) {
|
||||
console.log(`${table} has no data to migrate.`);
|
||||
return;
|
||||
}
|
||||
const { error: insertError } = await onprem.from(table).upsert(data);
|
||||
if (insertError) {
|
||||
console.error(`Error inserting into ${table}:`, insertError);
|
||||
} else {
|
||||
console.log(`Migrated ${data.length} rows into ${table}`);
|
||||
|
||||
console.log(`${tableName} 테이블 생성 완료`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`${tableName} 테이블 생성 중 오류:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 데이터 마이그레이션
|
||||
*/
|
||||
async function migrateTableData(tableName) {
|
||||
console.log(`테이블 데이터 마이그레이션 중: ${tableName}`);
|
||||
|
||||
try {
|
||||
// Cloud DB에서 데이터 가져오기
|
||||
const { data, error } = await cloud.from(tableName).select('*');
|
||||
|
||||
if (error) {
|
||||
if (error.code === '42P01') {
|
||||
console.warn(`Cloud DB에 ${tableName} 테이블이 없습니다. 건너뜁니다.`);
|
||||
return true;
|
||||
}
|
||||
console.error(`Cloud DB에서 ${tableName} 데이터 가져오기 실패:`, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
console.log(`${tableName} 테이블에 마이그레이션할 데이터가 없습니다.`);
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log(`${tableName} 테이블에서 ${data.length}개 행을 가져왔습니다.`);
|
||||
|
||||
// 데이터를 작은 배치로 나누어 삽입 (트랜잭션 삭제 안전성 고려)
|
||||
const batchSize = 100;
|
||||
for (let i = 0; i < data.length; i += batchSize) {
|
||||
const batch = data.slice(i, i + batchSize);
|
||||
const { error: insertError } = await onprem.from(tableName).upsert(batch);
|
||||
|
||||
if (insertError) {
|
||||
console.error(`${tableName} 테이블에 데이터 삽입 실패:`, insertError);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`${tableName} 테이블에 ${batch.length}개 행 삽입 완료 (${i + batch.length}/${data.length})`);
|
||||
|
||||
// 비동기 작업 사이에 짧은 지연 추가 (UI 스레드 차단 방지)
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
console.log(`${tableName} 테이블 데이터 마이그레이션 완료`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`${tableName} 테이블 데이터 마이그레이션 중 오류:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 메인 마이그레이션 함수
|
||||
*/
|
||||
async function main() {
|
||||
for (const table of tables) {
|
||||
await migrateTable(table);
|
||||
try {
|
||||
console.log('Supabase Cloud → On-Prem 마이그레이션 시작');
|
||||
|
||||
// 헬퍼 함수 생성
|
||||
const helperCreated = await createHelperFunctions();
|
||||
if (!helperCreated) {
|
||||
console.warn('헬퍼 함수 생성에 실패했습니다. 계속 진행합니다.');
|
||||
}
|
||||
|
||||
// 각 테이블에 대해 스키마 생성 및 데이터 마이그레이션 수행
|
||||
for (const tableName of tables) {
|
||||
// 테이블 생성
|
||||
const tableCreated = await createTable(tableName);
|
||||
if (!tableCreated) {
|
||||
console.warn(`${tableName} 테이블 생성에 실패했습니다. 데이터 마이그레이션을 건너뜁니다.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 데이터 마이그레이션
|
||||
const dataMigrated = await migrateTableData(tableName);
|
||||
if (!dataMigrated) {
|
||||
console.warn(`${tableName} 테이블 데이터 마이그레이션에 실패했습니다.`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Supabase Cloud → On-Prem 마이그레이션 완료');
|
||||
} catch (error) {
|
||||
console.error('마이그레이션 중 오류 발생:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('Migration complete.');
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('Migration failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
// 마이그레이션 실행
|
||||
main();
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
|
||||
// Supabase Cloud URL과 anon key 설정
|
||||
export const getSupabaseUrl = () => {
|
||||
return "https://qnerebtvwwfobfzdoftx.supabase.co";
|
||||
const url = import.meta.env.VITE_SUPABASE_URL;
|
||||
if (!url) throw new Error("VITE_SUPABASE_URL is not set");
|
||||
return url;
|
||||
};
|
||||
|
||||
export const getSupabaseKey = () => {
|
||||
return "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InFuZXJlYnR2d3dmb2JmemRvZnR4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDIwNTE0MzgsImV4cCI6MjA1NzYyNzQzOH0.Wm7h2DUhoQbeANuEM3wm2tz22ITrVEW8FizyLgIVmv8";
|
||||
const key = import.meta.env.VITE_SUPABASE_ANON_KEY;
|
||||
if (!key) throw new Error("VITE_SUPABASE_ANON_KEY is not set");
|
||||
return key;
|
||||
};
|
||||
|
||||
// Supabase 키 유효성 검사 - Cloud 환경에서는 항상 유효함
|
||||
export const isValidSupabaseKey = () => {
|
||||
return true;
|
||||
return Boolean(import.meta.env.VITE_SUPABASE_ANON_KEY);
|
||||
};
|
||||
|
||||
// 다음 함수들은 Cloud 환경에서는 필요 없지만 호환성을 위해 유지
|
||||
|
||||
Reference in New Issue
Block a user