804ba1f4c7
공통 유틸 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>
155 lines
4.8 KiB
Svelte
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}
|