CASTAD-v0.1/server/db.js

507 lines
21 KiB
JavaScript

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;