diff --git a/src/main/ipc/inboxApi.ts b/src/main/ipc/inboxApi.ts index 06daa52..628ae43 100644 --- a/src/main/ipc/inboxApi.ts +++ b/src/main/ipc/inboxApi.ts @@ -29,6 +29,10 @@ export function registerInboxApi(deps: InboxIpcDeps): void { } ); + ipcMain.handle('inbox:setDueDate', (_e, arg: { noteId: string; date: string | null }) => { + deps.repo.setDueDate(arg.noteId, arg.date); + }); + ipcMain.handle('inbox:delete', async (_e, noteId: string) => { await deps.capture.deleteNote(noteId); }); diff --git a/src/preload/index.ts b/src/preload/index.ts index 3d9acef..00073eb 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -11,6 +11,7 @@ const api: InklingApi = { listNotes: (opts) => ipcRenderer.invoke('inbox:list', opts), updateAiFields: (noteId, fields) => ipcRenderer.invoke('inbox:updateAi', { noteId, fields }), + setDueDate: (noteId, date) => ipcRenderer.invoke('inbox:setDueDate', { noteId, date }), deleteNote: (noteId) => ipcRenderer.invoke('inbox:delete', noteId), setIntent: (noteId, text) => ipcRenderer.invoke('inbox:setIntent', { noteId, text }), dismissIntent: (noteId) => ipcRenderer.invoke('inbox:dismissIntent', noteId), diff --git a/src/renderer/inbox/components/NoteCard.tsx b/src/renderer/inbox/components/NoteCard.tsx index c3277ba..c17c8f6 100644 --- a/src/renderer/inbox/components/NoteCard.tsx +++ b/src/renderer/inbox/components/NoteCard.tsx @@ -15,6 +15,93 @@ const aiBadgeStyle: React.CSSProperties = { background: '#eee', color: '#666', fontSize: 10, borderRadius: 3, verticalAlign: 'middle' }; +function isPastDue(iso: string, today: string): boolean { + return iso < today; // string comparison works for ISO YYYY-MM-DD +} + +function todayKstIso(): string { + const KST_OFFSET_MS = 9 * 60 * 60 * 1000; + const k = new Date(Date.now() + KST_OFFSET_MS); + return k.toISOString().slice(0, 10); +} + +function DueDateBadge({ + value, + isEdited, + today, + onSave +}: { + value: string | null; + isEdited: boolean; + today: string; + onSave: (next: string) => Promise; +}): React.ReactElement { + const [editing, setEditing] = useState(false); + const [draft, setDraft] = useState(value ?? ''); + + React.useEffect(() => { if (!editing) setDraft(value ?? ''); }, [value, editing]); + + if (!editing) { + if (value === null) { + return ( + setEditing(true)} + style={{ fontSize: 11, color: '#bbb', cursor: 'pointer' }} + title="마감일 추가" + > + 📅 마감일 추가 + + ); + } + const past = isPastDue(value, today); + return ( + + setEditing(true)} + style={{ + color: past ? '#999' : '#666', + textDecoration: past ? 'line-through' : 'none', + cursor: 'pointer' + }} + title={past ? '지난 마감일 — 클릭으로 편집' : '클릭으로 편집'} + > + 📅 {value} + + {!isEdited && ( + + AI + + )} + + ); + } + + return ( + setDraft(e.target.value)} + onBlur={async () => { + try { await onSave(draft); } + catch { /* keep editing if invalid */ return; } + setEditing(false); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') (e.target as HTMLInputElement).blur(); + if (e.key === 'Escape') { setDraft(value ?? ''); setEditing(false); } + }} + autoFocus + style={{ fontSize: 11, padding: 1 }} + /> + ); +} + export function NoteCard({ note, onDeleted, onUpdated }: Props): React.ReactElement { const [rawOpen, setRawOpen] = useState(note.aiStatus !== 'done'); const [local, setLocal] = useState(note); @@ -41,6 +128,17 @@ export function NoteCard({ note, onDeleted, onUpdated }: Props): React.ReactElem setLocal(updated); onUpdated(updated); } + async function saveDueDate(next: string) { + const value = next.trim() === '' ? null : next.trim(); + // Light validation: empty or YYYY-MM-DD + if (value !== null && !/^\d{4}-\d{2}-\d{2}$/.test(value)) { + throw new Error('Invalid date'); + } + await inboxApi.setDueDate(note.id, value); + const updated = { ...local, dueDate: value, dueDateEditedByUser: true }; + setLocal(updated); onUpdated(updated); + } + async function removeTag(tagName: string) { const next = local.tags.filter((t) => t.name !== tagName).map((t) => t.name); await inboxApi.updateAiFields(note.id, { tags: next }); @@ -102,6 +200,14 @@ export function NoteCard({ note, onDeleted, onUpdated }: Props): React.ReactElem /> {!local.summaryEditedByUser && AI} +
+ +
{local.tags.length > 0 && (
{local.tags.map((t) => ( diff --git a/src/shared/types.ts b/src/shared/types.ts index 311aad0..8af0560 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -59,6 +59,7 @@ export interface InboxApi { noteId: string, fields: { title?: string; summary?: string; tags?: string[] } ): Promise; + setDueDate(noteId: string, date: string | null): Promise; deleteNote(noteId: string): Promise; setIntent(noteId: string, text: string): Promise; dismissIntent(noteId: string): Promise;