diff --git a/frontend/src/routes/memos/+page.svelte b/frontend/src/routes/memos/+page.svelte index 249f335..c17a0c5 100644 --- a/frontend/src/routes/memos/+page.svelte +++ b/frontend/src/routes/memos/+page.svelte @@ -3,7 +3,8 @@ import { api } from '$lib/api'; import { addToast } from '$lib/stores/toast'; import { renderMemoHtml, todayIso, countHiddenTasks, DEFAULT_HIDE_AFTER_MS } from '$lib/utils/memoRenderer'; - import { Pin, PinOff, Pencil, Trash2, Eye, EyeOff, X, Check, Archive, ArchiveRestore, ListChecks, Bold, Heading, CalendarDays } from 'lucide-svelte'; + import { Pin, PinOff, Pencil, Trash2, Eye, EyeOff, X, Check, Archive, ArchiveRestore, ListChecks, Bold, Heading, CalendarDays, Mic, Calendar, Activity, ArrowRight, FileText, BookOpen } from 'lucide-svelte'; + import { getAccessToken } from '$lib/api'; import Button from '$lib/components/ui/Button.svelte'; import Card from '$lib/components/ui/Card.svelte'; import EmptyState from '$lib/components/ui/EmptyState.svelte'; @@ -249,6 +250,54 @@ } } + // ─── PR-2B: 메모 → events 1-click promote ─── + + async function promoteMemo(memoId, kind) { + const labels = { task: '할 일', calendar_event: '일정', activity_log: '활동 기록' }; + try { + const res = await api(`/memos/${memoId}/promote-to-event`, { + method: 'POST', + body: JSON.stringify({ kind }), + }); + addToast('success', `${labels[kind]} 로 승급 (events #${res.event_id})`); + // 로컬 상태 갱신 — promoted 표시를 위해 메모에 임시 마킹 (서버 미반영, UX 힌트만) + memos = memos.map((m) => m.id === memoId ? { ...m, _last_promoted: { event_id: res.event_id, kind } } : m); + } catch (err) { + addToast('error', err?.detail || '승급 실패'); + } + } + + async function dismissEventSuggestion(memoId) { + try { + const updated = await api(`/memos/${memoId}/dismiss-event-suggestion`, { method: 'POST' }); + memos = memos.map((m) => (m.id === memoId ? updated : m)); + } catch (err) { + addToast('error', '처리 실패'); + } + } + + // voice 메모 audio URL — /api/documents/{id}/file?token= 패턴 재사용 + function voiceAudioUrl(memoId) { + const token = getAccessToken(); + return `/api/documents/${memoId}/file?token=${encodeURIComponent(token ?? '')}`; + } + + // ai_event_kind 별 라벨 / 색상 + const KIND_LABELS = { + note: '메모', + task: '할 일', + calendar_event: '일정', + activity_log: '활동', + reference: '참조', + }; + const KIND_BADGE_CLASS = { + note: 'bg-surface text-dim', + task: 'bg-indigo-100 text-indigo-700', + calendar_event: 'bg-blue-100 text-blue-700', + activity_log: 'bg-emerald-100 text-emerald-700', + reference: 'bg-amber-100 text-amber-700', + }; + async function handleCheckboxClick(e, memo) { const target = e.target; if (target.tagName !== 'INPUT' || target.type !== 'checkbox') return; @@ -473,9 +522,36 @@ {:else} + + {#if memo.source_channel === 'voice' || memo.ai_event_kind || memo._last_promoted} +
+ {#if memo.source_channel === 'voice'} + + 음성 + + {/if} + {#if memo.ai_event_kind && memo.ai_event_kind !== 'note'} + + AI 추천: {KIND_LABELS[memo.ai_event_kind] || memo.ai_event_kind}{memo.ai_event_confidence != null ? ` · ${Math.round(memo.ai_event_confidence * 100)}%` : ''} + + {/if} + {#if memo._last_promoted} + + events #{memo._last_promoted.event_id} + + {/if} +
+ {/if} + {#if memo.title && memo.title !== memo.content?.split('\n')[0]?.replace(/^#+\s*/, '').slice(0, 80)}

{memo.title}

{/if} + + + {#if memo.source_channel === 'voice' && memo.file_path} + + {/if} +
handleCheckboxClick(e, memo)} > - {@html renderMemoHtml(memo.content || '', { - taskStates: memo.memo_task_state ?? {}, - now: nowTick, - })} + {#if memo.content} + {@html renderMemoHtml(memo.content || '', { + taskStates: memo.memo_task_state ?? {}, + now: nowTick, + })} + {:else if memo.source_channel === 'voice'} +

음성 → 텍스트 변환 대기 중…

+ {/if}
{#if countHiddenTasks(memo.memo_task_state, nowTick, DEFAULT_HIDE_AFTER_MS) > 0 || showHiddenByMemo[memo.id]} + + + + + {/if} + {#if editingId !== memo.id} {#if memo.user_tags?.length || memo.ai_tags?.length}