diff --git a/CHANGELOG.md b/CHANGELOG.md index c8b14a3..9ff42df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,25 @@ 본 파일은 Inkling 의 버전별 사용자 영향 변경 사항을 기록한다. 형식은 [Keep a Changelog](https://keepachangelog.com/) 를 느슨하게 따른다. +## [0.3.7] — 2026-05-11 + +`MoveStatusModal` 의 button hardcode 로 인해 완료/보관/휴지통 노트가 inbox 로 돌아올 수 없던 버그 fix. v0.2.9 Cut B 부터 존재한 잠재 결함 (dropdown 의 `possibleTargets` 필터가 modal 까지 흐르지 못함). + +### 수정 + +- **완료/보관/휴지통 노트의 Inbox 복원 path 부재.** `MoveStatusModal` 이 `완료/보관/휴지통` 3 button hardcode 라 currentStatus 외 3 status 만 동적으로 노출해야 한다는 의도가 누락. `currentStatus: NoteStatus` prop 추가 + 4 status 중 current 제외 동적 render. NoteCard 가 `local.status` 전달. +- **status label 일관성** — `statusLabel('active')` 가 '활성' 이었으나 헤더 탭 표기는 'Inbox'. modal button + AI 추천 텍스트 양쪽 모두 'Inbox' 로 통일. + +### 게이트 + +- 단위 736 → **739 PASS** (+3: completed/archived/trashed currentStatus button list 검증) +- typecheck 0 errors (src) +- 신규 npm dependency 0 + +### 업그레이드 + +v0.3.6 인스톨러 위에 v0.3.7 인스톨러를 같은 위치에 실행하면 in-place 업그레이드. 데이터/마이그레이션 변경 없음. + ## [0.3.6] — 2026-05-11 v0.3.5 의 이동 dropdown 단순화가 사용자 의도와 어긋난 점 정정. 이동 modal (사유 + AI 자동 분류 + 수동 status 선택) 은 보존해야 하는 핵심 UX 였음. diff --git a/package-lock.json b/package-lock.json index 554489f..e70f49e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "inkling", - "version": "0.3.6", + "version": "0.3.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "inkling", - "version": "0.3.6", + "version": "0.3.7", "dependencies": { "better-sqlite3": "12.9.0", "electron-log": "5.2.0", diff --git a/package.json b/package.json index 8aecc98..f8b6882 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "inkling", - "version": "0.3.6", + "version": "0.3.7", "private": true, "description": "Inkling — local-first 한 줄 보관 도구", "author": "altair823 ", diff --git a/src/renderer/inbox/components/MoveStatusModal.tsx b/src/renderer/inbox/components/MoveStatusModal.tsx index 814f0b7..fa8bac1 100644 --- a/src/renderer/inbox/components/MoveStatusModal.tsx +++ b/src/renderer/inbox/components/MoveStatusModal.tsx @@ -6,17 +6,24 @@ interface Props { noteId: string; rawText: string; summary: string; + /** 현재 노트 status. 이 값을 제외한 나머지 status 가 이동 버튼으로 노출. */ + currentStatus: NoteStatus; onClose: () => void; onMoved: (status: NoteStatus, reason: string | null) => void; } /** - * v0.2.9 Cut B Task 7 — 메모 이동 Modal. + * 메모 이동 Modal. * - * 사유 입력 + 3 status 버튼 (완료/보관/휴지통) + AI 자동 분류. + * 사유 입력 + AI 자동 분류 + 수동 status 선택. 버튼은 currentStatus 를 제외한 + * 나머지 status 만 노출 (v0.3.6 까지는 완료/보관/휴지통 hardcode 라 완료/보관 노트가 + * inbox 로 못 돌아오던 버그를 v0.3.7 에서 정정). */ +const ALL_STATUSES: readonly NoteStatus[] = ['active', 'completed', 'archived', 'trashed']; + export function MoveStatusModal({ noteId, + currentStatus, onClose, onMoved }: Props): React.ReactElement { @@ -90,9 +97,11 @@ export function MoveStatusModal({ - - - + {ALL_STATUSES.filter((s) => s !== currentStatus).map((s) => ( + + ))} @@ -126,7 +135,8 @@ export function MoveStatusModal({ export function statusLabel(s: NoteStatus): string { switch (s) { case 'active': - return '활성'; + // 헤더 탭 표기 ('Inbox') 와 일치. UI 전반에서 active = Inbox 동의어. + return 'Inbox'; case 'completed': return '완료'; case 'archived': diff --git a/src/renderer/inbox/components/NoteCard.tsx b/src/renderer/inbox/components/NoteCard.tsx index 11f6612..ef6280b 100644 --- a/src/renderer/inbox/components/NoteCard.tsx +++ b/src/renderer/inbox/components/NoteCard.tsx @@ -468,6 +468,7 @@ export function NoteCard({ note, onDeleted, onUpdated, mode = 'inbox', onRestore noteId={local.id} rawText={local.rawText} summary={local.aiSummary ?? ''} + currentStatus={local.status} onClose={() => setMoveOpen(false)} onMoved={(newStatus, reason) => { const updated = { ...local, status: newStatus, moveReason: reason }; diff --git a/tests/unit/MoveStatusModal.test.tsx b/tests/unit/MoveStatusModal.test.tsx index c083a86..7e41104 100644 --- a/tests/unit/MoveStatusModal.test.tsx +++ b/tests/unit/MoveStatusModal.test.tsx @@ -32,6 +32,7 @@ describe('MoveStatusModal', () => { noteId="n1" rawText="t" summary="" + currentStatus="active" onClose={vi.fn()} onMoved={vi.fn()} /> @@ -50,6 +51,7 @@ describe('MoveStatusModal', () => { noteId="n1" rawText="t" summary="" + currentStatus="active" onClose={vi.fn()} onMoved={onMoved} /> @@ -69,6 +71,7 @@ describe('MoveStatusModal', () => { noteId="n1" rawText="t" summary="" + currentStatus="active" onClose={vi.fn()} onMoved={onMoved} /> @@ -81,6 +84,59 @@ describe('MoveStatusModal', () => { await waitFor(() => expect(onMoved).toHaveBeenCalledWith('completed', '결재 끝')); }); + it('currentStatus=completed → Inbox/보관/휴지통 노출, 완료 미노출', () => { + render( + + ); + expect(screen.getByRole('button', { name: 'Inbox' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '보관' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '휴지통' })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: '완료' })).toBeNull(); + }); + + it('currentStatus=archived → Inbox 버튼 클릭 시 setStatus("active") 호출', async () => { + const onMoved = vi.fn(); + render( + + ); + fireEvent.click(screen.getByRole('button', { name: 'Inbox' })); + await waitFor(() => { + expect(mockSetStatus).toHaveBeenCalledWith('n1', 'active', null); + expect(onMoved).toHaveBeenCalledWith('active', null); + }); + }); + + it('currentStatus=trashed → Inbox/완료/보관 노출, 휴지통 미노출', () => { + render( + + ); + expect(screen.getByRole('button', { name: 'Inbox' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '완료' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '보관' })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: '휴지통' })).toBeNull(); + }); + it('빈 사유 → null reason 전달', async () => { const onMoved = vi.fn(); render( @@ -88,6 +144,7 @@ describe('MoveStatusModal', () => { noteId="n1" rawText="t" summary="" + currentStatus="active" onClose={vi.fn()} onMoved={onMoved} />