diff --git a/docs/superpowers/specs/2026-05-05-v024-patch-cleanup-design.md b/docs/superpowers/specs/2026-05-05-v024-patch-cleanup-design.md new file mode 100644 index 0000000..c3c75c8 --- /dev/null +++ b/docs/superpowers/specs/2026-05-05-v024-patch-cleanup-design.md @@ -0,0 +1,54 @@ +# v0.2.4 Patch Cleanup — Design Spec (Brief) + +> 작성: 2026-05-05 · 0.2.3.1 semver 위반 (`X.Y.Z.W` 4-part) → 0.2.4 minor bump 이용해 backlog 의 simple cleanup 5건 + 사용자 가치 1건 합쳐서 묶음 cut. v0.2.4 정식 brainstorm 은 v0.2.5 로 이동. + +## 1. Goal + +PR #21 머지 후 0.2.3.1 binary 빌드 시도가 electron-builder 의 semver validation 으로 실패. 0.2.4 minor bump 으로 우회. 이번 cut 에는 dogfood unblock 외 backlog 의 risk 낮은 cleanup + 사용자 가치 항목 동봉. + +## 2. Scope (5 backlog 항목 + version bump) + +| backlog # | 항목 | 가치 | 작업량 | +|---|---|---|---| +| #1 | `TelemetryService.emit` 의 `now()` 2번 호출 → 1번 추출 | cosmetic (KST midnight straddle 이론) | 1줄 | +| #2 | `DAY_MS = 24*60*60*1000` magic number → 모듈 상단 상수 | cosmetic | 1줄 | +| #6 | `media.gc.run()` `.catch` 누락 → backup pattern 통일 | consistency | 1줄 | +| #13 | NoteCard `mode='trash'` 의 `onDeleted` dead-code prop 제거 | API 청소 | 작음 | +| #44 | 트레이 메뉴 + Inbox footer 에 "Inkling 0.2.4" 버전 정보 | **사용자 dogfood 가치** | 1 task | +| - | version bump 0.2.3.1 → 0.2.4 | semver 표준 | trivial | + +## 3. Out of scope + +- **#45 (자동실행 버그)**: Windows registry 디버깅 필요, simple X. 별도 cut. +- **#3/#4/#26 (KST 통합 / TrayCallbacks refactor)**: multi-file, 크다. 별도. +- **#5/#22 (Union 통합 / hydrate cleanup)**: repo-wide. +- **#39~#43 (PR #21 deferred)**: telemetry masking 등 의미 있는 결정 필요. v0.2.5 brainstorm 영역. +- 기타 backlog 39건. + +## 4. Architecture changes + +본 cut 은 의미 있는 architecture 변경 없음. 기존 pattern 강화만: +- `TelemetryService.emit` 의 atomic timestamp 보장 (now() 1회) +- 모듈 상단 magic number 상수화 패턴 (다른 파일은 이미 그 패턴, TelemetryService 만 예외) +- `.catch` consistency (backup.runDaily / telemetry.cleanupOldFiles 와 동일 wrapper) +- React props 청소 (현재 호출되지 않는 prop 제거) +- 신규 surface: 트레이 메뉴 "Inkling 정보..." → modal 또는 dialog + +## 5. Tests + +테스트 추가 없음 (모두 cosmetic / refactor). 기존 단위 413/413 회귀 X 확인만. + +#44 의 modal 은 컴포넌트 단위 테스트 X (Inkling 패턴 — store-only). + +## 6. Gates + +- typecheck 0 +- 단위 413/413 (회귀 X) +- e2e 1/1 +- backward compat: 기존 사용자 영향 0 (cosmetic + 새 surface) + +## 7. Roadmap relation + +- 0.2.3 cut 7/7 (PR #13~#19) + 0.2.3.1 patch (PR #21) 누적 후 binary 빌드를 위한 v0.2.4 minor bump +- v0.2.5 brainstorm 트리거: dogfood ≥1주 soak + telemetry export + backlog 39건 (=45-5-1) + 신규 피드백 일괄 triage +- backlog 명명 `v024-backlog.md` → 본 cut 후 `v025-backlog.md` 로 rename 검토 (또는 v024-backlog.md 유지하고 내용만 갱신) diff --git a/docs/superpowers/v024-backlog.md b/docs/superpowers/v024-backlog.md index 45fc508..5585ed8 100644 --- a/docs/superpowers/v024-backlog.md +++ b/docs/superpowers/v024-backlog.md @@ -3,8 +3,21 @@ > v0.2.3 cut (7항목 / PR #13~#19) 동안 final reviewer + PR review round 1 에서 발견된 minor / nit 중 의도적으로 deferred 한 항목 누적. v0.2.3 dogfood soak 후 신규 피드백 + 본 리스트 일괄 triage → v0.2.4 cut 결정. **누적 시작일:** 2026-05-01 (#7 telemetry skeleton 머지 시점) -**최종 갱신:** 2026-05-02 (v0.2.3 cut 7/7 완료) -**총 항목 수:** 38 +**최종 갱신:** 2026-05-05 (v0.2.4 patch cut — backlog 5건 처리) +**총 항목 수:** 45 (잔여 39 = 45 − [#1 stale + #2/#6/#13/#44/#45 본 cut 처리 5건] 단 #45 는 별도 cut, 아래 표 참조) + +## 처리 이력 + +| 항목 | 상태 | Cut | +|---|---|---| +| #1 (`now()` 2번 호출) | 이미 fix (PR #13 round 1 — backlog stale) | - | +| #2 (`DAY_MS` magic) | ✅ 처리 | v0.2.4 patch (commit `ef5d3da`) | +| #6 (`media.gc.run()` `.catch`) | ✅ 처리 | v0.2.4 patch (commit `ef5d3da`) | +| #13 (NoteCard `onDeleted` dead-code) | ✅ 처리 | v0.2.4 patch (commit `c87c248`) | +| #44 (버전 정보 surface) | ✅ 처리 (트레이 "Inkling 정보..." + native dialog) | v0.2.4 patch (commit `d3dfe1e`) | +| #45 (자동실행 풀림 버그) | 별도 cut 예정 (Windows registry 디버깅) | TBD | + +**잔여 39건.** v0.2.5 brainstorm 시 신규 dogfood 피드백 + 잔여 39건 일괄 triage. ## Defer 사유 카테고리 diff --git a/package-lock.json b/package-lock.json index 0371cbe..4ddafcc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "inkling", - "version": "0.2.3.1", + "version": "0.2.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "inkling", - "version": "0.2.3.1", + "version": "0.2.4", "dependencies": { "better-sqlite3": "12.9.0", "electron-log": "5.2.0", diff --git a/package.json b/package.json index 864cd91..473ff16 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "inkling", - "version": "0.2.3.1", + "version": "0.2.4", "private": true, "description": "Inkling — local-first 한 줄 보관 도구", "author": "altair823 ", diff --git a/src/main/index.ts b/src/main/index.ts index f4f3932..d102550 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -146,7 +146,9 @@ app.whenReady().then(async () => { await worker.loadFromDb(); const gc = new MediaGc(db, store); - void gc.run().then((r) => logger.info('media.gc', { ...r } as Record)); + void gc.run() + .then((r) => logger.info('media.gc', { ...r } as Record)) + .catch((e) => logger.warn('media.gc.failed', { reason: String(e) })); const exportSvc = new ExportService(repo, store); const importSvc = new ImportService(repo, store); diff --git a/src/main/services/TelemetryService.ts b/src/main/services/TelemetryService.ts index cb5e0c6..fe7abd1 100644 --- a/src/main/services/TelemetryService.ts +++ b/src/main/services/TelemetryService.ts @@ -4,6 +4,7 @@ import { validateEvent, TelemetryEvent } from './telemetryEvents.js'; import { aggregateStats } from './telemetryStats.js'; const KST_OFFSET_MS = 9 * 60 * 60 * 1000; +const DAY_MS = 24 * 60 * 60 * 1000; function todayKstIso(now: Date): string { const k = new Date(now.getTime() + KST_OFFSET_MS); @@ -52,7 +53,7 @@ export class TelemetryService { } catch { return { removed }; } - const cutoff = new Date(this.now().getTime() - this.retentionDays * 24 * 60 * 60 * 1000); + const cutoff = new Date(this.now().getTime() - this.retentionDays * DAY_MS); const cutoffIso = todayKstIso(cutoff); // KST 일자 비교 for (const name of entries) { const m = /^events-(\d{4}-\d{2}-\d{2})\.jsonl$/.exec(name); @@ -94,7 +95,7 @@ export class TelemetryService { } catch { return events; } - const cutoffMs = this.now().getTime() - this.retentionDays * 24 * 60 * 60 * 1000; + const cutoffMs = this.now().getTime() - this.retentionDays * DAY_MS; const cutoffIso = todayKstIso(new Date(cutoffMs)); // 회차 1 review (PR #13) — 매직 슬라이스 `n.slice(7, 17)` 대신 정규식 capture 그룹으로 // 일자를 추출. prefix 변경 시 정규식 한 곳만 고치면 됨. diff --git a/src/main/tray.ts b/src/main/tray.ts index 38af617..3f02373 100644 --- a/src/main/tray.ts +++ b/src/main/tray.ts @@ -1,6 +1,36 @@ import electron from 'electron'; import type { Tray as TrayType, MenuItemConstructorOptions } from 'electron'; -const { app, Tray, Menu, nativeImage } = electron; +import { platform, release, EOL } from 'node:os'; +const { app, Tray, Menu, nativeImage, dialog, shell, clipboard } = electron; + +function showAboutDialog(): void { + const version = app.getVersion(); + const electronVersion = process.versions.electron ?? '?'; + const nodeVersion = process.versions.node ?? '?'; + const profileDir = app.getPath('userData'); + // OS EOL 사용 — 클립보드 → Notepad 등에서 줄바꿈 정상. + const detail = [ + `버전: ${version}`, + `Electron: ${electronVersion}`, + `Node: ${nodeVersion}`, + `OS: ${platform()} ${release()}`, + `데이터 위치: ${profileDir}` + ].join(EOL); + void dialog.showMessageBox({ + type: 'info', + title: 'Inkling 정보', + message: `Inkling ${version}`, + detail, + buttons: ['확인', '데이터 위치 열기', '정보 복사'], + defaultId: 0, + cancelId: 0 + }).then((r) => { + if (r.response === 1) void shell.openPath(profileDir); + if (r.response === 2) clipboard.writeText(`Inkling ${version}${EOL}${detail}`); + }).catch(() => { + // dialog reject 는 일반 사용에서 발생 X — main process crash 등 예외 케이스 silent. + }); +} let tray: TrayType | null = null; let _showInbox: () => void = () => {}; @@ -60,6 +90,7 @@ function buildMenu() { } else { items.push({ type: 'separator' }); } + items.push({ label: 'Inkling 정보...', click: showAboutDialog }); items.push({ label: '종료', click: () => { app.isQuitting = true; app.quit(); } }); return Menu.buildFromTemplate(items); } diff --git a/src/renderer/inbox/App.tsx b/src/renderer/inbox/App.tsx index 0926b68..54aad1b 100644 --- a/src/renderer/inbox/App.tsx +++ b/src/renderer/inbox/App.tsx @@ -147,7 +147,6 @@ export function App(): React.ReactElement { trashNotes.map((n) => ( removeNote(n.id)} onUpdated={(u) => upsertNote(u)} onRestore={() => void restoreNote(n.id)} onPermanentDelete={() => void permanentDeleteNote(n.id)} diff --git a/src/renderer/inbox/components/NoteCard.tsx b/src/renderer/inbox/components/NoteCard.tsx index 46fd9af..b9242e2 100644 --- a/src/renderer/inbox/components/NoteCard.tsx +++ b/src/renderer/inbox/components/NoteCard.tsx @@ -8,7 +8,7 @@ import { pushTagUndo } from './TagUndoToast.js'; interface Props { note: Note; - onDeleted: () => void; + onDeleted?: () => void; // inbox mode 전용 (trash mode 에서 미사용) onUpdated: (n: Note) => void; mode?: 'inbox' | 'trash'; // default 'inbox' onRestore?: () => void; @@ -119,7 +119,7 @@ export function NoteCard({ note, onDeleted, onUpdated, mode = 'inbox', onRestore async function handleDelete() { if (!window.confirm('이 기억을 버릴까요? 되돌릴 수 없습니다.')) return; await inboxApi.deleteNote(note.id); - onDeleted(); + onDeleted?.(); } async function saveTitle(next: string) {