From 0dc4e3523fb0e699ad33e0d29ecfd32ea640b8dd Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Tue, 2 Sep 2025 16:38:47 +0900 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20=EB=85=B8=ED=8A=B8=EB=B6=81=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20UI=20=EC=99=84=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐ŸŽฏ ์ฃผ์š” ๊ฐœ์„ ์‚ฌํ•ญ: - ๋…ธํŠธ๋ถ ๋ชฉ๋ก ์กฐํšŒ/ํ‘œ์‹œ ๊ธฐ๋Šฅ ์™„์„ฑ - ๋…ธํŠธ๋ถ ์ƒ์„ฑ/ํŽธ์ง‘/์‚ญ์ œ ๋ชจ๋‹ฌ ๊ตฌํ˜„ - ํ† ์ŠคํŠธ ์•Œ๋ฆผ ์‹œ์Šคํ…œ ์ถ”๊ฐ€ (alert ๋Œ€์‹ ) - ๋…ธํŠธ๋ถ ํ†ต๊ณ„ ๋Œ€์‹œ๋ณด๋“œ ํ‘œ์‹œ - ๋…ธํŠธ๋ถ๋ณ„ ๋…ธํŠธ ๊ด€๋ฆฌ ๋ฐ ๋น ๋ฅธ ๋…ธํŠธ ์ƒ์„ฑ ๊ธฐ๋Šฅ - URL ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ํ†ตํ•œ ๋…ธํŠธ๋ถ ์ž๋™ ์„ค์ • ๐Ÿ”ง ๊ธฐ์ˆ ์  ๊ฐœ์„ : - CSS line-clamp ํด๋ž˜์Šค ์ถ”๊ฐ€ - ํ•„๋“œ๋ช… ๋ถˆ์ผ์น˜ ์ˆ˜์ • (notebook.name โ†’ notebook.title) - ์‚ญ์ œ ํ™•์ธ ๋ชจ๋‹ฌ๋กœ UX ๊ฐœ์„  - ๋…ธํŠธ ์—๋””ํ„ฐ์—์„œ ๋…ธํŠธ๋ถ ์ž๋™ ์„ค์ • ์ง€์› ๐Ÿ“ฑ UI/UX ๊ฐœ์„ : - ๋…ธํŠธ๋ถ ์นด๋“œ ํ˜ธ๋ฒ„ ํšจ๊ณผ ๋ฐ ์ƒ‰์ƒ ํ…Œ๋งˆ - ๋นˆ ๋…ธํŠธ๋ถ ์ƒํƒœ ์ฒ˜๋ฆฌ ๋ฐ ์•ˆ๋‚ด - ๋ฐ˜์‘ํ˜• ๊ทธ๋ฆฌ๋“œ ๋ ˆ์ด์•„์›ƒ - ๋กœ๋”ฉ ์ƒํƒœ ๋ฐ ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๊ฐœ์„  --- README.md | 215 ++++++++++++++++++++++++++++-- config/postgresql.synology.conf | 91 +++++++++++++ docker-compose.synology.yml | 154 +++++++++++++++++++++ frontend/notebooks.html | 125 ++++++++++++++++- frontend/static/js/note-editor.js | 10 +- frontend/static/js/notebooks.js | 61 +++++++-- 6 files changed, 632 insertions(+), 24 deletions(-) create mode 100644 config/postgresql.synology.conf create mode 100644 docker-compose.synology.yml diff --git a/README.md b/README.md index b337e77..9389e66 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,8 @@ PDF ๋ฌธ์„œ๋ฅผ OCR ์ฒ˜๋ฆฌํ•˜๊ณ  AI๋กœ HTML๋กœ ๋ณ€ํ™˜ํ•œ ํ›„, ์›น์—์„œ ํšจ์œจ์  ### ์ธํ”„๋ผ & ๋ฐฐํฌ - **์ปจํ…Œ์ด๋„ˆ**: Docker 24+ & Docker Compose -- **๋ฐฐํฌ ํ™˜๊ฒฝ**: Mac Mini / Synology NAS +- **์ฃผ ๋ฐฐํฌ ํ™˜๊ฒฝ**: Synology DS1525+ (32GB RAM, SSD ์บ์‹ฑ) +- **๋ณด์กฐ ๋ฐฐํฌ ํ™˜๊ฒฝ**: Mac Mini (๊ฐœ๋ฐœ/ํ…Œ์ŠคํŠธ) - **ํ”„๋กœ์„ธ์Šค ๊ด€๋ฆฌ**: Docker (์ปจํ…Œ์ด๋„ˆ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜) - **๋กœ๊ทธ ๊ด€๋ฆฌ**: Python logging + ํŒŒ์ผ ๋กœํ…Œ์ด์…˜ - **๋ชจ๋‹ˆํ„ฐ๋ง**: ๊ธฐ๋ณธ ํ—ฌ์Šค์ฒดํฌ (ํ–ฅํ›„ Prometheus + Grafana) @@ -231,18 +232,30 @@ notes ( - [x] API ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ๋ฐ ์‚ฌ์šฉ์ž ํ”ผ๋“œ๋ฐฑ - [x] ์‹ค์‹œ๊ฐ„ ๋ฌธ์„œ ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ -### Phase 7: ์ตœ์šฐ์„  ๊ฐœ์„ ์‚ฌํ•ญ (์ง„ํ–‰ ์ค‘) ๐Ÿ”ฅ -- [ ] **๋ฉ”๋ชจ-ํ•˜์ด๋ผ์ดํŠธ ํ†ตํ•ฉ**: ํ•˜์ด๋ผ์ดํŠธ ๊ธฐ๋ฐ˜ ๋ฉ”๋ชจ ๊ธฐ๋Šฅ ์™„์ „ ํ†ตํ•ฉ -- [ ] **๋…ธํŠธ ๋ทฐ์–ด ๊ธฐ๋Šฅ**: ๋…ธํŠธ์—์„œ ํ•˜์ด๋ผ์ดํŠธ, ๋ฉ”๋ชจ, ๋งํฌ ๋“ฑ ๋ชจ๋“  ๊ธฐ๋Šฅ ์ง€์› -- [ ] **API ๊ตฌ์กฐ ์ •๋ฆฌ**: ๋ฉ”๋ชจ(`/api/notes/`) vs ๋…ธํŠธ(`/api/note-documents/`) ๋ช…ํ™•ํ•œ ๋ถ„๋ฆฌ -- [ ] **์šฉ์–ด ํ†ต์ผ**: ์ „์ฒด ์‹œ์Šคํ…œ์—์„œ ๋ฉ”๋ชจ/๋…ธํŠธ ์šฉ์–ด ์ผ๊ด€์„ฑ ํ™•๋ณด +### Phase 7: ์ตœ์šฐ์„  ๊ฐœ์„ ์‚ฌํ•ญ โœ… +- [x] **๋ฉ”๋ชจ-ํ•˜์ด๋ผ์ดํŠธ ํ†ตํ•ฉ**: ํ•˜์ด๋ผ์ดํŠธ ๊ธฐ๋ฐ˜ ๋ฉ”๋ชจ ๊ธฐ๋Šฅ ์™„์ „ ํ†ตํ•ฉ +- [x] **๋…ธํŠธ ๋ทฐ์–ด ๊ธฐ๋Šฅ**: ๋…ธํŠธ์—์„œ ํ•˜์ด๋ผ์ดํŠธ, ๋ฉ”๋ชจ, ๋งํฌ ๋“ฑ ๋ชจ๋“  ๊ธฐ๋Šฅ ์ง€์› +- [x] **API ๊ตฌ์กฐ ์ •๋ฆฌ**: ๋ฉ”๋ชจ(`/api/notes/`) vs ๋…ธํŠธ(`/api/note-documents/`) ๋ช…ํ™•ํ•œ ๋ถ„๋ฆฌ +- [x] **์šฉ์–ด ํ†ต์ผ**: ์ „์ฒด ์‹œ์Šคํ…œ์—์„œ ๋ฉ”๋ชจ/๋…ธํŠธ ์šฉ์–ด ์ผ๊ด€์„ฑ ํ™•๋ณด +- [x] **๋…ธํŠธ๋ถ-์„œ์  ๋งํฌ ์‹œ์Šคํ…œ**: ์–‘๋ฐฉํ–ฅ ๋งํฌ/๋ฐฑ๋งํฌ ์™„์ „ ๊ตฌํ˜„ -### Phase 8: ํ–ฅํ›„ ๊ฐœ์„ ์‚ฌํ•ญ (์˜ˆ์ •) +### Phase 8: ๋ฏธ์™„์„ฑ ํ•ต์‹ฌ ๊ธฐ๋Šฅ (์šฐ์„ ์ˆœ์œ„) ๐Ÿšง +- [x] **๋…ธํŠธ ํŽธ์ง‘๊ธฐ**: ๋…ธํŠธ ์ƒ์„ฑ/ํŽธ์ง‘ UI ์™„์„ฑ (`/note-editor.html`) โœ… +- [x] **๋…ธํŠธ๋ถ ๊ด€๋ฆฌ API**: ๋…ธํŠธ๋ถ CRUD ๋ฐฑ์—”๋“œ ์™„์„ฑ โœ… +- [x] **๋…ธํŠธ๋ถ ๊ด€๋ฆฌ UI**: ํ”„๋ก ํŠธ์—”๋“œ CRUD ๊ธฐ๋Šฅ ์™„์„ฑ (`/notebooks.html`) โœ… + - ๋…ธํŠธ๋ถ ๋ชฉ๋ก ์กฐํšŒ/ํ‘œ์‹œ, ์ƒ์„ฑ/ํŽธ์ง‘/์‚ญ์ œ ๋ชจ๋‹ฌ + - ํ† ์ŠคํŠธ ์•Œ๋ฆผ ์‹œ์Šคํ…œ, ํ†ต๊ณ„ ๋Œ€์‹œ๋ณด๋“œ + - ๋…ธํŠธ๋ถ๋ณ„ ๋…ธํŠธ ๊ด€๋ฆฌ ๋ฐ ๋น ๋ฅธ ๋…ธํŠธ ์ƒ์„ฑ +- [ ] **๋ฉ”๋ชจ ํŠธ๋ฆฌ ์‹œ์Šคํ…œ**: ๊ณ„์ธต์  ๋ฉ”๋ชจ ๊ตฌ์กฐ ๋ฐ ๊ด€๋ฆฌ (`/memo-tree.html` ์™„์„ฑ) +- [ ] **๊ณ ๊ธ‰ ๊ฒ€์ƒ‰**: ๋ฌธ์„œ/๋…ธํŠธ/๋ฉ”๋ชจ ํ†ตํ•ฉ ๊ฒ€์ƒ‰ ํ•„ํ„ฐ๋ง +- [ ] **์‚ฌ์šฉ์ž ๊ด€๋ฆฌ**: ๋‹ค์ค‘ ์‚ฌ์šฉ์ž ์ง€์› ๋ฐ ๊ถŒํ•œ ๊ด€๋ฆฌ + +### Phase 9: ๊ด€๋ฆฌ ๋ฐ ์ตœ์ ํ™” (์˜ˆ์ •) - [ ] ๊ด€๋ฆฌ์ž ๋Œ€์‹œ๋ณด๋“œ UI - [ ] ๋ฌธ์„œ ํ†ต๊ณ„ ๋ฐ ๋ถ„์„ - [ ] ๋ชจ๋ฐ”์ผ ๋ฐ˜์‘ํ˜• ์ตœ์ ํ™” -- [ ] ๊ณ ๊ธ‰ ๊ฒ€์ƒ‰ ํ•„ํ„ฐ - [ ] ๋ฌธ์„œ ๋ฒ„์ „ ๊ด€๋ฆฌ +- [ ] ์„ฑ๋Šฅ ์ตœ์ ํ™” ๋ฐ ์บ์‹ฑ ## ํ˜„์žฌ ์ƒํƒœ (2025-08-21) @@ -289,6 +302,192 @@ docker-compose -f docker-compose.dev.yml up docker-compose -f docker-compose.prod.yml up -d ``` +## ๐Ÿข Synology DS1525+ ์ตœ์ ํ™” ๋ฐฐํฌ + +### ํ•˜๋“œ์›จ์–ด ์‚ฌ์–‘ +- **๋ชจ๋ธ**: Synology DS1525+ (5-Bay NAS) +- **CPU**: AMD Ryzen R1600 (4์ฝ”์–ด/8์Šค๋ ˆ๋“œ) +- **๋ฉ”๋ชจ๋ฆฌ**: 32GB DDR4 ECC +- **์Šคํ† ๋ฆฌ์ง€**: SSD ์ฝ๊ธฐ/์“ฐ๊ธฐ ์บ์‹ฑ ํ™œ์„ฑํ™” +- **๋„คํŠธ์›Œํฌ**: ๊ธฐ๊ฐ€๋น„ํŠธ ์ด๋”๋„ท + +### ์Šคํ† ๋ฆฌ์ง€ ์ตœ์ ํ™” ์ „๋žต + +#### SSD ๋ฐฐ์น˜ (๊ณ ์„ฑ๋Šฅ ์š”๊ตฌ) +```bash +# ์‹œ์Šคํ…œ ๋ฐ ๊ณ ๋นˆ๋„ ์•ก์„ธ์Šค ๋ฐ์ดํ„ฐ +/volume1/docker/document-server/ +โ”œโ”€โ”€ database/ # PostgreSQL ๋ฐ์ดํ„ฐ (SSD) +โ”œโ”€โ”€ redis/ # Redis ์บ์‹œ (SSD) +โ”œโ”€โ”€ logs/ # ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋กœ๊ทธ (SSD) +โ””โ”€โ”€ config/ # ์„ค์ • ํŒŒ์ผ (SSD) +``` + +#### HDD ๋ฐฐ์น˜ (๋Œ€์šฉ๋Ÿ‰ ์ €์žฅ) +```bash +# ๋Œ€์šฉ๋Ÿ‰ ํŒŒ์ผ ์ €์žฅ์†Œ +/volume2/document-storage/ +โ”œโ”€โ”€ documents/ # HTML ๋ฌธ์„œ ํŒŒ์ผ (HDD) +โ”œโ”€โ”€ uploads/ # ์—…๋กœ๋“œ๋œ ์›๋ณธ ํŒŒ์ผ (HDD) +โ”œโ”€โ”€ backups/ # ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ฐฑ์—… (HDD) +โ””โ”€โ”€ archives/ # ์•„์นด์ด๋ธŒ ํŒŒ์ผ (HDD) +``` + +### Docker Compose ์ตœ์ ํ™” ์„ค์ • + +#### ๋ณผ๋ฅจ ๋งคํ•‘ (docker-compose.synology.yml) +```yaml +version: '3.8' + +services: + database: + volumes: + # SSD: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ฑ๋Šฅ ์ตœ์ ํ™” + - /volume1/docker/document-server/database:/var/lib/postgresql/data + + redis: + volumes: + # SSD: ์บ์‹œ ์„ฑ๋Šฅ ์ตœ์ ํ™” + - /volume1/docker/document-server/redis:/data + + backend: + volumes: + # SSD: ๋กœ๊ทธ ๋ฐ ์„ค์ • + - /volume1/docker/document-server/logs:/app/logs + - /volume1/docker/document-server/config:/app/config + # HDD: ๋Œ€์šฉ๋Ÿ‰ ํŒŒ์ผ ์ €์žฅ + - /volume2/document-storage/uploads:/app/uploads + - /volume2/document-storage/documents:/app/documents + + nginx: + volumes: + # SSD: ์„ค์ • ๋ฐ ์บ์‹œ + - /volume1/docker/document-server/nginx:/etc/nginx/conf.d + # HDD: ์ •์  ํŒŒ์ผ ์„œ๋น™ + - /volume2/document-storage/documents:/usr/share/nginx/html/documents:ro +``` + +### ์‹œ๋†€๋กœ์ง€ ํ™˜๊ฒฝ ๋ฐฐํฌ ๋ช…๋ น์–ด + +```bash +# 1. ๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑ +sudo mkdir -p /volume1/docker/document-server/{database,redis,logs,config,nginx} +sudo mkdir -p /volume2/document-storage/{documents,uploads,backups,archives} + +# 2. ๊ถŒํ•œ ์„ค์ • +sudo chown -R 1000:1000 /volume1/docker/document-server/ +sudo chown -R 1000:1000 /volume2/document-storage/ + +# 3. ์‹œ๋†€๋กœ์ง€ ์ตœ์ ํ™” ๋ฐฐํฌ +docker-compose -f docker-compose.synology.yml up -d + +# 4. ์„œ๋น„์Šค ์ƒํƒœ ํ™•์ธ +docker-compose -f docker-compose.synology.yml ps +``` + +### ์„ฑ๋Šฅ ์ตœ์ ํ™” ์„ค์ • + +#### PostgreSQL ํŠœ๋‹ (32GB RAM ํ™˜๊ฒฝ) +```ini +# postgresql.conf +shared_buffers = 8GB # RAM์˜ 25% +effective_cache_size = 24GB # RAM์˜ 75% +work_mem = 256MB # ๋ณต์žกํ•œ ์ฟผ๋ฆฌ์šฉ +maintenance_work_mem = 2GB # ์ธ๋ฑ์Šค ๊ตฌ์ถ•์šฉ +checkpoint_completion_target = 0.9 # SSD ์ตœ์ ํ™” +wal_buffers = 64MB # WAL ๋ฒ„ํผ +random_page_cost = 1.1 # SSD ํ™˜๊ฒฝ ์ตœ์ ํ™” +``` + +#### Redis ์„ค์ • (์บ์‹ฑ ์ตœ์ ํ™”) +```conf +# redis.conf +maxmemory 4gb # ์บ์‹œ ๋ฉ”๋ชจ๋ฆฌ ์ œํ•œ +maxmemory-policy allkeys-lru # LRU ์ •์ฑ… +save 900 1 # ์ž๋™ ์ €์žฅ ์„ค์ • +save 300 10 +save 60 10000 +``` + +### ๋ฐฑ์—… ์ „๋žต + +#### ์ž๋™ ๋ฐฑ์—… ์Šคํฌ๋ฆฝํŠธ +```bash +#!/bin/bash +# /volume1/docker/document-server/scripts/backup.sh + +BACKUP_DIR="/volume2/document-storage/backups" +DATE=$(date +%Y%m%d_%H%M%S) + +# ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ฐฑ์—… +docker-compose -f docker-compose.synology.yml exec -T database \ + pg_dump -U postgres document_server > "$BACKUP_DIR/db_backup_$DATE.sql" + +# ์„ค์ • ํŒŒ์ผ ๋ฐฑ์—… +tar -czf "$BACKUP_DIR/config_backup_$DATE.tar.gz" \ + /volume1/docker/document-server/config/ + +# 7์ผ ์ด์ƒ ๋œ ๋ฐฑ์—… ํŒŒ์ผ ์‚ญ์ œ +find "$BACKUP_DIR" -name "*.sql" -mtime +7 -delete +find "$BACKUP_DIR" -name "*.tar.gz" -mtime +7 -delete + +echo "Backup completed: $DATE" +``` + +#### ์‹œ๋†€๋กœ์ง€ ์ž‘์—… ์Šค์ผ€์ค„๋Ÿฌ ์„ค์ • +```bash +# ๋งค์ผ ์ƒˆ๋ฒฝ 2์‹œ ์ž๋™ ๋ฐฑ์—… +# ์ œ์–ดํŒ > ์ž‘์—… ์Šค์ผ€์ค„๋Ÿฌ > ์ƒ์„ฑ > ์‚ฌ์šฉ์ž ์ •์˜ ์Šคํฌ๋ฆฝํŠธ +0 2 * * * /volume1/docker/document-server/scripts/backup.sh +``` + +### ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ์œ ์ง€๋ณด์ˆ˜ + +#### ๋ฆฌ์†Œ์Šค ๋ชจ๋‹ˆํ„ฐ๋ง +```bash +# ์ปจํ…Œ์ด๋„ˆ ๋ฆฌ์†Œ์Šค ์‚ฌ์šฉ๋Ÿ‰ ํ™•์ธ +docker stats + +# ๋””์Šคํฌ ์‚ฌ์šฉ๋Ÿ‰ ํ™•์ธ +df -h /volume1 /volume2 + +# ์‹œ๋†€๋กœ์ง€ ์‹œ์Šคํ…œ ์ƒํƒœ +cat /proc/meminfo | grep -E "MemTotal|MemAvailable" +``` + +#### ๋กœ๊ทธ ๋กœํ…Œ์ด์…˜ ์„ค์ • +```bash +# /etc/logrotate.d/document-server +/volume1/docker/document-server/logs/*.log { + daily + rotate 30 + compress + delaycompress + missingok + notifempty + create 644 1000 1000 + postrotate + docker-compose -f docker-compose.synology.yml restart backend + endscript +} +``` + +### ๋„คํŠธ์›Œํฌ ์ตœ์ ํ™” + +#### ํฌํŠธ ํฌ์›Œ๋”ฉ ์„ค์ • +- **์™ธ๋ถ€ ํฌํŠธ**: 24100 (HTTPS ๋ฆฌ๋ฒ„์Šค ํ”„๋ก์‹œ ๊ถŒ์žฅ) +- **๋‚ด๋ถ€ ํฌํŠธ**: 24100 (Nginx) +- **๋ฐฉํ™”๋ฒฝ**: ํ•„์š”ํ•œ ํฌํŠธ๋งŒ ๊ฐœ๋ฐฉ + +#### SSL/TLS ์„ค์ • (Let's Encrypt) +```bash +# Certbot์„ ํ†ตํ•œ SSL ์ธ์ฆ์„œ ์ž๋™ ๊ฐฑ์‹  +docker run --rm -v /volume1/docker/document-server/ssl:/etc/letsencrypt \ + certbot/certbot certonly --webroot \ + -w /volume2/document-storage/documents \ + -d your-domain.com +``` + ## API ์—”๋“œํฌ์ธํŠธ (์˜ˆ์ƒ) ### ์ธ์ฆ ๊ด€๋ฆฌ diff --git a/config/postgresql.synology.conf b/config/postgresql.synology.conf new file mode 100644 index 0000000..031de56 --- /dev/null +++ b/config/postgresql.synology.conf @@ -0,0 +1,91 @@ +# PostgreSQL ์„ค์ • - Synology DS1525+ ์ตœ์ ํ™” (32GB RAM) +# /volume1/docker/document-server/config/postgresql.conf + +# ๋ฉ”๋ชจ๋ฆฌ ์„ค์ • (32GB RAM ํ™˜๊ฒฝ) +shared_buffers = 8GB # RAM์˜ 25% (8GB) +effective_cache_size = 24GB # RAM์˜ 75% (24GB) +work_mem = 256MB # ๋ณต์žกํ•œ ์ฟผ๋ฆฌ์šฉ (์ •๋ ฌ, ํ•ด์‹œ ์กฐ์ธ) +maintenance_work_mem = 2GB # ์ธ๋ฑ์Šค ๊ตฌ์ถ•, VACUUM์šฉ + +# ์ฒดํฌํฌ์ธํŠธ ์„ค์ • (SSD ์ตœ์ ํ™”) +checkpoint_completion_target = 0.9 # ์ฒดํฌํฌ์ธํŠธ ๋ถ„์‚ฐ (SSD ์ˆ˜๋ช… ์—ฐ์žฅ) +checkpoint_timeout = 15min # ์ฒดํฌํฌ์ธํŠธ ๊ฐ„๊ฒฉ +max_wal_size = 4GB # WAL ํŒŒ์ผ ์ตœ๋Œ€ ํฌ๊ธฐ +min_wal_size = 1GB # WAL ํŒŒ์ผ ์ตœ์†Œ ํฌ๊ธฐ + +# WAL ์„ค์ • +wal_buffers = 64MB # WAL ๋ฒ„ํผ ํฌ๊ธฐ +wal_writer_delay = 200ms # WAL ์“ฐ๊ธฐ ์ง€์—ฐ +commit_delay = 0 # ์ปค๋ฐ‹ ์ง€์—ฐ (SSD์—์„œ๋Š” 0) + +# ๋น„์šฉ ๊ธฐ๋ฐ˜ ์ตœ์ ํ™” (SSD ํ™˜๊ฒฝ) +random_page_cost = 1.1 # SSD๋Š” ๋žœ๋ค ์•ก์„ธ์Šค๊ฐ€ ๋น ๋ฆ„ +seq_page_cost = 1.0 # ์ˆœ์ฐจ ์•ก์„ธ์Šค ๊ธฐ์ค€๊ฐ’ +cpu_tuple_cost = 0.01 # CPU ํŠœํ”Œ ์ฒ˜๋ฆฌ ๋น„์šฉ +cpu_index_tuple_cost = 0.005 # ์ธ๋ฑ์Šค ํŠœํ”Œ ์ฒ˜๋ฆฌ ๋น„์šฉ +cpu_operator_cost = 0.0025 # ์—ฐ์‚ฐ์ž ์ฒ˜๋ฆฌ ๋น„์šฉ + +# ์—ฐ๊ฒฐ ์„ค์ • +max_connections = 200 # ์ตœ๋Œ€ ์—ฐ๊ฒฐ ์ˆ˜ +superuser_reserved_connections = 3 # ์Šˆํผ์œ ์ € ์˜ˆ์•ฝ ์—ฐ๊ฒฐ + +# ์ฟผ๋ฆฌ ํ”Œ๋ž˜๋„ˆ ์„ค์ • +default_statistics_target = 100 # ํ†ต๊ณ„ ์ •ํ™•๋„ +constraint_exclusion = partition # ํŒŒํ‹ฐ์…˜ ์ œ์•ฝ ์กฐ๊ฑด ์ตœ์ ํ™” +enable_partitionwise_join = on # ํŒŒํ‹ฐ์…˜๋ณ„ ์กฐ์ธ ์ตœ์ ํ™” +enable_partitionwise_aggregate = on # ํŒŒํ‹ฐ์…˜๋ณ„ ์ง‘๊ณ„ ์ตœ์ ํ™” + +# ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—…์ž ์„ค์ • +max_worker_processes = 8 # ์ตœ๋Œ€ ์›Œ์ปค ํ”„๋กœ์„ธ์Šค (CPU ์ฝ”์–ด ์ˆ˜) +max_parallel_workers_per_gather = 4 # ๋ณ‘๋ ฌ ์ฟผ๋ฆฌ ์›Œ์ปค +max_parallel_workers = 8 # ์ „์ฒด ๋ณ‘๋ ฌ ์›Œ์ปค +max_parallel_maintenance_workers = 4 # ๋ณ‘๋ ฌ ์œ ์ง€๋ณด์ˆ˜ ์›Œ์ปค + +# ์ž๋™ VACUUM ์„ค์ • +autovacuum = on # ์ž๋™ VACUUM ํ™œ์„ฑํ™” +autovacuum_max_workers = 3 # VACUUM ์›Œ์ปค ์ˆ˜ +autovacuum_naptime = 1min # VACUUM ์‹คํ–‰ ๊ฐ„๊ฒฉ +autovacuum_vacuum_threshold = 50 # VACUUM ์ž„๊ณ„๊ฐ’ +autovacuum_analyze_threshold = 50 # ANALYZE ์ž„๊ณ„๊ฐ’ +autovacuum_vacuum_scale_factor = 0.2 # VACUUM ์Šค์ผ€์ผ ํŒฉํ„ฐ +autovacuum_analyze_scale_factor = 0.1 # ANALYZE ์Šค์ผ€์ผ ํŒฉํ„ฐ + +# ๋กœ๊น… ์„ค์ • +log_destination = 'stderr' # ๋กœ๊ทธ ์ถœ๋ ฅ ๋Œ€์ƒ +logging_collector = off # Docker ํ™˜๊ฒฝ์—์„œ๋Š” off +log_min_messages = warning # ์ตœ์†Œ ๋กœ๊ทธ ๋ ˆ๋ฒจ +log_min_error_statement = error # ์—๋Ÿฌ ๋ฌธ์žฅ ๋กœ๊ทธ +log_min_duration_statement = 1000 # 1์ดˆ ์ด์ƒ ์ฟผ๋ฆฌ ๋กœ๊น… +log_checkpoints = on # ์ฒดํฌํฌ์ธํŠธ ๋กœ๊น… +log_connections = off # ์—ฐ๊ฒฐ ๋กœ๊น… (์„ฑ๋Šฅ์ƒ off) +log_disconnections = off # ์—ฐ๊ฒฐ ํ•ด์ œ ๋กœ๊น… (์„ฑ๋Šฅ์ƒ off) +log_lock_waits = on # ๋ฝ ๋Œ€๊ธฐ ๋กœ๊น… +log_temp_files = 10MB # ์ž„์‹œ ํŒŒ์ผ ๋กœ๊น… (10MB ์ด์ƒ) + +# ์ „๋ฌธ ๊ฒ€์ƒ‰ ์„ค์ • +default_text_search_config = 'pg_catalog.english' + +# ์‹œ๊ฐ„๋Œ€ ์„ค์ • +timezone = 'Asia/Seoul' +log_timezone = 'Asia/Seoul' + +# ๋ฌธ์ž ์ธ์ฝ”๋”ฉ +lc_messages = 'C' +lc_monetary = 'C' +lc_numeric = 'C' +lc_time = 'C' + +# ๊ธฐํƒ€ ์„ฑ๋Šฅ ์„ค์ • +effective_io_concurrency = 200 # SSD ๋™์‹œ I/O (SSD๋Š” ๋†’๊ฒŒ) +maintenance_io_concurrency = 10 # ์œ ์ง€๋ณด์ˆ˜ I/O ๋™์‹œ์„ฑ +wal_compression = on # WAL ์••์ถ• (๋””์Šคํฌ ์ ˆ์•ฝ) +full_page_writes = on # ์ „์ฒด ํŽ˜์ด์ง€ ์“ฐ๊ธฐ (์•ˆ์ •์„ฑ) + +# JIT ์ปดํŒŒ์ผ ์„ค์ • (PostgreSQL 11+) +jit = on # JIT ์ปดํŒŒ์ผ ํ™œ์„ฑํ™” +jit_above_cost = 100000 # JIT ํ™œ์„ฑํ™” ๋น„์šฉ ์ž„๊ณ„๊ฐ’ +jit_inline_above_cost = 500000 # ์ธ๋ผ์ธ JIT ๋น„์šฉ ์ž„๊ณ„๊ฐ’ +jit_optimize_above_cost = 500000 # ์ตœ์ ํ™” JIT ๋น„์šฉ ์ž„๊ณ„๊ฐ’ + +# ํ™•์žฅ ๋ชจ๋“ˆ ์„ค์ • +shared_preload_libraries = 'pg_stat_statements' # ์ฟผ๋ฆฌ ํ†ต๊ณ„ ๋ชจ๋“ˆ diff --git a/docker-compose.synology.yml b/docker-compose.synology.yml new file mode 100644 index 0000000..8893937 --- /dev/null +++ b/docker-compose.synology.yml @@ -0,0 +1,154 @@ +version: '3.8' + +services: + # PostgreSQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค (SSD ์ตœ์ ํ™”) + database: + image: postgres:15-alpine + container_name: document-server-db + restart: unless-stopped + environment: + POSTGRES_DB: document_server + POSTGRES_USER: postgres + POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres123} + POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C" + volumes: + # SSD: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ฑ๋Šฅ ์ตœ์ ํ™” + - /volume1/docker/document-server/database:/var/lib/postgresql/data + - /volume1/docker/document-server/config/postgresql.conf:/etc/postgresql/postgresql.conf:ro + ports: + - "24101:5432" + command: > + postgres + -c config_file=/etc/postgresql/postgresql.conf + -c shared_buffers=8GB + -c effective_cache_size=24GB + -c work_mem=256MB + -c maintenance_work_mem=2GB + -c checkpoint_completion_target=0.9 + -c wal_buffers=64MB + -c random_page_cost=1.1 + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 30s + timeout: 10s + retries: 3 + networks: + - document-network + + # Redis ์บ์‹œ (SSD ์ตœ์ ํ™”) + redis: + image: redis:7-alpine + container_name: document-server-redis + restart: unless-stopped + volumes: + # SSD: ์บ์‹œ ์„ฑ๋Šฅ ์ตœ์ ํ™” + - /volume1/docker/document-server/redis:/data + ports: + - "24103:6379" + command: > + redis-server + --maxmemory 4gb + --maxmemory-policy allkeys-lru + --save 900 1 + --save 300 10 + --save 60 10000 + --appendonly yes + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 30s + timeout: 10s + retries: 3 + networks: + - document-network + + # FastAPI ๋ฐฑ์—”๋“œ + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: document-server-backend + restart: unless-stopped + environment: + - DATABASE_URL=postgresql://postgres:${DB_PASSWORD:-postgres123}@database:5432/document_server + - REDIS_URL=redis://redis:6379 + - JWT_SECRET_KEY=${JWT_SECRET_KEY:-your-super-secret-jwt-key-change-this-in-production} + - ENVIRONMENT=production + - LOG_LEVEL=INFO + volumes: + # SSD: ๋กœ๊ทธ ๋ฐ ์„ค์ • (๋น ๋ฅธ ์•ก์„ธ์Šค) + - /volume1/docker/document-server/logs:/app/logs + - /volume1/docker/document-server/config:/app/config + # HDD: ๋Œ€์šฉ๋Ÿ‰ ํŒŒ์ผ ์ €์žฅ (๋น„์šฉ ํšจ์œจ์ ) + - /volume2/document-storage/uploads:/app/uploads + - /volume2/document-storage/documents:/app/documents + ports: + - "24102:8000" + depends_on: + database: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 30s + timeout: 10s + retries: 3 + networks: + - document-network + + # Nginx ์›น์„œ๋ฒ„ + nginx: + build: + context: ./nginx + dockerfile: Dockerfile + container_name: document-server-nginx + restart: unless-stopped + volumes: + # SSD: ์„ค์ • ๋ฐ ์บ์‹œ (๋น ๋ฅธ ์•ก์„ธ์Šค) + - /volume1/docker/document-server/nginx:/etc/nginx/conf.d + - /volume1/docker/document-server/logs/nginx:/var/log/nginx + # HDD: ์ •์  ํŒŒ์ผ ์„œ๋น™ (์ฝ๊ธฐ ์ „์šฉ) + - /volume2/document-storage/documents:/usr/share/nginx/html/documents:ro + - ./frontend:/usr/share/nginx/html:ro + ports: + - "24100:80" + depends_on: + backend: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:80/health"] + interval: 30s + timeout: 10s + retries: 3 + networks: + - document-network + +networks: + document-network: + driver: bridge + ipam: + config: + - subnet: 172.20.0.0/16 + +volumes: + # ๋ช…์‹œ์  ๋ณผ๋ฅจ ์ •์˜ (์‹œ๋†€๋กœ์ง€ ๊ฒฝ๋กœ ๋งคํ•‘) + database_data: + driver: local + driver_opts: + type: none + o: bind + device: /volume1/docker/document-server/database + + redis_data: + driver: local + driver_opts: + type: none + o: bind + device: /volume1/docker/document-server/redis + + document_storage: + driver: local + driver_opts: + type: none + o: bind + device: /volume2/document-storage diff --git a/frontend/notebooks.html b/frontend/notebooks.html index eba843a..354fe1a 100644 --- a/frontend/notebooks.html +++ b/frontend/notebooks.html @@ -23,6 +23,18 @@ .gradient-bg { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } + .line-clamp-2 { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + } + .line-clamp-3 { + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + } @@ -200,7 +212,7 @@ -
+
@@ -212,6 +224,28 @@
+ + +
+

