v4.0.0: 회원·덱 API, 인증 UI, 프로필 및 레이아웃 통일
Express/SQLite API와 Docker 구성을 추가하고 로그인·덱 저장·공개 덱·관리자·프로필 화면을 구현했다. 결과 화면 저장 UX, 헤더 프로필, 애드센스용 app-shell 레이아웃(1280px) 및 문서를 반영했다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
102
api/src/db.js
Normal file
102
api/src/db.js
Normal file
@@ -0,0 +1,102 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* SQLite DB 연결 및 스키마 초기화
|
||||
* @param {string} dbPath
|
||||
* @returns {import('better-sqlite3').Database}
|
||||
*/
|
||||
export function initDb(dbPath) {
|
||||
const dir = path.dirname(dbPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
const db = new Database(dbPath);
|
||||
db.pragma('journal_mode = WAL');
|
||||
db.pragma('foreign_keys = ON');
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
display_name TEXT,
|
||||
avatar_url TEXT,
|
||||
role TEXT NOT NULL DEFAULT 'user',
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS decks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
share_code TEXT NOT NULL UNIQUE,
|
||||
share_type TEXT NOT NULL DEFAULT 'user',
|
||||
content_hash TEXT,
|
||||
user_id INTEGER,
|
||||
series TEXT NOT NULL,
|
||||
series_name TEXT,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
cards_json TEXT NOT NULL,
|
||||
is_public INTEGER NOT NULL DEFAULT 1,
|
||||
view_count INTEGER NOT NULL DEFAULT 0,
|
||||
expires_at TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_decks_user_id ON decks(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_decks_content_hash ON decks(content_hash);
|
||||
`);
|
||||
|
||||
const deckCols = db.prepare('PRAGMA table_info(decks)').all();
|
||||
if (!deckCols.some((c) => c.name === 'preview_image')) {
|
||||
db.exec('ALTER TABLE decks ADD COLUMN preview_image TEXT');
|
||||
}
|
||||
|
||||
const userCols = db.prepare('PRAGMA table_info(users)').all();
|
||||
if (!userCols.some((c) => c.name === 'default_deck_public')) {
|
||||
db.exec('ALTER TABLE users ADD COLUMN default_deck_public INTEGER NOT NULL DEFAULT 1');
|
||||
}
|
||||
if (!userCols.some((c) => c.name === 'skip_deck_public_prompt')) {
|
||||
db.exec('ALTER TABLE users ADD COLUMN skip_deck_public_prompt INTEGER NOT NULL DEFAULT 0');
|
||||
}
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
/**
|
||||
* 환경변수 기준 관리자 계정 시드
|
||||
* ADMIN_EMAIL로 이미 가입한 경우 역할만 admin으로 승격한다.
|
||||
* @param {import('better-sqlite3').Database} db
|
||||
*/
|
||||
export function seedAdmin(db) {
|
||||
const email = process.env.ADMIN_EMAIL?.trim().toLowerCase();
|
||||
const password = process.env.ADMIN_PASSWORD;
|
||||
const username = process.env.ADMIN_USERNAME || 'admin';
|
||||
|
||||
if (!email || !password) return;
|
||||
|
||||
const existingAdmin = db.prepare('SELECT id FROM users WHERE role = ?').get('admin');
|
||||
if (existingAdmin) return;
|
||||
|
||||
const existingUser = db.prepare('SELECT id FROM users WHERE email = ?').get(email);
|
||||
if (existingUser) {
|
||||
db.prepare(`
|
||||
UPDATE users SET role = 'admin', updated_at = datetime('now')
|
||||
WHERE id = ?
|
||||
`).run(existingUser.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const hash = bcrypt.hashSync(password, 12);
|
||||
db.prepare(`
|
||||
INSERT INTO users (email, username, password_hash, display_name, role)
|
||||
VALUES (?, ?, ?, ?, 'admin')
|
||||
`).run(email, username, hash, '관리자');
|
||||
}
|
||||
Reference in New Issue
Block a user