feat(notes): 원문 편집/이력 복원 시 AI 재처리

dogfood: 사용자가 노트 본문 수정해도 기존 AI 제목/요약이 그대로 남는 문제.
NoteRepository.markAiPendingForReprocess 추가 — done/failed/pending 노트를
pending 으로 reset + pending_jobs 재투입. disabled 는 사용자가 명시적으로
비활성화한 상태라 존중하여 no-op.

inboxApi 의 update-raw-text / restore-revision 핸들러가 raw 갱신 후 위
헬퍼 + worker.enqueue + pushNoteUpdated 호출. NoteCard.saveRaw 는 optimistic
으로 aiStatus='pending' 즉시 반영 → UI 가 "Inkling이 정리하는 중…" 즉시
표시, 백엔드 push 로 자동 sync. updateAiResult 의 user-edit 가드가 사용자가
직접 편집한 title/summary 는 새 AI 결과로 덮어쓰지 않으므로 안전.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
th-kim0823
2026-05-14 13:11:38 +09:00
parent a68feae20e
commit 352457189e
4 changed files with 48 additions and 1 deletions

View File

@@ -303,6 +303,7 @@ export function registerInboxApi(deps: InboxIpcDeps): void {
return { ok: false as const, reason: 'empty' as const };
}
deps.repo.updateRawText(id, newText);
await reprocessAi(deps, id);
return { ok: true as const };
});
@@ -311,6 +312,7 @@ export function registerInboxApi(deps: InboxIpcDeps): void {
ipcMain.handle('inbox:restore-revision', async (_e, id: string, revId: number) => {
try {
deps.repo.restoreRevision(id, revId);
await reprocessAi(deps, id);
return { ok: true as const };
} catch (e) {
return { ok: false as const, reason: (e as Error).message };
@@ -344,6 +346,19 @@ export function pushNoteUpdated(getWin: () => BrowserWindow | null, note: Note):
w.webContents.send('note:updated', note);
}
/**
* 원문 변경 후 AI 재처리 트리거. ai_status='pending' 으로 reset + pending_jobs 재투입 +
* worker enqueue + renderer push. disabled 노트는 사용자 명시 비활성화 의도 존중하여 skip.
*/
async function reprocessAi(deps: InboxIpcDeps, id: string): Promise<void> {
const now = new Date().toISOString();
const r = deps.repo.markAiPendingForReprocess(id, now);
if (!r.ok) return;
if (deps.enqueue) await deps.enqueue(id);
const updated = deps.repo.findById(id);
if (updated !== null) pushNoteUpdated(deps.getInboxWindow, updated);
}
export function pushOllamaStatus(getWin: () => BrowserWindow | null, status: HealthResult): void {
const w = getWin();
if (!w || w.isDestroyed()) return;

View File

@@ -269,6 +269,29 @@ export class NoteRepository {
return { ok: true };
}
/**
* raw_text 편집/복원 직후 AI 재처리 트리거. done/failed/pending 노트를 pending 으로
* reset + pending_jobs row 보장 (attempts=0). disabled 는 사용자의 명시적 비활성화
* 의도 존중 — no-op. updateAiResult 의 user-edit 가드 (title_edited_by_user 등) 가
* 사용자가 직접 편집한 필드는 새 AI 결과로 덮어쓰지 않음.
*/
markAiPendingForReprocess(id: string, now: string): { ok: boolean } {
const row = this.db
.prepare(`SELECT ai_status FROM notes WHERE id=? AND deleted_at IS NULL`)
.get(id) as { ai_status: string } | undefined;
if (!row || row.ai_status === 'disabled') return { ok: false };
const tx = this.db.transaction(() => {
this.db
.prepare(`UPDATE notes SET ai_status='pending', ai_error=NULL, updated_at=? WHERE id=?`)
.run(now, id);
this.db
.prepare(`INSERT OR REPLACE INTO pending_jobs (note_id, attempts, next_run_at) VALUES (?, 0, ?)`)
.run(id, now);
});
tx();
return { ok: true };
}
/**
* v0.3.9 — pending 노트의 AI 처리 cancel. ai_status='disabled' 로 전환 + pending_jobs 삭제.
* raw_text 는 보존. 사용자가 무한 pending (Ollama 끊김 등) 에서 빠져나오는 path.

View File

@@ -154,7 +154,15 @@ export function NoteCard({ note, onDeleted, onUpdated, mode = 'inbox', onRestore
if (next.trim().length === 0) return;
const r = await inboxApi.updateRawText(note.id, next);
if (!r.ok) return;
const updated = { ...local, rawText: next, updatedAt: new Date().toISOString() };
// disabled 노트는 AI 재처리 안 됨 (서버에서 skip) — aiStatus 유지.
// 그 외는 optimistic 으로 pending 표시 → AiWorker 완료 시 push 로 자동 sync.
const willReprocess = local.aiStatus !== 'disabled';
const updated = {
...local,
rawText: next,
updatedAt: new Date().toISOString(),
...(willReprocess ? { aiStatus: 'pending' as const, aiError: null } : {})
};
setLocal(updated);
onUpdated(updated);
setEditingRaw(false);

View File

@@ -22,6 +22,7 @@ function makeDeps(overrides: Partial<InboxIpcDeps> = {}): InboxIpcDeps {
updateRawText: vi.fn(),
listRevisions: vi.fn(() => []),
restoreRevision: vi.fn(),
markAiPendingForReprocess: vi.fn(() => ({ ok: false })),
findById: vi.fn(),
list: vi.fn(),
listByStatus: vi.fn(),