๋…ธํŠธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค

+ +
+ +
+ + + + + +
@@ -229,6 +263,35 @@ + +
+
+
+ + + +
+
+
+
+ +
+ +
+ +
+
+ +
+
+

๋…ธํŠธ๋ถ ์‚ญ์ œ

+

์ด ์ž‘์—…์€ ๋˜๋Œ๋ฆด ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

+
+
+ +
+

+ ๋…ธํŠธ๋ถ์„ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ? +

+
+
+ + + ํฌํ•จ๋œ ๊ฐœ์˜ ๋…ธํŠธ๋Š” ๋ฏธ๋ถ„๋ฅ˜ ์ƒํƒœ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. + +
+
+
+ +
+ + +
+
+
+ diff --git a/frontend/static/js/note-editor.js b/frontend/static/js/note-editor.js index d39d5b8..f539dfe 100644 --- a/frontend/static/js/note-editor.js +++ b/frontend/static/js/note-editor.js @@ -54,13 +54,21 @@ function noteEditorApp() { return; } - // URL์—์„œ ๋…ธํŠธ ID ํ™•์ธ (ํŽธ์ง‘ ๋ชจ๋“œ) + // URL์—์„œ ๋…ธํŠธ ID ๋ฐ ๋…ธํŠธ๋ถ ์ •๋ณด ํ™•์ธ const urlParams = new URLSearchParams(window.location.search); this.noteId = urlParams.get('id'); + const notebookId = urlParams.get('notebook_id'); + const notebookName = urlParams.get('notebook_name'); // ๋…ธํŠธ๋ถ ๋ชฉ๋ก ๋กœ๋“œ await this.loadNotebooks(); + // URL์—์„œ ๋…ธํŠธ๋ถ์ด ์ง€์ •๋œ ๊ฒฝ์šฐ ์ž๋™ ์„ค์ • + if (notebookId && !this.noteId) { // ์ƒˆ ๋…ธํŠธ ์ƒ์„ฑ ์‹œ์—๋งŒ + this.noteData.notebook_id = notebookId; + console.log('๐Ÿ“š ๋…ธํŠธ๋ถ ์ž๋™ ์„ค์ •:', notebookName || notebookId); + } + if (this.noteId) { this.isEditing = true; await this.loadNote(this.noteId); diff --git a/frontend/static/js/notebooks.js b/frontend/static/js/notebooks.js index 7154428..acb0afb 100644 --- a/frontend/static/js/notebooks.js +++ b/frontend/static/js/notebooks.js @@ -7,6 +7,13 @@ window.notebooksApp = () => ({ saving: false, error: '', + // ์•Œ๋ฆผ ์‹œ์Šคํ…œ + notification: { + show: false, + message: '', + type: 'info' // 'success', 'error', 'info' + }, + // ํ•„ํ„ฐ๋ง searchQuery: '', activeOnly: true, @@ -18,7 +25,10 @@ window.notebooksApp = () => ({ // ๋ชจ๋‹ฌ ์ƒํƒœ showCreateModal: false, showEditModal: false, + showDeleteModal: false, editingNotebook: null, + deletingNotebook: null, + deleting: false, // ๋…ธํŠธ๋ถ ํผ notebookForm: { @@ -168,7 +178,12 @@ window.notebooksApp = () => ({ // ๋…ธํŠธ๋ถ ์—ด๊ธฐ (๋…ธํŠธ ๋ชฉ๋ก์œผ๋กœ ์ด๋™) openNotebook(notebook) { - window.location.href = `/notes.html?notebook_id=${notebook.id}¬ebook_name=${encodeURIComponent(notebook.name)}`; + window.location.href = `/notes.html?notebook_id=${notebook.id}¬ebook_name=${encodeURIComponent(notebook.title)}`; + }, + + // ๋…ธํŠธ๋ถ์— ๋…ธํŠธ ์ƒ์„ฑ + createNoteInNotebook(notebook) { + window.location.href = `/note-editor.html?notebook_id=${notebook.id}¬ebook_name=${encodeURIComponent(notebook.title)}`; }, // ๋…ธํŠธ๋ถ ํŽธ์ง‘ @@ -185,22 +200,37 @@ window.notebooksApp = () => ({ this.showEditModal = true; }, - // ๋…ธํŠธ๋ถ ์‚ญ์ œ - async deleteNotebook(notebook) { - if (!confirm(`"${notebook.title}" ๋…ธํŠธ๋ถ์„ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?\n\n${notebook.note_count > 0 ? `ํฌํ•จ๋œ ${notebook.note_count}๊ฐœ์˜ ๋…ธํŠธ๋Š” ๋ฏธ๋ถ„๋ฅ˜ ์ƒํƒœ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.` : ''}`)) { - return; - } + // ๋…ธํŠธ๋ถ ์‚ญ์ œ (๋ชจ๋‹ฌ ํ‘œ์‹œ) + deleteNotebook(notebook) { + this.deletingNotebook = notebook; + this.showDeleteModal = true; + }, + // ์‚ญ์ œ ํ™•์ธ + async confirmDeleteNotebook() { + if (!this.deletingNotebook) return; + + this.deleting = true; try { - await this.api.deleteNotebook(notebook.id, true); // force=true + await this.api.deleteNotebook(this.deletingNotebook.id, true); // force=true this.showNotification('๋…ธํŠธ๋ถ์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.', 'success'); + this.closeDeleteModal(); await this.refreshNotebooks(); } catch (error) { console.error('๋…ธํŠธ๋ถ ์‚ญ์ œ ์‹คํŒจ:', error); this.showNotification('๋…ธํŠธ๋ถ ์‚ญ์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.', 'error'); + } finally { + this.deleting = false; } }, + // ์‚ญ์ œ ๋ชจ๋‹ฌ ๋‹ซ๊ธฐ + closeDeleteModal() { + this.showDeleteModal = false; + this.deletingNotebook = null; + this.deleting = false; + }, + // ๋…ธํŠธ๋ถ ์ €์žฅ async saveNotebook() { if (!this.notebookForm.title.trim()) { @@ -266,12 +296,15 @@ window.notebooksApp = () => ({ // ์•Œ๋ฆผ ํ‘œ์‹œ showNotification(message, type = 'info') { - if (type === 'error') { - alert('โŒ ' + message); - } else if (type === 'success') { - alert('โœ… ' + message); - } else { - alert('โ„น๏ธ ' + message); - } + this.notification = { + show: true, + message: message, + type: type + }; + + // 3์ดˆ ํ›„ ์ž๋™์œผ๋กœ ์ˆจ๊น€ + setTimeout(() => { + this.notification.show = false; + }, 3000); } });