diff --git a/src/main/index.ts b/src/main/index.ts
index b30372f..8aee3ec 100644
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -169,7 +169,9 @@ app.whenReady().then(async () => {
registerInboxApi({
repo, continuity, capture, health, intent,
getInboxWindow, settings: settingsSvc, providerHolder,
- paths: { profileDir: paths.profileDir }
+ paths: { profileDir: paths.profileDir },
+ // v0.2.9 Cut B Task 16 — disabled 메모 일괄 재투입 시 in-memory queue 갱신.
+ enqueue: (id) => worker.enqueue(id)
});
// registerSettingsApi 는 backup / exportSvc / importSvc / syncSvc / telemetry 가
// 생성된 뒤에 호출 (Task 10) — 아래 BackupService/ExportService/... 초기화 직후로 이동.
diff --git a/src/main/ipc/inboxApi.ts b/src/main/ipc/inboxApi.ts
index e17ed55..6f33776 100644
--- a/src/main/ipc/inboxApi.ts
+++ b/src/main/ipc/inboxApi.ts
@@ -25,6 +25,10 @@ export interface InboxIpcDeps {
providerHolder: ProviderHolder;
// v0.2.8 Cut A — `inbox:open-media` 의 path traversal 검사 baseline.
paths: { profileDir: string };
+ // v0.2.9 Cut B Task 16 — disabled 메모 일괄 처리 시 in-memory worker queue 갱신.
+ // 미주입 시 fire-and-forget skip (다음 launch 의 loadFromDb 가 처리). 본 hook 은
+ // AiWorker 인스턴스 직접 주입을 피해 IPC 모듈이 worker import 를 갖지 않도록 분리.
+ enqueue?: (noteId: string) => Promise;
}
export function registerInboxApi(deps: InboxIpcDeps): void {
@@ -226,6 +230,29 @@ export function registerInboxApi(deps: InboxIpcDeps): void {
});
});
+ // v0.2.9 Cut B Task 16 — disabled 메모 (ai_enabled OFF 시기 캡처) 일괄 재투입.
+ // OFF→ON 전환 후 사용자가 "지금 모두 처리" 버튼 클릭 path. repo.requeueDisabled 가
+ // ai_status='pending' + pending_jobs row 보장, worker.enqueue 가 in-memory queue 갱신.
+ ipcMain.handle('inbox:enqueue-disabled', async () => {
+ // requeue 전 대상 id 수집 — UPDATE 가 status 바꾸므로 select 후 update 필요 없이
+ // requeueDisabled 가 처리한 다음 pending_jobs 에서 다시 가져와 enqueue.
+ const targets = deps.repo.getAllPendingJobs().map((j) => j.noteId);
+ const before = new Set(targets);
+ const count = deps.repo.requeueDisabled();
+ if (count > 0 && deps.enqueue) {
+ const after = deps.repo.getAllPendingJobs();
+ // requeue 직후 새로 들어온 pending_jobs row 만 enqueue (기존 row 는 이미 in-memory queue 에).
+ for (const j of after) {
+ if (!before.has(j.noteId)) {
+ await deps.enqueue(j.noteId);
+ }
+ }
+ }
+ return { count };
+ });
+
+ ipcMain.handle('inbox:get-disabled-count', () => deps.repo.countByAiStatus('disabled'));
+
ipcMain.handle('inbox:saveOllamaSettings', async (_e, value: { endpoint: string; model: string }) => {
// 검증: 새 인스턴스로 healthCheck
const trial = new LocalOllamaProvider({ endpoint: value.endpoint, model: value.model });
diff --git a/src/main/repository/NoteRepository.ts b/src/main/repository/NoteRepository.ts
index adaecb1..679ec69 100644
--- a/src/main/repository/NoteRepository.ts
+++ b/src/main/repository/NoteRepository.ts
@@ -216,6 +216,50 @@ export class NoteRepository {
return { ids };
}
+ /**
+ * v0.2.9 Cut B Task 16 — 모든 ai_status='disabled' 노트를 'pending' 으로 reset 하고
+ * pending_jobs 재투입. 사용자가 settings.ai_enabled OFF→ON 전환 후 "지금 모두 처리"
+ * 버튼을 누른 path. 단일 transaction. 호출자가 `now` 주입 가능 (테스트성).
+ *
+ * INSERT OR IGNORE — race 안전 (이미 pending_jobs row 존재 시 skip).
+ * 반환값 = 처리된 노트 수 (UI 가 "N건 처리됨" 토스트 등 표시용).
+ */
+ requeueDisabled(now: Date = new Date()): number {
+ const tx = this.db.transaction(() => {
+ const ts = now.toISOString();
+ const targets = this.db
+ .prepare(`SELECT id FROM notes WHERE ai_status='disabled'`)
+ .all() as Array<{ id: string }>;
+ for (const { id } of targets) {
+ this.db
+ .prepare(`UPDATE notes SET ai_status='pending', updated_at=? WHERE id=?`)
+ .run(ts, id);
+ this.db
+ .prepare(
+ `INSERT OR IGNORE INTO pending_jobs (note_id, attempts, next_run_at) VALUES (?, 0, ?)`
+ )
+ .run(id, ts);
+ }
+ return targets.length;
+ });
+ return tx();
+ }
+
+ /**
+ * v0.2.9 Cut B Task 16 — ai_status 별 row count.
+ * 설정 페이지의 "원문만 저장된 메모 N건" 표기용 (status='disabled' 카운트).
+ * deleted_at 필터 없음 — disabled 메모도 trash 갈 수 있는데 사용자 의도는
+ * "AI 처리할 게 얼마나 남았나?" 라 trashed 까지 포함되면 안 됨. → deleted_at IS NULL 추가.
+ */
+ countByAiStatus(status: AiStatus): number {
+ const row = this.db
+ .prepare(
+ `SELECT COUNT(*) AS c FROM notes WHERE ai_status=? AND deleted_at IS NULL`
+ )
+ .get(status) as { c: number };
+ return row.c;
+ }
+
/**
* pending_jobs 의 next_run_at + last_error 만 갱신, attempts 변경 없음.
* v0.2.3 #2 — unreachable/timeout 무한 retry 시 사용 (incrementJobAttempt 와 별도 경로).
diff --git a/src/preload/index.ts b/src/preload/index.ts
index 74355be..33b6d42 100644
--- a/src/preload/index.ts
+++ b/src/preload/index.ts
@@ -78,6 +78,9 @@ const api: InklingApi = {
getSettings: () => ipcRenderer.invoke('settings:get'),
setAiEnabled: (enabled: boolean) => ipcRenderer.invoke('settings:set-ai-enabled', enabled),
setOnboardingCompleted: (completed: boolean) => ipcRenderer.invoke('settings:set-onboarding-completed', completed),
+ // v0.2.9 Cut B Task 16 — disabled 메모 재투입 + count.
+ enqueueDisabled: () => ipcRenderer.invoke('inbox:enqueue-disabled'),
+ getDisabledCount: () => ipcRenderer.invoke('inbox:get-disabled-count'),
}
};
diff --git a/src/renderer/inbox/components/settings/AiProviderSection.tsx b/src/renderer/inbox/components/settings/AiProviderSection.tsx
index db68e77..cd32e12 100644
--- a/src/renderer/inbox/components/settings/AiProviderSection.tsx
+++ b/src/renderer/inbox/components/settings/AiProviderSection.tsx
@@ -10,8 +10,9 @@ export function AiProviderSection(): React.ReactElement {
const [error, setError] = useState(null);
const [saveResult, setSaveResult] = useState(null);
const [recheckResult, setRecheckResult] = useState(null);
- // v0.2.9 Cut B Task 15: AI 자동 처리 토글.
+ // v0.2.9 Cut B Task 15-16: AI 자동 처리 토글 + disabled 메모 일괄 처리.
const [aiEnabled, setAiEnabledState] = useState(null);
+ const [disabledCount, setDisabledCount] = useState(0);
useEffect(() => {
void (async () => {
@@ -21,13 +22,29 @@ export function AiProviderSection(): React.ReactElement {
setModel(s.model);
}
const settings = await inboxApi.getSettings();
- setAiEnabledState(settings.ai_enabled ?? true);
+ const enabled = settings.ai_enabled ?? true;
+ setAiEnabledState(enabled);
+ if (enabled) {
+ const c = await inboxApi.getDisabledCount();
+ setDisabledCount(c);
+ }
})();
}, []);
async function onToggleAi(checked: boolean): Promise {
await inboxApi.setAiEnabled(checked);
setAiEnabledState(checked);
+ if (checked) {
+ const c = await inboxApi.getDisabledCount();
+ setDisabledCount(c);
+ } else {
+ setDisabledCount(0);
+ }
+ }
+
+ async function onProcessDisabled(): Promise {
+ await inboxApi.enqueueDisabled();
+ setDisabledCount(0);
}
async function onSave(): Promise {
@@ -79,6 +96,27 @@ export function AiProviderSection(): React.ReactElement {
)}
+ {/* v0.2.9 Cut B Task 16 — ON 전환 후 disabled 메모 일괄 처리 prompt */}
+ {aiEnabled === true && disabledCount > 0 && (
+
+ 원문만 저장된 메모 {disabledCount}건이 있습니다.
+
+
+ )}