Refactor: Modularize memo.py to 3-layer architecture, Fix tag extraction regex, and Enhance infinite scroll for large screens

This commit is contained in:
leeyj
2026-04-20 11:04:00 +09:00
parent 6fca65eef1
commit ac58e14c8c
10 changed files with 476 additions and 372 deletions
+210
View File
@@ -0,0 +1,210 @@
import datetime
from ..database import get_db
class MemoRepository:
@staticmethod
def get_all(filters, limit=20, offset=0):
"""필터 조건에 따른 메모 목록 조회 및 연관 데이터(태그, 링크 등) 통합 조회"""
conn = get_db()
c = conn.cursor()
where_clauses = []
params = []
group = filters.get('group', 'all')
query = filters.get('query', '')
date = filters.get('date', '')
category = filters.get('category', '')
if group == 'done':
where_clauses.append("status = 'done'")
elif group.startswith('tag:'):
tag_name = group.split(':')[-1]
where_clauses.append("status != 'done'")
where_clauses.append("id IN (SELECT memo_id FROM tags WHERE name = ?)")
params.append(tag_name)
elif group != 'all':
where_clauses.append("status != 'done'")
where_clauses.append("group_name = ?")
params.append(group)
else:
where_clauses.append("status != 'done'")
if query:
where_clauses.append("(title LIKE ? OR content LIKE ?)")
params.extend([f"%{query}%", f"%{query}%"])
if date:
where_clauses.append("created_at LIKE ?")
params.append(f"{date}%")
if category:
where_clauses.append("category = ?")
params.append(category)
where_sql = " AND ".join(where_clauses) if where_clauses else "1=1"
query_sql = f"SELECT * FROM memos WHERE {where_sql} ORDER BY is_pinned DESC, updated_at DESC LIMIT ? OFFSET ?"
c.execute(query_sql, params + [limit, offset])
memo_rows = c.fetchall()
if not memo_rows:
conn.close()
return []
memos = [dict(r) for r in memo_rows]
memo_ids = [m['id'] for m in memos]
placeholders = ','.join(['?'] * len(memo_ids))
# Bulk Fetch Tags
c.execute(f'SELECT memo_id, name, source FROM tags WHERE memo_id IN ({placeholders})', memo_ids)
tags_map = {}
for t in c.fetchall():
tags_map.setdefault(t['memo_id'], []).append(dict(t))
# Bulk Fetch Attachments
c.execute(f'SELECT id, memo_id, filename, original_name, file_type, size FROM attachments WHERE memo_id IN ({placeholders})', memo_ids)
attachments_map = {}
for a in c.fetchall():
attachments_map.setdefault(a['memo_id'], []).append(dict(a))
# Bulk Fetch Backlinks
c.execute(f'''
SELECT ml.target_id, m.id as source_id, m.title
FROM memo_links ml
JOIN memos m ON ml.source_id = m.id
WHERE ml.target_id IN ({placeholders})
''', memo_ids)
backlinks_map = {}
for l in c.fetchall():
backlinks_map.setdefault(l['target_id'], []).append(dict(l))
# Bulk Fetch Forward Links
c.execute(f'''
SELECT ml.source_id, m.id as target_id, m.title
FROM memo_links ml
JOIN memos m ON ml.target_id = m.id
WHERE ml.source_id IN ({placeholders})
''', memo_ids)
links_map = {}
for l in c.fetchall():
links_map.setdefault(l['source_id'], []).append(dict(l))
for m in memos:
m['tags'] = tags_map.get(m['id'], [])
m['attachments'] = attachments_map.get(m['id'], [])
m['backlinks'] = backlinks_map.get(m['id'], [])
m['links'] = links_map.get(m['id'], [])
conn.close()
return memos
@staticmethod
def get_by_id(memo_id):
conn = get_db()
c = conn.cursor()
c.execute('SELECT * FROM memos WHERE id = ?', (memo_id,))
row = c.fetchone()
if not row:
conn.close()
return None
memo = dict(row)
c.execute('SELECT name, source FROM tags WHERE memo_id = ?', (memo_id,))
memo['tags'] = [dict(r) for r in c.fetchall()]
c.execute('SELECT id, filename, original_name, file_type, size FROM attachments WHERE memo_id = ?', (memo_id,))
memo['attachments'] = [dict(r) for r in c.fetchall()]
conn.close()
return memo
@staticmethod
def create(data, tags=[], links=[], attachment_filenames=[]):
conn = get_db()
c = conn.cursor()
try:
placeholders = ', '.join(['?'] * len(data))
columns = ', '.join(data.keys())
c.execute(f'INSERT INTO memos ({columns}) VALUES ({placeholders})', list(data.values()))
memo_id = c.lastrowid
for tag in tags:
if tag.strip():
c.execute('INSERT INTO tags (memo_id, name, source) VALUES (?, ?, ?)', (memo_id, tag.strip(), 'user'))
for target_id in links:
c.execute('INSERT INTO memo_links (source_id, target_id) VALUES (?, ?)', (memo_id, target_id))
for fname in set(attachment_filenames):
c.execute('UPDATE attachments SET memo_id = ? WHERE filename = ?', (memo_id, fname))
conn.commit()
return memo_id
except Exception as e:
conn.rollback()
raise e
finally:
conn.close()
@staticmethod
def update(memo_id, updates, tags=None, links=None, attachment_filenames=None):
conn = get_db()
c = conn.cursor()
try:
if updates:
sql = "UPDATE memos SET " + ", ".join([f"{k} = ?" for k in updates.keys()]) + " WHERE id = ?"
c.execute(sql, list(updates.values()) + [memo_id])
if tags is not None:
c.execute("DELETE FROM tags WHERE memo_id = ?", (memo_id,))
for tag in tags:
if tag.strip():
c.execute('INSERT INTO tags (memo_id, name, source) VALUES (?, ?, ?)', (memo_id, tag.strip(), 'user'))
if links is not None:
c.execute("DELETE FROM memo_links WHERE source_id = ?", (memo_id,))
for target_id in links:
c.execute('INSERT INTO memo_links (source_id, target_id) VALUES (?, ?)', (memo_id, target_id))
if attachment_filenames is not None:
c.execute('UPDATE attachments SET memo_id = NULL WHERE memo_id = ?', (memo_id,))
for fname in set(attachment_filenames):
c.execute('UPDATE attachments SET memo_id = ? WHERE filename = ?', (memo_id, fname))
conn.commit()
return True
except Exception as e:
conn.rollback()
raise e
finally:
conn.close()
@staticmethod
def delete(memo_id):
conn = get_db()
c = conn.cursor()
try:
# Physical file lookup is done in service layer
c.execute('DELETE FROM attachments WHERE memo_id = ?', (memo_id,))
c.execute('DELETE FROM memos WHERE id = ?', (memo_id,))
conn.commit()
return True
except Exception as e:
conn.rollback()
raise e
finally:
conn.close()
@staticmethod
def get_heatmap(days=365):
conn = get_db()
c = conn.cursor()
start_date = (datetime.datetime.now() - datetime.timedelta(days=days)).isoformat()
c.execute('''
SELECT strftime('%Y-%m-%d', created_at) as date, COUNT(*) as count
FROM memos
WHERE created_at >= ?
GROUP BY date
''', (start_date,))
stats = [dict(s) for s in c.fetchall()]
conn.close()
return stats