diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..72809ae --- /dev/null +++ b/.dockerignore @@ -0,0 +1,23 @@ +.git +.gitignore +.env +.env.* +!.env.example +.venv +venv +__pycache__ +*.py[cod] +*.db +*.db-shm +*.db-wal +data +config/*.json +frontend/node_modules +frontend/dist +tests +.pytest_cache +.mypy_cache +.ruff_cache +*.md +compose*.yml +Dockerfile* diff --git a/.env.example b/.env.example index 330d923..83a750b 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,15 @@ # JWT 시그니용 키 (변경 권장) JWT_SECRET_KEY=my-super-secret-key-change-me -# JWT_SECRET_KEY 미설정 시 생성할 키 파일 (기본: config/.jwt-secret) -# JWT_SECRET_FILE=config/.jwt-secret +# JWT_SECRET_KEY 미설정 시 생성할 키 파일 (기본: data/.jwt-secret) +# JWT_SECRET_FILE=data/.jwt-secret + +# 영속 SQLite 데이터베이스 +DATABASE_PATH=data/translator.db + +# users.json이 없고 DB가 비어 있을 때만 사용할 최초 관리자 +# BOOTSTRAP_ADMIN_ID=admin +# BOOTSTRAP_ADMIN_PASSWORD=change-this-password # LLM 설정 파일 경로 (기본: config/llms.json) LLMS_CONFIG_PATH=config/llms.json diff --git a/.gitignore b/.gitignore index 18c2811..9400048 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,12 @@ __pycache__/ # Build output frontend/dist/ +# Persistent application data +data/ +*.db +*.db-shm +*.db-wal + # Env files .env config/.jwt-secret diff --git a/AGENTS.md b/AGENTS.md index 818a716..97648f7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,7 +32,8 @@ LLM-translator/ │ ├── llm_client.py # OpenAI-compatible client (async) │ ├── models.py # Pydantic schemas for all API I/O │ ├── prompts.py # Phase 1–4 prompt templates -│ └── sessions.py # In-memory session store (TTL 24h, UUID keys) +│ ├── database.py # SQLite schema, users, sessions, revision persistence +│ └── sessions.py # Persistent session facade + transient progress ├── frontend/ # Vue 3 SPA source │ ├── src/ │ │ ├── main.ts # App entry + dark mode pre-mount class application @@ -46,7 +47,7 @@ LLM-translator/ │ │ │ └── ComparisonView.vue # 3-column Phase 1/3/4 result comparison │ │ ├── stores/ │ │ │ ├── auth.ts # JWT state (check/login/logout) -│ │ │ ├── translation.ts # Pipeline state + localStorage persistence +│ │ │ ├── translation.ts # Pipeline state + server session persistence │ │ │ └── theme.ts # Dark mode toggle + localStorage + system preference detection │ │ ├── api.ts # Axios client with JWT interceptor │ │ └── types.ts # TypeScript interfaces mirroring backend models @@ -68,7 +69,9 @@ LLM-translator/ - **No `passlib`** — use `bcrypt` module directly (`bcrypt.hashpw`, `bcrypt.checkpw`). The project uses Python 3.14 which has compatibility issues with passlib's bcrypt wrapper. - **LLM client caching** — `llm_client.py` caches an `AsyncOpenAI` client by alias. Clients are lazily instantiated on first use. -- **Session store** — process-local in-memory dict (`sessions.py`) scoped by authenticated owner. Key is UUID hex (16 chars). TTL is 24h from last access; there is no persistence beyond process lifetime. +- **Session store** — SQLite-backed, owner-scoped sessions with no automatic TTL deletion. Completed and draft sessions persist across restarts. Streaming progress remains process-local and transient. +- **Session revisions** — the latest previous translation snapshot is saved before an edit or phase rerun overwrites results, allowing one-step restore. +- **Users** — users are stored in SQLite and created by administrators. `users.json` is imported once for compatibility; password changes and deactivation invalidate existing JWTs through `token_version`. - **JSON extraction** — LLM responses may be wrapped in markdown code blocks. `_extract_json()` strips ```` ```json` ... ```` wrappers before `json.loads()`. - **Long-text chunking** — phases 1, 3, and 4 process text sequentially near sentence boundaries. Defaults are 1,500 characters per chunk and a 180-second timeout per LLM call; both are configurable by environment variables. - **Progress feedback** — LLM calls stream response deltas into transient session progress. The frontend polls `/api/sessions/{session_id}/progress` and displays the current chunk and a rolling preview. @@ -77,7 +80,8 @@ LLM-translator/ ### Frontend - **All Vue components use ` + + diff --git a/frontend/src/views/TranslatorView.vue b/frontend/src/views/TranslatorView.vue index 4c0d4db..05b4667 100644 --- a/frontend/src/views/TranslatorView.vue +++ b/frontend/src/views/TranslatorView.vue @@ -11,11 +11,12 @@ const store = useTranslationStore() const auth = useAuthStore() const theme = useThemeStore() const showHistorySidebar = ref(false) +defineEmits<{ (event: 'open-admin'): void }>() onMounted(async () => { await store.loadModels() store.newSession() - store.loadHistory() + await store.loadHistory() }) // Language options @@ -56,13 +57,26 @@ function handleNewTranslation() { } store.newSession() } + +async function openHistorySession(id: string) { + await store.openSession(id) + showHistorySidebar.value = false +} + +async function removeSession(id: string) { + if (confirm('이 번역 기록을 영구 삭제하시겠습니까?')) await store.remove(id) +} + +async function restoreRevision() { + if (confirm('현재 내용을 직전 저장 버전으로 되돌리시겠습니까?')) await store.restorePreviousRevision() +}