const sqlite3 = require('sqlite3').verbose(); const path = require('path'); const bcrypt = require('bcrypt'); const DB_PATH = path.join(__dirname, 'database.sqlite'); const db = new sqlite3.Database(DB_PATH, (err) => { if (err) { console.error('데이터베이스 연결 실패:', err.message); } else { console.log('SQLite 데이터베이스에 연결되었습니다.'); } }); // 테이블 초기화 db.serialize(() => { // Users 테이블 db.run(`CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, email TEXT UNIQUE, password TEXT NOT NULL, name TEXT, phone TEXT, role TEXT DEFAULT 'user', approved INTEGER DEFAULT 0, email_verified INTEGER DEFAULT 0, verification_token TEXT, reset_token TEXT, reset_token_expiry DATETIME, createdAt DATETIME DEFAULT CURRENT_TIMESTAMP )`); // 기존 테이블에 새 컬럼 추가 (이미 존재하면 무시) db.run("ALTER TABLE users ADD COLUMN email TEXT UNIQUE", (err) => {}); db.run("ALTER TABLE users ADD COLUMN email_verified INTEGER DEFAULT 0", (err) => {}); db.run("ALTER TABLE users ADD COLUMN verification_token TEXT", (err) => {}); db.run("ALTER TABLE users ADD COLUMN reset_token TEXT", (err) => {}); db.run("ALTER TABLE users ADD COLUMN reset_token_expiry DATETIME", (err) => {}); // OAuth 컬럼 추가 db.run("ALTER TABLE users ADD COLUMN oauth_provider TEXT", (err) => {}); db.run("ALTER TABLE users ADD COLUMN oauth_provider_id TEXT", (err) => {}); db.run("ALTER TABLE users ADD COLUMN profile_image TEXT", (err) => {}); // 크레딧 컬럼 추가 (무료 플랜 기본값 10) db.run("ALTER TABLE users ADD COLUMN credits INTEGER DEFAULT 10", (err) => {}); // 구독 플랜 컬럼 추가 (free, basic, pro, business) db.run("ALTER TABLE users ADD COLUMN plan_type TEXT DEFAULT 'free'", (err) => {}); // 최대 펜션 수 (free/basic: 1, pro: 5, business: unlimited) db.run("ALTER TABLE users ADD COLUMN max_pensions INTEGER DEFAULT 1", (err) => {}); // 월간 크레딧 한도 (플랜별 다름, 무료=10) db.run("ALTER TABLE users ADD COLUMN monthly_credits INTEGER DEFAULT 10", (err) => {}); // 구독 시작일 db.run("ALTER TABLE users ADD COLUMN subscription_started_at DATETIME", (err) => {}); // 구독 만료일 db.run("ALTER TABLE users ADD COLUMN subscription_expires_at DATETIME", (err) => {}); // ============================================ // 펜션/브랜드 프로필 테이블 (다중 펜션 지원) // ============================================ db.run(`CREATE TABLE IF NOT EXISTS pension_profiles ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, is_default INTEGER DEFAULT 0, brand_name TEXT, brand_name_en TEXT, region TEXT, address TEXT, pension_types TEXT, target_customers TEXT, key_features TEXT, nearby_attractions TEXT, booking_url TEXT, homepage_url TEXT, kakao_channel TEXT, instagram_handle TEXT, languages TEXT DEFAULT 'KO', price_range TEXT, description TEXT, createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE )`); // 펜션별 is_default 컬럼 추가 (기존 테이블용) db.run("ALTER TABLE pension_profiles ADD COLUMN is_default INTEGER DEFAULT 0", (err) => { // 이미 존재하면 에러가 발생하므로 무시 }); // 펜션별 YouTube 플레이리스트 ID 직접 연결 db.run("ALTER TABLE pension_profiles ADD COLUMN youtube_playlist_id TEXT", (err) => {}); db.run("ALTER TABLE pension_profiles ADD COLUMN youtube_playlist_title TEXT", (err) => {}); // ============================================ // YouTube 분석 데이터 캐시 테이블 (펜션별) // ============================================ db.run(`CREATE TABLE IF NOT EXISTS youtube_analytics ( id INTEGER PRIMARY KEY AUTOINCREMENT, pension_id INTEGER NOT NULL, playlist_id TEXT NOT NULL, date DATE NOT NULL, views INTEGER DEFAULT 0, playlist_starts INTEGER DEFAULT 0, average_time_in_playlist REAL DEFAULT 0, estimated_minutes_watched REAL DEFAULT 0, subscribers_gained INTEGER DEFAULT 0, likes INTEGER DEFAULT 0, comments INTEGER DEFAULT 0, shares INTEGER DEFAULT 0, cached_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(pension_id) REFERENCES pension_profiles(id) ON DELETE CASCADE, UNIQUE(pension_id, date) )`); // 펜션별 월간 요약 통계 테이블 db.run(`CREATE TABLE IF NOT EXISTS pension_monthly_stats ( id INTEGER PRIMARY KEY AUTOINCREMENT, pension_id INTEGER NOT NULL, year_month TEXT NOT NULL, total_views INTEGER DEFAULT 0, total_videos INTEGER DEFAULT 0, total_watch_time REAL DEFAULT 0, avg_view_duration REAL DEFAULT 0, top_video_id TEXT, growth_rate REAL DEFAULT 0, cached_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(pension_id) REFERENCES pension_profiles(id) ON DELETE CASCADE, UNIQUE(pension_id, year_month) )`); // ============================================ // YouTube OAuth 토큰 테이블 // ============================================ db.run(`CREATE TABLE IF NOT EXISTS youtube_connections ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER UNIQUE NOT NULL, google_user_id TEXT, google_email TEXT, youtube_channel_id TEXT, youtube_channel_title TEXT, access_token TEXT, refresh_token TEXT, token_expiry DATETIME, scopes TEXT, connected_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE )`); // ============================================ // YouTube 업로드 설정 테이블 // ============================================ db.run(`CREATE TABLE IF NOT EXISTS youtube_settings ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER UNIQUE NOT NULL, default_privacy TEXT DEFAULT 'private', default_category_id TEXT DEFAULT '19', default_tags TEXT, default_hashtags TEXT, auto_upload INTEGER DEFAULT 0, upload_timing TEXT DEFAULT 'manual', scheduled_day TEXT, scheduled_time TEXT, default_playlist_id TEXT, notify_on_upload INTEGER DEFAULT 1, createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE )`); // ============================================ // YouTube 플레이리스트 캐시 테이블 (펜션별 연결 지원) // ============================================ db.run(`CREATE TABLE IF NOT EXISTS youtube_playlists ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, pension_id INTEGER, playlist_id TEXT NOT NULL, title TEXT, item_count INTEGER DEFAULT 0, cached_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY(pension_id) REFERENCES pension_profiles(id) ON DELETE SET NULL, UNIQUE(user_id, playlist_id) )`); // 플레이리스트에 pension_id 컬럼 추가 (기존 테이블용) db.run("ALTER TABLE youtube_playlists ADD COLUMN pension_id INTEGER", (err) => { // 이미 존재하면 에러가 발생하므로 무시 }); // ============================================ // 업로드 히스토리 테이블 (펜션별 연결 지원) // ============================================ db.run(`CREATE TABLE IF NOT EXISTS upload_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, pension_id INTEGER, history_id INTEGER, youtube_video_id TEXT, youtube_url TEXT, title TEXT, privacy_status TEXT, playlist_id TEXT, uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP, status TEXT DEFAULT 'completed', error_message TEXT, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY(pension_id) REFERENCES pension_profiles(id) ON DELETE SET NULL, FOREIGN KEY(history_id) REFERENCES history(id) ON DELETE SET NULL )`); // 업로드 히스토리에 pension_id 컬럼 추가 (기존 테이블용) db.run("ALTER TABLE upload_history ADD COLUMN pension_id INTEGER", (err) => { // 이미 존재하면 에러가 발생하므로 무시 }); // ============================================ // Instagram 연결 정보 테이블 // ============================================ db.run(`CREATE TABLE IF NOT EXISTS instagram_connections ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER UNIQUE NOT NULL, instagram_username TEXT NOT NULL, encrypted_password TEXT NOT NULL, encrypted_session TEXT, is_active INTEGER DEFAULT 1, last_login_at DATETIME, two_factor_required INTEGER DEFAULT 0, connected_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE )`); // ============================================ // Instagram 업로드 설정 테이블 // ============================================ db.run(`CREATE TABLE IF NOT EXISTS instagram_settings ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER UNIQUE NOT NULL, auto_upload INTEGER DEFAULT 0, upload_as_reel INTEGER DEFAULT 1, default_caption_template TEXT, default_hashtags TEXT, max_uploads_per_week INTEGER DEFAULT 1, notify_on_upload INTEGER DEFAULT 1, createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE )`); // ============================================ // Instagram 업로드 히스토리 테이블 // ============================================ db.run(`CREATE TABLE IF NOT EXISTS instagram_upload_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, pension_id INTEGER, history_id INTEGER, instagram_media_id TEXT, instagram_post_code TEXT, permalink TEXT, caption TEXT, upload_type TEXT DEFAULT 'reel', status TEXT DEFAULT 'pending', error_message TEXT, retry_count INTEGER DEFAULT 0, uploaded_at DATETIME, createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY(pension_id) REFERENCES pension_profiles(id) ON DELETE SET NULL, FOREIGN KEY(history_id) REFERENCES history(id) ON DELETE SET NULL )`); // ============================================ // TikTok 연결 정보 테이블 // ============================================ db.run(`CREATE TABLE IF NOT EXISTS tiktok_connections ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER UNIQUE NOT NULL, open_id TEXT NOT NULL, display_name TEXT, avatar_url TEXT, follower_count INTEGER DEFAULT 0, following_count INTEGER DEFAULT 0, access_token TEXT NOT NULL, refresh_token TEXT, token_expiry DATETIME, scopes TEXT, connected_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE )`); // ============================================ // TikTok 업로드 설정 테이블 // ============================================ db.run(`CREATE TABLE IF NOT EXISTS tiktok_settings ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER UNIQUE NOT NULL, default_privacy TEXT DEFAULT 'SELF_ONLY', disable_duet INTEGER DEFAULT 0, disable_comment INTEGER DEFAULT 0, disable_stitch INTEGER DEFAULT 0, auto_upload INTEGER DEFAULT 0, upload_to_inbox INTEGER DEFAULT 1, default_hashtags TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE )`); // ============================================ // TikTok 업로드 히스토리 테이블 // ============================================ db.run(`CREATE TABLE IF NOT EXISTS tiktok_upload_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, pension_id INTEGER, history_id INTEGER, publish_id TEXT, video_id TEXT, title TEXT, privacy_level TEXT DEFAULT 'SELF_ONLY', status TEXT DEFAULT 'pending', error_message TEXT, uploaded_at DATETIME, createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY(pension_id) REFERENCES pension_profiles(id) ON DELETE SET NULL, FOREIGN KEY(history_id) REFERENCES history(id) ON DELETE SET NULL )`); // ============================================ // 플랫폼 통합 통계 테이블 (YouTube, Instagram, TikTok) // ============================================ db.run(`CREATE TABLE IF NOT EXISTS platform_stats ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, pension_id INTEGER, platform TEXT NOT NULL, date DATE NOT NULL, views INTEGER DEFAULT 0, likes INTEGER DEFAULT 0, comments INTEGER DEFAULT 0, shares INTEGER DEFAULT 0, followers_gained INTEGER DEFAULT 0, impressions INTEGER DEFAULT 0, reach INTEGER DEFAULT 0, engagement_rate REAL DEFAULT 0, cached_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY(pension_id) REFERENCES pension_profiles(id) ON DELETE SET NULL, UNIQUE(user_id, pension_id, platform, date) )`); // ============================================ // 시스템 활동 로그 테이블 (어드민 분석용) // ============================================ db.run(`CREATE TABLE IF NOT EXISTS activity_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, action_type TEXT NOT NULL, action_detail TEXT, ip_address TEXT, user_agent TEXT, metadata TEXT, createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE SET NULL )`); // ============================================ // 시스템 통계 스냅샷 테이블 (일별 집계) // ============================================ db.run(`CREATE TABLE IF NOT EXISTS system_stats_daily ( id INTEGER PRIMARY KEY AUTOINCREMENT, date DATE UNIQUE NOT NULL, total_users INTEGER DEFAULT 0, new_users INTEGER DEFAULT 0, active_users INTEGER DEFAULT 0, total_videos_generated INTEGER DEFAULT 0, total_uploads INTEGER DEFAULT 0, youtube_uploads INTEGER DEFAULT 0, instagram_uploads INTEGER DEFAULT 0, tiktok_uploads INTEGER DEFAULT 0, total_credits_used INTEGER DEFAULT 0, avg_generation_time REAL DEFAULT 0, createdAt DATETIME DEFAULT CURRENT_TIMESTAMP )`); // ============================================ // 사용자 에셋 테이블 (이미지, 오디오, 비디오) // ============================================ db.run(`CREATE TABLE IF NOT EXISTS user_assets ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, pension_id INTEGER, history_id INTEGER, asset_type TEXT NOT NULL, source_type TEXT NOT NULL, file_name TEXT NOT NULL, file_path TEXT NOT NULL, file_size INTEGER DEFAULT 0, mime_type TEXT, thumbnail_path TEXT, duration REAL, width INTEGER, height INTEGER, metadata TEXT, is_deleted INTEGER DEFAULT 0, createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY(pension_id) REFERENCES pension_profiles(id) ON DELETE SET NULL, FOREIGN KEY(history_id) REFERENCES history(id) ON DELETE SET NULL )`); // 사용자별 스토리지 한도 컬럼 추가 (MB 단위, 기본 500MB) db.run("ALTER TABLE users ADD COLUMN storage_limit INTEGER DEFAULT 500", (err) => {}); // 현재 사용 용량 (캐시) db.run("ALTER TABLE users ADD COLUMN storage_used INTEGER DEFAULT 0", (err) => {}); // ============================================ // 크레딧 요청 테이블 // ============================================ db.run(`CREATE TABLE IF NOT EXISTS credit_requests ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, requested_credits INTEGER DEFAULT 10, status TEXT DEFAULT 'pending', reason TEXT, admin_note TEXT, processed_by INTEGER, processed_at DATETIME, createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY(processed_by) REFERENCES users(id) ON DELETE SET NULL )`); // ============================================ // 크레딧 히스토리 테이블 (변동 내역 추적) // ============================================ db.run(`CREATE TABLE IF NOT EXISTS credit_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, amount INTEGER NOT NULL, type TEXT NOT NULL, description TEXT, balance_after INTEGER, related_request_id INTEGER, createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY(related_request_id) REFERENCES credit_requests(id) ON DELETE SET NULL )`); // 기존 테이블에 business_name 컬럼 추가 (존재하지 않을 경우를 대비해 try-catch 대신 별도 실행) // SQLite는 IF NOT EXISTS 컬럼 추가를 지원하지 않으므로, 에러를 무시하는 방식으로 처리하거나 스키마 버전을 관리해야 함. // 여기서는 간단히 컬럼 추가 시도 후 에러 무시 패턴을 사용. db.run("ALTER TABLE users ADD COLUMN business_name TEXT", (err) => { // 이미 존재하면 에러가 발생하므로 무시 }); db.run("ALTER TABLE history ADD COLUMN final_video_path TEXT", (err) => { // 이미 존재하면 에러가 발생하므로 무시 }); db.run("ALTER TABLE history ADD COLUMN poster_path TEXT", (err) => { // 이미 존재하면 에러가 발생하므로 무시 }); db.run("ALTER TABLE history ADD COLUMN pension_id INTEGER", (err) => { // 이미 존재하면 에러가 발생하므로 무시 }); // History 테이블 db.run(`CREATE TABLE IF NOT EXISTS history ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, business_name TEXT, details TEXT, createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(user_id) REFERENCES users(id) )`); // 기본 관리자 계정 생성 (존재하지 않을 경우) const adminUsername = 'admin'; const adminPassword = 'admin123'; // 초기 비밀번호 db.get("SELECT * FROM users WHERE username = ?", [adminUsername], (err, row) => { if (!row) { const salt = bcrypt.genSaltSync(10); const hash = bcrypt.hashSync(adminPassword, salt); db.run(`INSERT INTO users (username, password, name, phone, role, approved, credits) VALUES (?, ?, ?, ?, ?, ?, ?)`, [adminUsername, hash, 'Super Admin', '000-0000-0000', 'admin', 1, 999999], (err) => { if (err) console.error("초기 관리자 생성 실패:", err); else console.log(`초기 관리자 계정 생성 완료. (ID: ${adminUsername}, PW: ${adminPassword})`); }); } else if (row.role === 'admin' && (row.credits === null || row.credits < 999999)) { // 기존 관리자에게 무제한 크레딧 부여 db.run("UPDATE users SET credits = 999999 WHERE id = ?", [row.id]); } }); }); module.exports = db;