Files
hyungi_document_server/frontend/src/lib/components/QuickMemoButton.svelte
T
Hyungi Ahn 804ba1f4c7 feat(memos): 체크박스 수정 + 마감일 badge + 대시보드 인터랙션
공통 유틸 memoRenderer.ts 분리 (drift 방지):
- checkbox regex 속성 순서 독립으로 수정 (버그 원인)
- due date: checkbox line 마지막 @YYYY-MM-DD만 badge 변환
  overdue=빨강, soon(3일)=노랑, normal=dim, checked=dim
- toggleTaskLine: taskIndex 기반 안전한 토글
- 날짜 비교 로컬 기준 (TZ 이슈 회피)

메모 페이지:
- 렌더링/토글 공통 유틸 import
- 툴바에 📅 마감일 버튼 추가

대시보드:
- 핀 메모 체크박스 토글 가능 (optimistic + rollback)
- stopPropagation으로 details 토글 충돌 방지
- renderMdSimple → renderMemoHtml 통일

QuickMemoButton:
- 체크리스트 + 마감일 버튼 2개 추가

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:40:18 +09:00

155 lines
4.8 KiB
Svelte

<script>
import { StickyNote, X, Send, ListChecks, CalendarDays } from 'lucide-svelte';
import { api } from '$lib/api';
import { addToast } from '$lib/stores/toast';
import { goto } from '$app/navigation';
import { todayIso } from '$lib/utils/memoRenderer';
import Button from '$lib/components/ui/Button.svelte';
let open = $state(false);
let content = $state('');
let submitting = $state(false);
function toggle() {
open = !open;
if (open) {
// 다음 틱에 textarea에 포커스
requestAnimationFrame(() => {
document.querySelector('[data-quick-memo-input]')?.focus();
});
}
}
function close() {
open = false;
}
async function submit() {
const text = content.trim();
if (!text) return;
submitting = true;
try {
await api('/memos/', {
method: 'POST',
body: JSON.stringify({ content: text }),
});
content = '';
open = false;
addToast('success', '메모 생성됨');
} catch (err) {
addToast('error', '메모 생성 실패');
} finally {
submitting = false;
}
}
function handleKeydown(e) {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
submit();
}
if (e.key === 'Escape') {
close();
}
}
// Ctrl+M 글로벌 단축키
function handleGlobalKeydown(e) {
if (e.key === 'm' && (e.ctrlKey || e.metaKey) && !['INPUT', 'TEXTAREA'].includes(document.activeElement?.tagName)) {
e.preventDefault();
toggle();
}
}
</script>
<svelte:window on:keydown={handleGlobalKeydown} />
<!-- FAB 버튼 -->
{#if !open}
<button
onclick={toggle}
class="fixed bottom-6 right-6 z-40 w-12 h-12 rounded-full bg-accent text-white shadow-lg
flex items-center justify-center hover:bg-accent-hover transition-colors
focus-visible:ring-2 focus-visible:ring-accent-ring focus-visible:outline-none"
title="빠른 메모 (Ctrl+M)"
>
<StickyNote size={20} />
</button>
{/if}
<!-- 빠른 메모 패널 -->
{#if open}
<!-- 배경 오버레이 -->
<button
onclick={close}
class="fixed inset-0 z-40 bg-black/30"
aria-label="닫기"
></button>
<!-- 패널 -->
<div class="fixed bottom-6 right-6 z-50 w-80 bg-surface border border-default rounded-xl shadow-2xl">
<div class="flex items-center justify-between px-4 py-3 border-b border-default">
<span class="text-sm font-semibold text-text">빠른 메모</span>
<button onclick={close} class="p-1 rounded text-dim hover:text-text hover:bg-surface-hover transition-colors">
<X size={16} />
</button>
</div>
<div class="p-4">
<textarea
data-quick-memo-input
bind:value={content}
onkeydown={handleKeydown}
placeholder="메모 입력... (Ctrl+Enter)"
class="w-full h-28 bg-bg border border-default rounded-md px-3 py-2 text-sm text-text
resize-none outline-none focus:border-accent placeholder:text-dim"
></textarea>
<div class="flex items-center gap-0.5 mt-2">
<button
onclick={() => {
const ta = document.querySelector('[data-quick-memo-input]');
if (!ta) return;
const s = ta.selectionStart;
const pre = content.slice(0, s);
const nl = s > 0 && pre[pre.length - 1] !== '\n' ? '\n' : '';
content = pre + nl + '- [ ] ' + content.slice(ta.selectionEnd);
requestAnimationFrame(() => { ta.focus(); ta.setSelectionRange(s + nl.length + 6, s + nl.length + 6); });
}}
class="p-1.5 rounded text-dim hover:text-text hover:bg-surface-hover transition-colors" title="체크리스트"
><ListChecks size={15} /></button>
<button
onclick={() => {
const ta = document.querySelector('[data-quick-memo-input]');
if (!ta) return;
const s = ta.selectionStart;
const ins = ` @${todayIso()}`;
content = content.slice(0, s) + ins + content.slice(ta.selectionEnd);
requestAnimationFrame(() => { ta.focus(); ta.setSelectionRange(s + ins.length, s + ins.length); });
}}
class="p-1.5 rounded text-dim hover:text-text hover:bg-surface-hover transition-colors" title="마감일"
><CalendarDays size={15} /></button>
</div>
<div class="flex justify-between items-center mt-2">
<span class="text-[11px] text-dim">#태그 지원</span>
<div class="flex gap-2">
<Button variant="ghost" size="sm" onclick={() => { close(); goto('/memos'); }}>
목록
</Button>
<Button
variant="primary"
size="sm"
icon={Send}
loading={submitting}
disabled={!content.trim()}
onclick={submit}
>
저장
</Button>
</div>
</div>
</div>
</div>
{/if}