Merge pull request 'chore(release): v0.2.4 — patch cut (backlog 5건 + dogfood unblock)' (#22) from feat/v024-patch-cleanup into main
Reviewed-on: #22
This commit was merged in pull request #22.
This commit is contained in:
@@ -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 유지하고 내용만 갱신)
|
||||
@@ -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 사유 카테고리
|
||||
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "inkling",
|
||||
"version": "0.2.3.1",
|
||||
"version": "0.2.4",
|
||||
"private": true,
|
||||
"description": "Inkling — local-first 한 줄 보관 도구",
|
||||
"author": "altair823 <dlsrks0734@gmail.com>",
|
||||
|
||||
@@ -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<string, unknown>));
|
||||
void gc.run()
|
||||
.then((r) => logger.info('media.gc', { ...r } as Record<string, unknown>))
|
||||
.catch((e) => logger.warn('media.gc.failed', { reason: String(e) }));
|
||||
|
||||
const exportSvc = new ExportService(repo, store);
|
||||
const importSvc = new ImportService(repo, store);
|
||||
|
||||
@@ -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 변경 시 정규식 한 곳만 고치면 됨.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -147,7 +147,6 @@ export function App(): React.ReactElement {
|
||||
trashNotes.map((n) => (
|
||||
<NoteCard
|
||||
key={n.id} note={n} mode="trash"
|
||||
onDeleted={() => removeNote(n.id)}
|
||||
onUpdated={(u) => upsertNote(u)}
|
||||
onRestore={() => void restoreNote(n.id)}
|
||||
onPermanentDelete={() => void permanentDeleteNote(n.id)}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user