feat(memos): 선택적 제목, 툴바 버튼, 대시보드 핀 펼침
메모 입력/편집: - 선택적 제목 토글 (기본 숨김, "제목" 버튼으로 활성화) - 툴바 버튼: 체크리스트/굵게/제목 (모바일에서 마크다운 수동 입력 불필요) - 편집 모드에도 동일 툴바 대시보드 핀 메모: - 클릭 시 /memos 이동 대신 인라인 펼침/접힘 (details) - 제목이 있으면 제목 표시, 없으면 첫 줄 - 펼치면 마크다운 렌더링된 본문 + "메모함에서 보기" 링크 Backend: - MemoCreate/MemoUpdate에 선택적 title 파라미터 복원 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+4
-2
@@ -69,11 +69,13 @@ async def _enqueue_ai_stages(session: AsyncSession, document_id: int):
|
||||
|
||||
class MemoCreate(BaseModel):
|
||||
content: str
|
||||
title: str | None = None # 선택적 제목 (없으면 첫 줄 자동 생성)
|
||||
ask_includable: bool = True
|
||||
|
||||
|
||||
class MemoUpdate(BaseModel):
|
||||
content: str
|
||||
title: str | None = None # 명시 제목 변경 (None이면 자동 생성)
|
||||
|
||||
|
||||
class ArchiveSet(BaseModel):
|
||||
@@ -146,7 +148,7 @@ async def create_memo(
|
||||
file_format="md",
|
||||
file_size=len(content.encode("utf-8")),
|
||||
file_type="note",
|
||||
title=_auto_title(content),
|
||||
title=body.title.strip() if body.title and body.title.strip() else _auto_title(content),
|
||||
extracted_text=content,
|
||||
review_status="approved",
|
||||
source_channel="memo",
|
||||
@@ -245,7 +247,7 @@ async def update_memo(
|
||||
doc.extracted_text = content
|
||||
doc.file_hash = _content_hash(content)
|
||||
doc.file_size = len(content.encode("utf-8"))
|
||||
doc.title = _auto_title(content)
|
||||
doc.title = body.title.strip() if body.title and body.title.strip() else _auto_title(content)
|
||||
doc.user_tags = _parse_hashtags(content)
|
||||
|
||||
# stale AI 데이터 즉시 초기화
|
||||
|
||||
@@ -14,6 +14,17 @@
|
||||
import EmptyState from '$lib/components/ui/EmptyState.svelte';
|
||||
import Skeleton from '$lib/components/ui/Skeleton.svelte';
|
||||
import { Inbox, Scale, FileText, Activity, StickyNote, Newspaper, Pin, ChevronRight } from 'lucide-svelte';
|
||||
import { marked } from 'marked';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
marked.use({ gfm: true });
|
||||
function renderMdSimple(text) {
|
||||
return DOMPurify.sanitize(marked(text), {
|
||||
USE_PROFILES: { html: true },
|
||||
FORBID_TAGS: ['style', 'script'],
|
||||
FORBID_ATTR: ['onerror', 'onclick'],
|
||||
});
|
||||
}
|
||||
|
||||
let summary = $derived<DashboardSummary | null>($dashboardSummary);
|
||||
let loading = $derived(summary === null);
|
||||
@@ -114,20 +125,28 @@
|
||||
|
||||
{:else if summary}
|
||||
|
||||
<!-- ═══ 2. 핀 고정 메모 (조건부) ═══ -->
|
||||
<!-- ═══ 2. 핀 고정 메모 (조건부, 펼침/접힘) ═══ -->
|
||||
{#if pinnedMemos.length > 0}
|
||||
<div class="mb-5 space-y-1.5">
|
||||
{#each pinnedMemos as memo (memo.id)}
|
||||
<a
|
||||
href="/memos"
|
||||
class="flex items-center gap-2.5 px-3 py-2 bg-surface border border-default/50 rounded-lg
|
||||
hover:bg-surface-hover transition-colors text-sm group"
|
||||
>
|
||||
<Pin size={13} class="text-accent shrink-0" />
|
||||
<span class="text-text truncate group-hover:text-accent transition-colors">
|
||||
{memo.content?.split('\n')[0] || '메모'}
|
||||
</span>
|
||||
</a>
|
||||
<details class="group/pin">
|
||||
<summary class="flex items-center gap-2.5 px-3 py-2 bg-surface border border-default/50 rounded-lg
|
||||
hover:bg-surface-hover transition-colors text-sm cursor-pointer select-none list-none">
|
||||
<Pin size={13} class="text-accent shrink-0" />
|
||||
<span class="text-text truncate flex-1">
|
||||
{memo.title && memo.title !== memo.content?.split('\n')[0]?.replace(/^#+\s*/, '').slice(0, 80)
|
||||
? memo.title
|
||||
: memo.content?.split('\n')[0] || '메모'}
|
||||
</span>
|
||||
<ChevronRight size={13} class="text-dim shrink-0 transition-transform group-open/pin:rotate-90" />
|
||||
</summary>
|
||||
<div class="mt-1 px-3 py-2.5 bg-surface/50 border border-default/30 rounded-lg text-sm text-text">
|
||||
<div class="prose prose-sm max-w-none memo-content-pin">
|
||||
{@html renderMdSimple(memo.content || '')}
|
||||
</div>
|
||||
<a href="/memos" class="text-[11px] text-accent hover:underline mt-2 inline-block">메모함에서 보기 →</a>
|
||||
</div>
|
||||
</details>
|
||||
{/each}
|
||||
{#if pinnedMemos.length >= 3}
|
||||
<a href="/memos" class="text-[11px] text-accent hover:underline pl-8">더보기 →</a>
|
||||
@@ -304,4 +323,8 @@
|
||||
<style>
|
||||
details[open] .details-chevron { transform: rotate(90deg); }
|
||||
details summary::-webkit-details-marker { display: none; }
|
||||
.memo-content-pin :global(p) { margin: 0.2em 0; }
|
||||
.memo-content-pin :global(ul), .memo-content-pin :global(ol) { margin: 0.2em 0; padding-left: 1.5em; }
|
||||
.memo-content-pin :global(code) { background: var(--bg); padding: 0.1em 0.3em; border-radius: 3px; font-size: 0.85em; }
|
||||
.memo-content-pin :global(a) { color: var(--accent); }
|
||||
</style>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { addToast } from '$lib/stores/toast';
|
||||
import { marked } from 'marked';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { Pin, PinOff, Pencil, Trash2, Eye, EyeOff, X, Check, Archive, ArchiveRestore } from 'lucide-svelte';
|
||||
import { Pin, PinOff, Pencil, Trash2, Eye, EyeOff, X, Check, Archive, ArchiveRestore, ListChecks, Bold, Heading, ChevronDown, ChevronRight } from 'lucide-svelte';
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
import Card from '$lib/components/ui/Card.svelte';
|
||||
import EmptyState from '$lib/components/ui/EmptyState.svelte';
|
||||
@@ -16,16 +16,13 @@
|
||||
// task list 줄 패턴
|
||||
const TASK_RE = /^(\s*- \[)([ x])(\].*)$/;
|
||||
|
||||
// 렌더링 — #태그 링크 + task checkbox에 data-task-index 부여
|
||||
function renderMd(text) {
|
||||
// #태그를 클릭 가능한 링크로 변환
|
||||
const withTags = text.replace(
|
||||
/(?:^|(?<=\s))#([가-힣a-zA-Z0-9_]{2,})/g,
|
||||
'<a href="/memos?tag=$1" class="text-accent hover:underline">#$1</a>'
|
||||
);
|
||||
let html = marked(withTags);
|
||||
|
||||
// task checkbox에 data-task-index 부여
|
||||
let taskIdx = 0;
|
||||
html = html.replace(
|
||||
/<input\s+(checked\s+)?disabled\s+type="checkbox"\s*\/?>/gi,
|
||||
@@ -46,7 +43,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
// task toggle — n번째 task item 줄만 토글
|
||||
function toggleTask(content, taskIndex) {
|
||||
const lines = content.split('\n');
|
||||
let ti = 0;
|
||||
@@ -70,11 +66,15 @@
|
||||
|
||||
// 빠른 입력
|
||||
let newContent = $state('');
|
||||
let newTitle = $state('');
|
||||
let showTitle = $state(false);
|
||||
let submitting = $state(false);
|
||||
let inputRef = $state(null);
|
||||
|
||||
// 인라인 편집
|
||||
let editingId = $state(null);
|
||||
let editContent = $state('');
|
||||
let editTitle = $state('');
|
||||
let saving = $state(false);
|
||||
|
||||
// 필터
|
||||
@@ -106,13 +106,17 @@
|
||||
if (!content) return;
|
||||
submitting = true;
|
||||
try {
|
||||
const body = { content };
|
||||
if (newTitle.trim()) body.title = newTitle.trim();
|
||||
const memo = await api('/memos/', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ content }),
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
memos = [memo, ...memos];
|
||||
total += 1;
|
||||
newContent = '';
|
||||
newTitle = '';
|
||||
showTitle = false;
|
||||
addToast('success', '메모 생성됨');
|
||||
} catch (err) {
|
||||
addToast('error', '메모 생성 실패');
|
||||
@@ -121,29 +125,98 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 툴바: textarea에 텍스트 삽입 ───
|
||||
function insertAtCursor(textarea, text) {
|
||||
if (!textarea) return;
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const before = newContent.slice(0, start);
|
||||
const after = newContent.slice(end);
|
||||
|
||||
// 줄 시작에 삽입해야 하는 경우
|
||||
const needsNewline = start > 0 && before[before.length - 1] !== '\n';
|
||||
const prefix = needsNewline ? '\n' : '';
|
||||
|
||||
newContent = before + prefix + text + after;
|
||||
// 커서 위치 조정
|
||||
requestAnimationFrame(() => {
|
||||
const pos = start + prefix.length + text.length;
|
||||
textarea.focus();
|
||||
textarea.setSelectionRange(pos, pos);
|
||||
});
|
||||
}
|
||||
|
||||
function addCheckbox() {
|
||||
const ta = document.querySelector('[data-memo-input]');
|
||||
insertAtCursor(ta, '- [ ] ');
|
||||
}
|
||||
|
||||
function addHeading() {
|
||||
const ta = document.querySelector('[data-memo-input]');
|
||||
insertAtCursor(ta, '## ');
|
||||
}
|
||||
|
||||
function addBold() {
|
||||
const ta = document.querySelector('[data-memo-input]');
|
||||
if (!ta) return;
|
||||
const start = ta.selectionStart;
|
||||
const end = ta.selectionEnd;
|
||||
const selected = newContent.slice(start, end);
|
||||
if (selected) {
|
||||
newContent = newContent.slice(0, start) + `**${selected}**` + newContent.slice(end);
|
||||
requestAnimationFrame(() => {
|
||||
ta.focus();
|
||||
ta.setSelectionRange(start + 2, end + 2);
|
||||
});
|
||||
} else {
|
||||
insertAtCursor(ta, '**텍스트**');
|
||||
}
|
||||
}
|
||||
|
||||
// 편집 모드 툴바
|
||||
function editInsertAtCursor(text) {
|
||||
const ta = document.querySelector('[data-edit-input]');
|
||||
if (!ta) return;
|
||||
const start = ta.selectionStart;
|
||||
const end = ta.selectionEnd;
|
||||
const before = editContent.slice(0, start);
|
||||
const after = editContent.slice(end);
|
||||
const needsNewline = start > 0 && before[before.length - 1] !== '\n';
|
||||
const prefix = needsNewline ? '\n' : '';
|
||||
editContent = before + prefix + text + after;
|
||||
requestAnimationFrame(() => {
|
||||
const pos = start + prefix.length + text.length;
|
||||
ta.focus();
|
||||
ta.setSelectionRange(pos, pos);
|
||||
});
|
||||
}
|
||||
|
||||
function startEdit(memo) {
|
||||
editingId = memo.id;
|
||||
editContent = memo.content || '';
|
||||
editTitle = memo.title || '';
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
editingId = null;
|
||||
editContent = '';
|
||||
editTitle = '';
|
||||
}
|
||||
|
||||
async function saveEdit(memoId) {
|
||||
saving = true;
|
||||
try {
|
||||
const body = { content: editContent };
|
||||
if (editTitle.trim()) body.title = editTitle.trim();
|
||||
const updated = await api(`/memos/${memoId}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ content: editContent }),
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
memos = memos.map((m) => (m.id === memoId ? updated : m));
|
||||
editingId = null;
|
||||
addToast('success', '메모 수정됨');
|
||||
} catch (err) {
|
||||
addToast('error', '메모 수정 실패');
|
||||
// 실패 시 편집 상태 유지 (내용 손실 방지)
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
@@ -182,7 +255,6 @@
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ archived }),
|
||||
});
|
||||
// 아카이브/복원 시 현재 뷰에서 제거
|
||||
memos = memos.filter((m) => m.id !== memoId);
|
||||
total -= 1;
|
||||
addToast('success', archived ? '아카이브됨' : '복원됨');
|
||||
@@ -200,19 +272,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 체크박스 클릭 핸들러 (이벤트 위임)
|
||||
async function handleCheckboxClick(e, memo) {
|
||||
const target = e.target;
|
||||
if (target.tagName !== 'INPUT' || target.type !== 'checkbox') return;
|
||||
|
||||
e.preventDefault();
|
||||
const taskIndex = parseInt(target.dataset.taskIndex, 10);
|
||||
if (isNaN(taskIndex)) return;
|
||||
|
||||
const oldContent = memo.content;
|
||||
const newCont = toggleTask(oldContent, taskIndex);
|
||||
|
||||
// optimistic update
|
||||
memo.content = newCont;
|
||||
memos = [...memos];
|
||||
|
||||
@@ -223,7 +291,6 @@
|
||||
});
|
||||
memos = memos.map((m) => (m.id === memo.id ? updated : m));
|
||||
} catch (err) {
|
||||
// 롤백
|
||||
memo.content = oldContent;
|
||||
memos = [...memos];
|
||||
addToast('error', '체크박스 변경 실패');
|
||||
@@ -261,9 +328,7 @@
|
||||
e.preventDefault();
|
||||
saveEdit(memoId);
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
cancelEdit();
|
||||
}
|
||||
if (e.key === 'Escape') { cancelEdit(); }
|
||||
}
|
||||
|
||||
function formatTime(dateStr) {
|
||||
@@ -293,7 +358,7 @@
|
||||
{showArchived ? 'bg-accent/15 text-accent' : 'text-dim hover:text-text hover:bg-surface'}"
|
||||
>
|
||||
<Archive size={12} />
|
||||
{showArchived ? '아카이브' : '아카이브'}
|
||||
아카이브
|
||||
</button>
|
||||
{#if total > 0}
|
||||
<span class="text-sm text-dim">{total}개</span>
|
||||
@@ -301,7 +366,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 태그 필터 표시 -->
|
||||
<!-- 태그 필터 -->
|
||||
{#if activeTag}
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<span class="text-sm text-dim">필터:</span>
|
||||
@@ -309,22 +374,54 @@
|
||||
onclick={clearTag}
|
||||
class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs bg-accent/15 text-accent hover:bg-accent/25 transition-colors"
|
||||
>
|
||||
#{activeTag}
|
||||
<X size={12} />
|
||||
#{activeTag} <X size={12} />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- 빠른 입력 (아카이브 뷰에서는 숨김) -->
|
||||
<!-- ═══ 빠른 입력 ═══ -->
|
||||
{#if !showArchived}
|
||||
<Card class="mb-5">
|
||||
<!-- 선택적 제목 -->
|
||||
{#if showTitle}
|
||||
<input
|
||||
type="text"
|
||||
bind:value={newTitle}
|
||||
placeholder="제목 (선택)"
|
||||
class="w-full bg-transparent text-text text-sm font-medium outline-none placeholder:text-dim mb-2 pb-2 border-b border-default/50"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<textarea
|
||||
data-memo-input
|
||||
bind:value={newContent}
|
||||
onkeydown={handleKeydown}
|
||||
placeholder="메모 입력... (Ctrl+Enter 저장, #태그, - [ ] 체크리스트)"
|
||||
placeholder="메모 입력... (Ctrl+Enter 저장)"
|
||||
class="w-full h-20 bg-transparent text-text text-sm resize-none outline-none placeholder:text-dim"
|
||||
></textarea>
|
||||
<div class="flex justify-end mt-1.5">
|
||||
|
||||
<!-- 툴바 + 저장 -->
|
||||
<div class="flex items-center justify-between mt-1.5 pt-1.5 border-t border-default/30">
|
||||
<div class="flex items-center gap-0.5">
|
||||
<button onclick={addCheckbox} class="p-1.5 rounded text-dim hover:text-text hover:bg-surface transition-colors" title="체크리스트">
|
||||
<ListChecks size={16} />
|
||||
</button>
|
||||
<button onclick={addBold} class="p-1.5 rounded text-dim hover:text-text hover:bg-surface transition-colors" title="굵게">
|
||||
<Bold size={16} />
|
||||
</button>
|
||||
<button onclick={addHeading} class="p-1.5 rounded text-dim hover:text-text hover:bg-surface transition-colors" title="제목">
|
||||
<Heading size={16} />
|
||||
</button>
|
||||
<span class="w-px h-4 bg-default/50 mx-1"></span>
|
||||
<button
|
||||
onclick={() => (showTitle = !showTitle)}
|
||||
class="px-1.5 py-1 rounded text-[11px] transition-colors
|
||||
{showTitle ? 'text-accent bg-accent/10' : 'text-dim hover:text-text hover:bg-surface'}"
|
||||
title="제목 추가"
|
||||
>
|
||||
제목
|
||||
</button>
|
||||
</div>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
@@ -338,12 +435,10 @@
|
||||
</Card>
|
||||
{/if}
|
||||
|
||||
<!-- 메모 피드 -->
|
||||
<!-- ═══ 메모 피드 ═══ -->
|
||||
{#if loading}
|
||||
{#each Array(3) as _}
|
||||
<div class="mb-3">
|
||||
<Skeleton class="h-24 w-full rounded-card" />
|
||||
</div>
|
||||
<div class="mb-3"><Skeleton class="h-24 w-full rounded-card" /></div>
|
||||
{/each}
|
||||
{:else if memos.length === 0}
|
||||
<EmptyState>
|
||||
@@ -360,7 +455,6 @@
|
||||
<div class="space-y-2.5">
|
||||
{#each memos as memo (memo.id)}
|
||||
<Card padded={false} class="group relative {showArchived ? 'opacity-70' : ''}">
|
||||
<!-- 핀 뱃지 -->
|
||||
{#if memo.pinned && !showArchived}
|
||||
<div class="absolute top-2 right-2">
|
||||
<span class="text-xs text-accent"><Pin size={12} /></span>
|
||||
@@ -369,22 +463,41 @@
|
||||
|
||||
<div class="px-4 py-3">
|
||||
{#if editingId === memo.id}
|
||||
<!-- 인라인 편집 모드 -->
|
||||
<!-- ═══ 인라인 편집 모드 ═══ -->
|
||||
<input
|
||||
type="text"
|
||||
bind:value={editTitle}
|
||||
placeholder="제목 (선택)"
|
||||
class="w-full bg-transparent text-text text-sm font-medium outline-none placeholder:text-dim mb-2 pb-1 border-b border-default/50"
|
||||
/>
|
||||
<textarea
|
||||
data-edit-input
|
||||
bind:value={editContent}
|
||||
onkeydown={(e) => handleEditKeydown(e, memo.id)}
|
||||
class="w-full h-32 bg-bg border border-default rounded-md px-3 py-2 text-sm text-text resize-none outline-none focus:border-accent"
|
||||
placeholder="메모 내용..."
|
||||
></textarea>
|
||||
<!-- 편집 툴바 -->
|
||||
<div class="flex items-center justify-between mt-2">
|
||||
<span class="text-[11px] text-dim">Ctrl+Enter 저장 / Esc 취소</span>
|
||||
<div class="flex gap-2">
|
||||
<div class="flex items-center gap-0.5">
|
||||
<button onclick={() => editInsertAtCursor('- [ ] ')} class="p-1.5 rounded text-dim hover:text-text hover:bg-surface transition-colors" title="체크리스트">
|
||||
<ListChecks size={15} />
|
||||
</button>
|
||||
<button onclick={() => editInsertAtCursor('**텍스트**')} class="p-1.5 rounded text-dim hover:text-text hover:bg-surface transition-colors" title="굵게">
|
||||
<Bold size={15} />
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="text-[11px] text-dim">Ctrl+Enter / Esc</span>
|
||||
<Button variant="ghost" size="sm" icon={X} onclick={cancelEdit}>취소</Button>
|
||||
<Button variant="primary" size="sm" icon={Check} loading={saving} onclick={() => saveEdit(memo.id)}>저장</Button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- 읽기 모드 -->
|
||||
<!-- ═══ 읽기 모드 ═══ -->
|
||||
{#if memo.title && memo.title !== memo.content?.split('\n')[0]?.replace(/^#+\s*/, '').slice(0, 80)}
|
||||
<p class="text-xs font-semibold text-dim mb-1">{memo.title}</p>
|
||||
{/if}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
@@ -397,75 +510,40 @@
|
||||
|
||||
<!-- 태그 + 하단 -->
|
||||
{#if editingId !== memo.id}
|
||||
<!-- 태그 -->
|
||||
{#if memo.user_tags?.length || memo.ai_tags?.length}
|
||||
<div class="flex flex-wrap gap-1 mt-2">
|
||||
{#each memo.user_tags || [] as tag}
|
||||
<button
|
||||
onclick={() => filterByTag(tag)}
|
||||
class="px-1.5 py-0.5 rounded text-[11px] bg-accent/10 text-accent hover:bg-accent/20 transition-colors"
|
||||
>#{tag}</button>
|
||||
<button onclick={() => filterByTag(tag)} class="px-1.5 py-0.5 rounded text-[11px] bg-accent/10 text-accent hover:bg-accent/20 transition-colors">#{tag}</button>
|
||||
{/each}
|
||||
{#each memo.ai_tags || [] as tag}
|
||||
<button
|
||||
onclick={() => filterByTag(tag)}
|
||||
class="px-1.5 py-0.5 rounded text-[11px] bg-surface text-dim hover:bg-surface-hover transition-colors"
|
||||
>{tag}</button>
|
||||
<button onclick={() => filterByTag(tag)} class="px-1.5 py-0.5 rounded text-[11px] bg-surface text-dim hover:bg-surface-hover transition-colors">{tag}</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- 하단: 시간 + 액션 -->
|
||||
<div class="flex items-center justify-between mt-2 pt-1.5 border-t border-default/50">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-[11px] text-dim">{formatTime(memo.created_at)}</span>
|
||||
{#if !memo.ask_includable}
|
||||
<span class="text-[10px] text-dim/50 flex items-center gap-0.5">
|
||||
<EyeOff size={10} /> AI 제외
|
||||
</span>
|
||||
<span class="text-[10px] text-dim/50 flex items-center gap-0.5"><EyeOff size={10} /> AI 제외</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-0.5 md:opacity-0 md:group-hover:opacity-100 transition-opacity">
|
||||
{#if !showArchived}
|
||||
<button
|
||||
onclick={() => togglePin(memo.id)}
|
||||
class="p-1 rounded text-dim hover:text-accent hover:bg-surface transition-colors"
|
||||
title={memo.pinned ? '핀 해제' : '핀 고정'}
|
||||
>
|
||||
<button onclick={() => togglePin(memo.id)} class="p-1 rounded text-dim hover:text-accent hover:bg-surface transition-colors" title={memo.pinned ? '핀 해제' : '핀 고정'}>
|
||||
{#if memo.pinned}<PinOff size={13} />{:else}<Pin size={13} />{/if}
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
onclick={() => toggleAskIncludable(memo.id)}
|
||||
class="p-1 rounded text-dim hover:text-text hover:bg-surface transition-colors"
|
||||
title={memo.ask_includable ? 'AI 답변에서 제외' : 'AI 답변에 포함'}
|
||||
>
|
||||
<button onclick={() => toggleAskIncludable(memo.id)} class="p-1 rounded text-dim hover:text-text hover:bg-surface transition-colors" title={memo.ask_includable ? 'AI 답변에서 제외' : 'AI 답변에 포함'}>
|
||||
{#if memo.ask_includable}<Eye size={13} />{:else}<EyeOff size={13} />{/if}
|
||||
</button>
|
||||
{#if !showArchived}
|
||||
<button
|
||||
onclick={() => startEdit(memo)}
|
||||
class="p-1 rounded text-dim hover:text-text hover:bg-surface transition-colors"
|
||||
title="편집"
|
||||
><Pencil size={13} /></button>
|
||||
<button
|
||||
onclick={() => setArchive(memo.id, true)}
|
||||
class="p-1 rounded text-dim hover:text-text hover:bg-surface transition-colors"
|
||||
title="아카이브"
|
||||
><Archive size={13} /></button>
|
||||
<button onclick={() => startEdit(memo)} class="p-1 rounded text-dim hover:text-text hover:bg-surface transition-colors" title="편집"><Pencil size={13} /></button>
|
||||
<button onclick={() => setArchive(memo.id, true)} class="p-1 rounded text-dim hover:text-text hover:bg-surface transition-colors" title="아카이브"><Archive size={13} /></button>
|
||||
{:else}
|
||||
<button
|
||||
onclick={() => setArchive(memo.id, false)}
|
||||
class="p-1 rounded text-dim hover:text-accent hover:bg-surface transition-colors"
|
||||
title="복원"
|
||||
><ArchiveRestore size={13} /></button>
|
||||
<button onclick={() => setArchive(memo.id, false)} class="p-1 rounded text-dim hover:text-accent hover:bg-surface transition-colors" title="복원"><ArchiveRestore size={13} /></button>
|
||||
{/if}
|
||||
<button
|
||||
onclick={() => deleteMemo(memo.id)}
|
||||
class="p-1 rounded text-dim hover:text-error hover:bg-error/10 transition-colors"
|
||||
title="삭제"
|
||||
><Trash2 size={13} /></button>
|
||||
<button onclick={() => deleteMemo(memo.id)} class="p-1 rounded text-dim hover:text-error hover:bg-error/10 transition-colors" title="삭제"><Trash2 size={13} /></button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -474,7 +552,6 @@
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- 페이지네이션 -->
|
||||
{#if total > 20}
|
||||
<div class="flex justify-center gap-2 mt-6">
|
||||
<Button variant="ghost" size="sm" disabled={page <= 1} onclick={() => { page -= 1; loadMemos(); }}>이전</Button>
|
||||
|
||||
Reference in New Issue
Block a user