# π Todo Project μ’ ν© κ°λ° κ°μ΄λ > **"Simple Todo, Smart Integration"** > κ°κ²°ν ν μΌ κ΄λ¦¬ + μλλ‘μ§ μνκ³ μ°λμΌλ‘ μλ²½ν κ°μΈ μμ°μ± λꡬ --- ## π― νλ‘μ νΈ κ°μ ### λͺ©ν μ¬μ§κ³Ό λ©λͺ¨λ₯Ό κΈ°λ°μΌλ‘ ν κ°λ¨ν μΌμ κ΄λ¦¬ μμ€ν κ΅¬μΆ ### ν΅μ¬ κΈ°λ₯ - **μ λ ₯**: μ¬μ§(μ ν) + ν μ€νΈ λ©λͺ¨ - **λΆλ₯**: GTDν / μΊλ¦°λν / 체ν¬λ¦¬μ€νΈν - **νλ«νΌ**: iOS + Apple Watch + μΉ ### μμ€ν νκ²½ - **λ°°ν¬ μλ²**: Synology DS1525+ - **κ°λ° νκ²½**: Mac mini M4 Pro (macOS) - **λ€νΈμν¬**: LG U+ 2.5G + Synology Router ### ν΅μ¬ κ°μΉ - β‘ **μ¦μ μ κ·Ό**: λ°λ‘ μΌμ λ°λ‘ μΈ μ μλ κ°νΈν¨ - π― **κ°κ²°ν¨**: 볡μ‘νμ§ μμ μ§κ΄μ μΈ μΈν°νμ΄μ€ - π **μ€λ§νΈ μ°λ**: μλλ‘μ§ μΊλ¦°λ/λ©μΌκ³Ό μλ λκΈ°ν - π± **μ΄λμλ**: ν°, μ»΄ν¨ν°μμ λμΌν κ²½ν --- ## π μμ€ν μν€ν μ² ### λ³Όλ₯¨ λ§€ν ꡬ쑰 ``` Synology NAS: /volume3/docker/todo-app/ # μμ€ν /컨ν μ΄λ βββ docker-compose.yml βββ app/ # μ ν리μΌμ΄μ μ½λ βββ config/ # μ€μ νμΌ /volume1/todo-data/ # λ°μ΄ν° μ μ₯ βββ database/ # DB νμΌ βββ uploads/images/ # μ λ‘λ μ΄λ―Έμ§ βββ backups/ # λ°±μ ``` ### κΈ°μ μ€ν ``` Backend: FastAPI (Python) / PostgreSQL Frontend: Vanilla JS + Alpine.js + Tailwind CSS Database: PostgreSQL (ν¬νΈ: 5434) Deployment: Docker + Docker Compose Integration: CalDAV, SMTP, DSM API ``` ### μ 체 ꡬ쑰 ``` βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ β Frontend β β Backend β β Synology β β (4000) βββββΊβ (9000) βββββΊβ Services β β β β β β β β β’ PWA Support β β β’ FastAPI β β β’ Calendar β β β’ Offline Mode β β β’ SQLAlchemy β β β’ MailPlus β β β’ Auto Login β β β’ PostgreSQL β β β’ DSM API β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ ``` ### API ꡬ쑰 ``` POST /api/items # μμ± GET /api/items?type={type} # μ‘°ν PUT /api/items/{id} # μμ DELETE /api/items/{id} # μμ POST /api/upload/image # μ΄λ―Έμ§ ``` --- ## π νλ‘μ νΈ κ΅¬μ‘° ``` Todo-Project/ βββ README.md βββ DESIGN.md # μ€κ³ λ¬Έμ βββ SYNOLOGY_INTEGRATION.md # μλλ‘μ§ μ°λ κ°μ΄λ βββ docker-compose.yml βββ .env.example βββ backend/ β βββ Dockerfile β βββ pyproject.toml β βββ src/ β β βββ main.py β β βββ core/ β β β βββ config.py β β β βββ database.py β β β βββ security.py β β βββ models/ β β β βββ user.py β β β βββ todo.py β β βββ schemas/ β β β βββ auth.py β β β βββ todo.py β β βββ api/ β β β βββ dependencies.py β β β βββ routes/ β β β βββ auth.py β β β βββ users.py β β β βββ todos.py β β βββ integrations/ β β βββ synology/ β β β βββ dsm_auth.py β β β βββ calendar_sync.py β β β βββ mail_service.py β β βββ device_auth.py β βββ migrations/ βββ frontend/ β βββ index.html β βββ login.html β βββ static/ β β βββ css/ β β β βββ main.css β β βββ js/ β β β βββ api.js β β β βββ auth.js β β β βββ todos.js β β β βββ synology-sync.js β β βββ icons/ β βββ components/ β βββ manifest.json # PWA μ€μ βββ docs/ β βββ API.md # API λ¬Έμ β βββ DEPLOYMENT.md # λ°°ν¬ κ°μ΄λ β βββ SECURITY.md # 보μ κ°μ΄λ βββ database/ βββ init/ βββ 01_init.sql ``` --- ## π λ°μ΄ν°λ² μ΄μ€ μ€κ³ ### ERD (Entity Relationship Diagram) ``` βββββββββββββββ βββββββββββββββ βββββββββββββββ β Users β β TodoItems β βTodoComments β βββββββββββββββ€ βββββββββββββββ€ βββββββββββββββ€ β id (PK) βββββββββββ user_id(FK) βββββββββββtodo_item_id β β email β β content β β content β β password β β status β β created_at β β full_name β β start_date β βββββββββββββββ β is_active β β estimated β β created_at β β completed β βββββββββββββββ β parent_id β βββ βββββββββββββββ β β β ββββββββββββ (Self Reference) ``` ### μν κ΄λ¦¬ ```python TODO_STATES = { "draft": "κ²ν νμ - μμ§ μΌμ λ―Έμ€μ ", "scheduled": "μμ λ¨ - μΌμ μ€μ μλ£", "active": "μ§νμ€ - νμ¬ μμ μ€", "completed": "μλ£λ¨ - μμ μλ£", "delayed": "μ§μ°λ¨ - μλ‘μ΄ λ μ§λ‘ μ°κΈ°" } ``` ### λ°μ΄ν° νλ‘μ° ``` 1. ν μΌ μμ± β 2. μΌμ μ€μ β 3. μΊλ¦°λ λκΈ°ν β 4. λ©μΌ μλ¦Ό β β β β Draft Scheduled Calendar Email Event Sent ``` --- ## π 보μ μ€κ³ ### κ°μΈμ© μ΅μ ν 보μ λͺ¨λΈ #### κΈ°κΈ° λ±λ‘ λ°©μ ```python class DeviceAuth: """κ°μΈ κΈ°κΈ° μΈμ¦ μμ€ν """ def register_device(self, user_id, device_info): """μ λ’°ν μ μλ κΈ°κΈ° λ±λ‘""" # κΈ°κΈ° κ³ μ ID μμ± (λΈλΌμ°μ fingerprint + μ¬μ©μ μ λ ₯) device_id = self.generate_device_id(device_info) # μ₯κΈ°κ° μ ν¨ν ν ν° μμ± (30μΌ) device_token = self.create_device_token(user_id, device_id) return device_token def quick_login(self, device_token): """κΈ°κΈ° ν ν°μΌλ‘ λΉ λ₯Έ λ‘κ·ΈμΈ""" if self.is_valid_device_token(device_token): return self.create_session_token() return None ``` #### 보μ λ 벨 - **Minimal**: κΈ°κΈ° λ±λ‘ ν λΉλ°λ²νΈ λΆνμ (κ°μΈμ© κΆμ₯) - **Balanced**: μ£ΌκΈ°μ λΉλ°λ²νΈ νμΈ - **Secure**: λ§€λ² μΈμ¦ + μ체 μΈμ¦ --- ## π μλλ‘μ§ μ°λ μ€κ³ ### μ°λ κ°μ Todo-Projectλ μλλ‘μ§ μνκ³μ **μλ°©ν₯ μ°λ**λμ΄ **κ°κ²°ν ν μΌ κ΄λ¦¬**μ **μ 체 μΌμ μ‘°λ§**μ λμμ μ 곡ν©λλ€. #### ν΅μ¬ μ² ν - **Todo-Project**: λΉ λ₯Έ ν μΌ μΆκ° λ° κ΄λ¦¬ (κ°κ²°ν¨) - **μλλ‘μ§ μΊλ¦°λ**: μ 체 μΌμ μ‘°λ§ λ° ν΅ν© κ΄λ¦¬ (μμ μ±) - **MailPlus**: μΈλΆ μμ²μ Todoλ‘ μλ λ³ν (μλν) ### π κ°μνλ μ°λ νλ‘μ° β μλ‘μ΄ μ κ·Ό #### ν΅μ¬ μμ΄λμ΄: **λ©μΌ μ€μ¬ μν¬νλ‘μ°** ``` π§ MailPlus λ©μΌ μμ β π μλλ‘μ§ μΊλ¦°λ μ΄λ²€νΈ μμ± (μλ/μλ) β π Todo μλ μΆκ° (λ©μΌ λ΄μ© + 첨λΆνμΌ ν¬ν¨) β π μΊλ¦°λ β Todo μλ°©ν₯ λκΈ°ν ``` #### μ₯μ - **λ¨μν¨**: 볡μ‘ν API μ°λ μ΅μν - **μμ°μ€λ¬μ**: κΈ°μ‘΄ λ©μΌ μν¬νλ‘μ° νμ© - **μ μ°μ±**: λ€μν λ©μΌ ν΄λΌμ΄μΈνΈμμ μλ - **μμ μ±**: μλλ‘μ§ κΈ°λ³Έ κΈ°λ₯ νμ© ### κ°μνλ ꡬν λ°©λ² #### 1. λ©μΌ λͺ¨λν°λ§ (ν΅μ¬ κΈ°λ₯) ```python class SimpleMailMonitor: """κ°λ¨ν λ©μΌ λͺ¨λν°λ§ μλΉμ€""" def __init__(self, imap_server, username, password): self.imap_server = imap_server self.username = username self.password = password async def check_new_emails(self): """μ λ©μΌ νμΈ (IMAP)""" import imaplib import email mail = imaplib.IMAP4_SSL(self.imap_server, 993) mail.login(self.username, self.password) mail.select('inbox') # μ½μ§ μμ λ©μΌ κ²μ status, messages = mail.search(None, 'UNSEEN') new_emails = [] for msg_id in messages[0].split(): status, msg_data = mail.fetch(msg_id, '(RFC822)') email_body = msg_data[0][1] email_message = email.message_from_bytes(email_body) new_emails.append({ 'id': msg_id, 'subject': email_message['Subject'], 'from': email_message['From'], 'body': self._extract_body(email_message), 'attachments': self._extract_attachments(email_message) }) mail.close() mail.logout() return new_emails ``` #### 2. λ©μΌ β Todo μλ λ³ν μλΉμ€ ```python class MailToTodoService: """λ©μΌμ Todoλ‘ μλ λ³ννλ κ°λ¨ν μλΉμ€""" def __init__(self, mail_monitor, todo_service): self.mail_monitor = mail_monitor self.todo_service = todo_service async def monitor_emails(self): """λ©μΌ λͺ¨λν°λ§ λ° μλ λ³ν""" while True: try: new_emails = await self.mail_auth.check_new_emails() for email in new_emails: if self._is_todo_email(email): todo_item = await self._convert_email_to_todo(email) await self.todo_service.create_todo(todo_item) # μΊλ¦°λμλ λκΈ°ν await self.calendar_sync.sync_todo_to_calendar(todo_item) except Exception as e: logger.error(f"λ©μΌ λͺ¨λν°λ§ μ€λ₯: {e}") await asyncio.sleep(60) # 1λΆλ§λ€ μ²΄ν¬ def _is_todo_email(self, email): """Todo λ³ν λμ λ©μΌμΈμ§ νλ¨""" # μ λͺ©μ νΉμ ν€μλ ν¬ν¨ todo_keywords = ['ν μΌ', 'TODO', 'Task', 'μμ ', 'μμ²'] subject = email['subject'].lower() return any(keyword.lower() in subject for keyword in todo_keywords) async def _convert_email_to_todo(self, email): """λ©μΌμ Todo νλͺ©μΌλ‘ λ³ν""" # μ λͺ©μμ ν μΌ λ΄μ© μΆμΆ content = self._extract_todo_content(email['subject']) # λ³Έλ¬Έμμ λ μ§/μκ° μΆμΆ due_date = self._extract_date_from_body(email['body']) # 첨λΆνμΌ μ²λ¦¬ attachments = await self._save_attachments(email['attachments']) todo_data = { 'content': content, 'description': email['body'], 'due_date': due_date, 'attachments': attachments, 'source': 'email', 'source_id': email['id'], 'sender': email['from'] } return todo_data def _extract_todo_content(self, subject): """μ λͺ©μμ ν μΌ λ΄μ© μΆμΆ""" # "ν μΌ: λ¬Έμ κ²ν " β "λ¬Έμ κ²ν " # "TODO: Review document" β "Review document" import re patterns = [ r'ν μΌ:\s*(.+)', r'TODO:\s*(.+)', r'Task:\s*(.+)', r'μμ :\s*(.+)', r'μμ²:\s*(.+)' ] for pattern in patterns: match = re.search(pattern, subject, re.IGNORECASE) if match: return match.group(1).strip() return subject # ν¨ν΄μ΄ μμΌλ©΄ μ 체 μ λͺ© μ¬μ© def _extract_date_from_body(self, body): """λ³Έλ¬Έμμ λ μ§ μΆμΆ""" import re from datetime import datetime, timedelta # λ μ§ ν¨ν΄λ€ date_patterns = [ r'(\d{4}-\d{2}-\d{2})', # 2024-01-15 r'(\d{2}/\d{2}/\d{4})', # 01/15/2024 r'(\d{1,2}μ\s*\d{1,2}μΌ)', # 1μ 15μΌ r'(λ΄μΌ|tomorrow)', r'(λ€μμ£Ό|next week)', r'(μ΄λ²μ£Ό|this week)' ] for pattern in date_patterns: match = re.search(pattern, body, re.IGNORECASE) if match: date_str = match.group(1) return self._parse_date_string(date_str) # κΈ°λ³Έκ°: 3μΌ ν return datetime.now() + timedelta(days=3) async def _save_attachments(self, attachments): """첨λΆνμΌμ NASμ μ μ₯""" saved_files = [] for attachment in attachments: # /volume1/todo-data/attachments/ μ μ μ₯ file_path = f"/data/attachments/{attachment['filename']}" with open(file_path, 'wb') as f: f.write(attachment['content']) saved_files.append({ 'filename': attachment['filename'], 'path': file_path, 'size': len(attachment['content']) }) return saved_files ``` ### βοΈ νκ²½ μ€μ #### νκ²½ λ³μ μ€μ ```bash # .env νμΌ # μλλ‘μ§ λ©μΌ μ€μ (νμ) SYNOLOGY_MAIL_SERVER=your-nas.synology.me SYNOLOGY_MAIL_USERNAME=todo_user SYNOLOGY_MAIL_PASSWORD=your_secure_password # λ©μΌ λͺ¨λν°λ§ μ€μ ENABLE_MAIL_MONITORING=true MAIL_CHECK_INTERVAL=60 # μ΄ λ¨μ TODO_KEYWORDS=ν μΌ,TODO,Task,μμ ,μμ² # 첨λΆνμΌ μ μ₯ κ²½λ‘ ATTACHMENTS_PATH=/data/attachments ``` #### μλλ‘μ§ NAS μ€μ ##### 1. MailPlus μ€μ ```bash # MailPlus ν¨ν€μ§μμ: 1. IMAP νμ±ν: μ€μ β IMAP/POP3 β IMAP νμ±ν 2. ν¬νΈ: 993 (SSL) λλ 143 (μΌλ°) 3. μ μ© κ³μ μμ±: todo_mail_user 4. κΆν: MailPlus μ κ·Όλ§ νμ© ``` ##### 2. μΊλ¦°λ μ€μ (μ νμ¬ν) ```bash # Calendar ν¨ν€μ§μμ: 1. μ μΊλ¦°λ μμ±: "Todo Tasks" 2. μμ: 보λΌμ (#6366f1) 3. λ©μΌμμ μΌμ μΆκ° κΈ°λ₯ νμ±ν ``` ### π§ μ¬μ© λ°©λ² #### 1. λ©μΌλ‘ ν μΌ μΆκ° ``` μ λͺ©: ν μΌ: νλ‘μ νΈ λ¬Έμ κ²ν λ³Έλ¬Έ: - λ§κ°μΌ: 2024-01-20 - μμ μμμκ°: 2μκ° - 첨λΆνμΌ: project_spec.pdf β Todo μ±μ μλμΌλ‘ μΆκ°λ¨ ``` #### 2. μΊλ¦°λ μ°λ (μ ν) ``` MailPlus β μΊλ¦°λ μ΄λ²€νΈ μμ± β Todo λκΈ°ν μΊλ¦°λμμ μλ£ μ²λ¦¬ β Todo μν μλ μ λ°μ΄νΈ ``` #### 3. 첨λΆνμΌ κ΄λ¦¬ ``` λ©μΌ 첨λΆνμΌ β NAS μ μ₯ (/data/attachments/) Todoμμ νμΌ λ§ν¬λ‘ μ κ·Ό κ°λ₯ ``` --- ## π± νλ‘ νΈμλ μ€κ³ ### PWA (Progressive Web App) ꡬ쑰 ```javascript // manifest.json { "name": "Todo Project", "short_name": "Todo", "start_url": "/", "display": "standalone", "theme_color": "#6366f1", "shortcuts": [ { "name": "λΉ λ₯Έ ν μΌ μΆκ°", "url": "/quick-add" } ] } ``` ### μ€νλΌμΈ μ§μ ```javascript // Service Worker self.addEventListener('fetch', event => { if (event.request.url.includes('/api/todos')) { event.respondWith( // μ¨λΌμΈ: API νΈμΆ // μ€νλΌμΈ: λ‘컬 μΊμ μ¬μ© caches.match(event.request) || fetch(event.request) ); } }); ``` ### μν κ΄λ¦¬ ```javascript class TodoState { constructor() { this.todos = []; this.syncQueue = []; // μ€νλΌμΈ μ λκΈ°ν λκΈ°μ΄ this.isOnline = navigator.onLine; } async addTodo(content) { const todo = this.createTodo(content); if (this.isOnline) { await this.syncToServer(todo); } else { this.syncQueue.push({action: 'create', todo}); } return todo; } } ``` ### κ°κ²°ν μΈν°νμ΄μ€ μμΉ #### λ©μΈ νλ©΄ ```html