Files
Community_Nursing_Atlas/server/db.py
T
jsahn 4e943700fc feat: add common utilities and feedback handling scripts
- Introduced common.js for shared functions including API calls, error reporting, and UI helpers.
- Implemented feedback.js for the feedback reporting page, including form handling and displaying feedback status.
- Added layout.js for managing galaxy and section layouts in the visualization.
- Created login.js for user authentication with session management.
- Developed search.js for term searching based on user input and synonyms.
- Implemented signup.js for user registration with validation and immediate login.
2026-10-06 23:14:45 +09:00

225 lines
7.8 KiB
Python

"""SQLite 저장소. 스키마, 연결, JSON 컬럼 도우미."""
import json
import os
import sqlite3
import threading
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
DB_PATH = Path(os.environ.get("ATLAS_DB", ROOT / "data" / "atlas.db"))
SCHEMA = """
CREATE TABLE IF NOT EXISTS books (
book_id TEXT PRIMARY KEY, title TEXT, short TEXT, edition TEXT, year INTEGER,
is_primary INTEGER, filename TEXT
);
CREATE TABLE IF NOT EXISTS galaxies (
galaxy_id TEXT PRIMARY KEY, name TEXT, sub TEXT, color TEXT, ord INTEGER, chapter_no INTEGER,
edited INTEGER DEFAULT 0, updated_at TEXT
);
CREATE TABLE IF NOT EXISTS toc (
toc_id TEXT PRIMARY KEY, book_id TEXT, kind TEXT, level INTEGER, number TEXT, title TEXT,
page INTEGER, parent_id TEXT, galaxy_id TEXT, easy_sub TEXT, edited INTEGER DEFAULT 0
);
CREATE TABLE IF NOT EXISTS terms (
term_id TEXT PRIMARY KEY,
name TEXT NOT NULL,
kind TEXT DEFAULT 'concept',
lang TEXT DEFAULT 'ko',
aliases TEXT DEFAULT '[]',
search_terms TEXT DEFAULT '[]',
galaxy_id TEXT,
section_id TEXT,
section_guess INTEGER DEFAULT 0,
item_id TEXT,
section_ids TEXT DEFAULT '[]',
definition_text TEXT, definition_book TEXT, definition_page INTEGER, definition_method TEXT,
easy TEXT, example TEXT, easy_method TEXT,
review_status TEXT DEFAULT 'draft',
hidden INTEGER DEFAULT 0,
origin TEXT DEFAULT 'auto',
edited INTEGER DEFAULT 0,
reviewer TEXT, note TEXT,
created_at TEXT, updated_at TEXT
);
CREATE INDEX IF NOT EXISTS terms_galaxy ON terms(galaxy_id);
CREATE INDEX IF NOT EXISTS terms_section ON terms(section_id);
CREATE TABLE IF NOT EXISTS term_sources (
id INTEGER PRIMARY KEY AUTOINCREMENT,
term_id TEXT NOT NULL, book_id TEXT, page INTEGER, pdf_page INTEGER,
toc_id TEXT, section_id TEXT, galaxy_id TEXT, path TEXT, snippet TEXT,
origin TEXT DEFAULT 'auto'
);
CREATE INDEX IF NOT EXISTS sources_term ON term_sources(term_id);
CREATE TABLE IF NOT EXISTS relations (
relation_id TEXT PRIMARY KEY,
source TEXT NOT NULL, target TEXT NOT NULL,
type TEXT NOT NULL, label TEXT, reason TEXT,
directed INTEGER DEFAULT 1,
method TEXT, evidence TEXT DEFAULT '[]',
review_status TEXT DEFAULT 'draft',
hidden INTEGER DEFAULT 0,
edited INTEGER DEFAULT 0,
reviewer TEXT, created_at TEXT, updated_at TEXT
);
CREATE INDEX IF NOT EXISTS rel_source ON relations(source);
CREATE INDEX IF NOT EXISTS rel_target ON relations(target);
CREATE TABLE IF NOT EXISTS feedback (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at TEXT, updated_at TEXT,
kind TEXT, -- content | bug | search | suggestion | client_error
category TEXT,
target_type TEXT, target_id TEXT, target_label TEXT,
message TEXT, suggestion TEXT,
page_url TEXT, user_agent TEXT, context TEXT,
reporter TEXT, contact TEXT,
status TEXT DEFAULT 'new', -- new | in_progress | resolved | rejected | duplicate
admin_note TEXT, resolution TEXT, handled_by TEXT,
occurrences INTEGER DEFAULT 1, fingerprint TEXT
);
CREATE INDEX IF NOT EXISTS feedback_status ON feedback(status);
CREATE INDEX IF NOT EXISTS feedback_fp ON feedback(fingerprint);
CREATE TABLE IF NOT EXISTS audit (
id INTEGER PRIMARY KEY AUTOINCREMENT,
at TEXT, actor TEXT, action TEXT, entity TEXT, entity_id TEXT, before TEXT, after TEXT
);
CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT);
CREATE TABLE IF NOT EXISTS admins (
admin_id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE COLLATE NOCASE,
password_hash TEXT NOT NULL, name TEXT,
created_at TEXT, last_login TEXT
);
CREATE TABLE IF NOT EXISTS classes (
class_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, note TEXT, created_at TEXT
);
CREATE TABLE IF NOT EXISTS users (
user_id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE COLLATE NOCASE,
password_hash TEXT NOT NULL,
name TEXT NOT NULL, student_no TEXT, note TEXT,
status TEXT DEFAULT 'pending', -- pending | approved | rejected | disabled
class_id INTEGER,
created_at TEXT, approved_at TEXT, approved_by TEXT, last_login TEXT
);
CREATE INDEX IF NOT EXISTS users_status ON users(status);
CREATE TABLE IF NOT EXISTS signup_codes (
code_id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT NOT NULL UNIQUE COLLATE NOCASE,
class_id INTEGER, note TEXT,
active INTEGER DEFAULT 1, auto_approve INTEGER DEFAULT 0,
max_uses INTEGER, uses INTEGER DEFAULT 0, expires_at TEXT,
created_by TEXT, created_at TEXT
);
CREATE TABLE IF NOT EXISTS questions (
question_id INTEGER PRIMARY KEY AUTOINCREMENT,
term_id TEXT, galaxy_id TEXT,
qtype TEXT DEFAULT 'mcq', -- mcq | ox
prompt TEXT NOT NULL, choices TEXT DEFAULT '[]', answer INTEGER NOT NULL,
explanation TEXT,
review_status TEXT DEFAULT 'draft', hidden INTEGER DEFAULT 0,
created_by TEXT, created_at TEXT, updated_at TEXT
);
CREATE INDEX IF NOT EXISTS questions_term ON questions(term_id);
CREATE TABLE IF NOT EXISTS attempts (
attempt_id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER, class_id INTEGER,
qkey TEXT, term_id TEXT, galaxy_id TEXT, qtype TEXT,
choice INTEGER, correct INTEGER, created_at TEXT
);
CREATE INDEX IF NOT EXISTS attempts_user ON attempts(user_id);
CREATE INDEX IF NOT EXISTS attempts_class ON attempts(class_id);
CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);
"""
DEFAULT_SETTINGS = {
# 학생 화면에 보여줄 검수 상태
"public_statuses": ["draft", "review", "confirmed"],
# 학생 화면에 '검수 전' 표시
"show_status_badges": True,
}
JSON_COLS = {"aliases", "search_terms", "section_ids", "evidence", "choices"}
_local = threading.local()
def now() -> str:
return time.strftime("%Y-%m-%dT%H:%M:%S")
def connect(path: Path | None = None) -> sqlite3.Connection:
p = Path(path or DB_PATH)
p.parent.mkdir(parents=True, exist_ok=True)
con = sqlite3.connect(p, check_same_thread=False, timeout=15)
con.row_factory = sqlite3.Row
con.execute("PRAGMA journal_mode=WAL")
con.execute("PRAGMA foreign_keys=ON")
con.executescript(SCHEMA)
_migrate(con)
return con
def _migrate(con):
"""기존 DB에 새 컬럼 추가."""
cols = {r[1] for r in con.execute("PRAGMA table_info(users)")}
if "signup_code_id" not in cols:
con.execute("ALTER TABLE users ADD COLUMN signup_code_id INTEGER")
con.commit()
def get() -> sqlite3.Connection:
con = getattr(_local, "con", None)
if con is None or getattr(_local, "path", None) != str(DB_PATH):
con = _local.con = connect()
_local.path = str(DB_PATH)
return con
def row(r: sqlite3.Row | None) -> dict | None:
if r is None:
return None
d = dict(r)
for k in JSON_COLS & d.keys():
try:
d[k] = json.loads(d[k]) if d[k] else []
except (TypeError, json.JSONDecodeError):
d[k] = []
return d
def rows(rs) -> list[dict]:
return [row(r) for r in rs]
def dumps(v) -> str:
return json.dumps(v, ensure_ascii=False)
def get_setting(con, key):
r = con.execute("SELECT value FROM settings WHERE key=?", (key,)).fetchone()
if r is None:
return DEFAULT_SETTINGS.get(key)
return json.loads(r["value"])
def all_settings(con) -> dict:
out = dict(DEFAULT_SETTINGS)
for r in con.execute("SELECT key, value FROM settings"):
out[r["key"]] = json.loads(r["value"])
return out
def set_setting(con, key, value):
con.execute("INSERT INTO settings(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value",
(key, dumps(value)))
def audit(con, actor, action, entity, entity_id, before=None, after=None):
con.execute(
"INSERT INTO audit(at, actor, action, entity, entity_id, before, after) VALUES(?,?,?,?,?,?,?)",
(now(), actor, action, entity, entity_id,
dumps(before) if before is not None else None, dumps(after) if after is not None else None),
)