F3: '구출' (rescue) is unnatural everyday Korean. Replace per-surface: - 트레이 '구출한 메모 보기' → '보관한 메모 보기' - 트레이 '기억 구출하기' → '한 줄 적기' - 토스트 #2 → '머릿속에서 꺼내 두었습니다.' - 토스트 #3 → '방금 한 줄 잡아뒀습니다.' - QC 힌트 'Ctrl+Enter 구출' → 'Ctrl+Enter 저장' - package.json description → 'local-first 한 줄 보관 도구' F4-E (Zeigarnik priming): empty state copy reframed to evoke the "unfinished thought tugging at memory" → "외재화로 해소" loop: - '첫 기억을 구출해보세요.' → '머릿속에 떠다니는 한 줄을 적어보세요.' E2E smoke assertion updated to match. Slice §1.1 invariant 5 ('실패/끊김/연속 실패' 금지) preserved. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
34 lines
912 B
TypeScript
34 lines
912 B
TypeScript
import { createHash } from 'node:crypto';
|
|
|
|
export const REWARD_COPIES = [
|
|
'이 생각은 이제 Inkling이 들고 있습니다.',
|
|
'머릿속에서 꺼내 두었습니다.',
|
|
'방금 한 줄 잡아뒀습니다.',
|
|
'기록 완료. 이제 잊어도 됩니다.'
|
|
] as const;
|
|
|
|
export interface NotificationDeps {
|
|
isSupported: () => boolean;
|
|
send: (body: string) => void;
|
|
}
|
|
|
|
export class NotificationService {
|
|
constructor(private deps: NotificationDeps) {}
|
|
|
|
celebrate(noteId: string): void {
|
|
if (!this.deps.isSupported()) return;
|
|
const idx = this.pick(noteId);
|
|
const body = REWARD_COPIES[idx]!;
|
|
try {
|
|
this.deps.send(body);
|
|
} catch {
|
|
// Swallow notification errors — capture must not fail because of toast.
|
|
}
|
|
}
|
|
|
|
private pick(noteId: string): number {
|
|
const hash = createHash('sha256').update(noteId).digest();
|
|
return hash[0]! % REWARD_COPIES.length;
|
|
}
|
|
}
|