27 task / 6 phase. Phase 1 (Linux 빌드 risk-reduction first) → Phase 2 (설정 페이지 + IPC) → Phase 3 (트레이 슬림) → Phase 4 (F14 dock fix) → Phase 5 (F12 deeper fix) → Phase 6 (cleanup + version bump). 각 task TDD red→green→typecheck→commit 순서. spec coverage / placeholder / type consistency self-review 통과. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
76 KiB
v0.2.7 Cross-Platform 입구 정상화 Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Windows 트레이 의존을 끊고 macOS/Linux 사용자에게 동등한 입구 제공 — Linux 앱 빌드 + 설정 페이지 + 트레이 슬림 + macOS dock 클릭 fix + 자동 실행 진단 노출.
Architecture: electron-builder 에 linux target (AppImage + deb) 추가하여 빌드 매트릭스 확장. inbox 윈도우 안에 showSettings boolean state 기반 설정 페이지 추가, 4 섹션 (AI 제공자 / 자동 실행 / 백업/복원 / 정보) 으로 트레이 메뉴 8 항목 흡수. 트레이는 4 항목 (한 줄 적기 / 보관함 / 설정 / 종료) 으로 슬림. macOS dock 클릭은 getInboxWindow().show() + focus() 분기 추가로 hidden 창 복원. 자동 실행은 main process IPC settings:autostart-state 가 withArgs/noArgs/execPath/registry 정보 수집 → 설정 페이지 진단 패널 표시.
Tech Stack: Electron 41 + React 19 + zustand 5 + better-sqlite3 12.9 + electron-builder + vitest 4 + Playwright
선행 spec: docs/superpowers/specs/2026-05-06-v027-cross-platform-design.md
File Structure
신규 파일
| 경로 | 책임 |
|---|---|
src/renderer/inbox/components/SettingsPage.tsx |
설정 페이지 컨테이너 (4 섹션 vertical scroll + "← 돌아가기" 헤더) |
src/renderer/inbox/components/settings/AiProviderSection.tsx |
Ollama endpoint/model + "지금 재확인" + 마지막 ping 결과 (OllamaSettingsModal 흡수) |
src/renderer/inbox/components/settings/AutostartSection.tsx |
토글 + 진단 패널 (펼치기) + "재등록" 버튼 |
src/renderer/inbox/components/settings/BackupSection.tsx |
5 버튼 (백업/내보내기/복원/동기화/telemetry) |
src/renderer/inbox/components/settings/InfoSection.tsx |
버전 + 데이터 위치 + "데이터 위치 열기" + "정보 복사" |
src/main/services/AutostartDiagnostic.ts |
autostart-state 정보 수집 (getLoginItemSettings 양쪽 + Windows registry 조회) |
src/main/ipc/settingsApi.ts |
settings:autostart-state, settings:autostart-set, inbox:navigate, settings:* (백업/복원/내보내기/동기화/telemetry) IPC 핸들러 |
위 항목들의 .test.ts(x) |
단위 테스트 (vitest) |
수정 파일
| 경로 | 변경 내용 |
|---|---|
package.json |
build.linux target (AppImage + deb x64) + dist:linux script 추가 |
src/main/index.ts |
F14 activate 핸들러 5줄 수정, settings IPC 등록, 트레이 callback wiring 슬림화, HealthChecker/AiWorker refreshTray 호출 제거 |
src/main/tray.ts |
TrayCallbacks (showInbox/showCapture/showSettings 만), TrayState (todayCount 만), buildMenu 4 항목 |
src/renderer/inbox/store.ts |
showSettings: boolean + setShowSettings(b) action 추가 |
src/renderer/inbox/App.tsx |
showSettings 분기 + 헤더 톱니바퀴 아이콘 + 트레이 IPC inbox:navigate 구독 |
src/renderer/inbox/api.ts |
settings:* IPC 호출 wrapper |
src/preload/index.ts |
신규 IPC 채널 노출 (settings:autostart-state, settings:autostart-set, inbox:navigate, settings:backup/export/import/sync/exportTelemetry) |
제거 파일
| 경로 | 이유 |
|---|---|
src/renderer/inbox/components/OllamaSettingsModal.tsx |
AiProviderSection 으로 흡수 후 dead code |
Phase 개요
Phase 1: Linux 빌드 (Task 1~5) ← Risk-reduction first
Phase 2: 설정 페이지 + IPC (Task 6~13)
Phase 3: 트레이 슬림 (Task 14~17)
Phase 4: F14 dock fix (Task 18)
Phase 5: F12 deeper fix (Task 19~24)
Phase 6: Cleanup + verification (Task 25~27)
각 task 끝마다 commit. 단위 테스트는 vitest, e2e 는 Playwright. typecheck (npm run typecheck) 는 task 별로 step 으로 포함.
Phase 1: Linux 빌드
Task 1: better-sqlite3 linux-x64 prebuild 가용성 검증
Files:
-
검증 only — 코드 변경 없음. 결과를
docs/superpowers/specs/2026-05-06-v027-cross-platform-design.md§11 Risk 표 끝에 기록. -
Step 1: macOS 호스트에서 brew 도구 설치
brew install dpkg fakeroot
확인: which dpkg-deb 와 which fakeroot 둘 다 경로 출력.
- Step 2: better-sqlite3 prebuild 가용성 직접 조회
curl -sI https://github.com/WiseLibs/better-sqlite3/releases/download/v12.9.0/better-sqlite3-v12.9.0-electron-v41.3.0-linux-x64.tar.gz | head -1
Expected: HTTP/2 302 (또는 200) — prebuild 존재. 404 면 node-gyp 로컬 빌드 fallback 필요 (Step 3 에서 확인).
- Step 3: 로컬 prebuild 시도 (linux 타깃)
cd node_modules/better-sqlite3
./node_modules/.bin/prebuild-install --runtime=electron --target=41.3.0 --platform=linux --arch=x64 --tag-prefix=v --verbose
Expected: Successfully installed prebuilt binary 메시지 + build/Release/better_sqlite3.node 파일 존재.
실패 시: error 로그를 그대로 spec §11 끝에 기록하고 Task 1 종료. node-gyp fallback 은 Task 3 에서 dist:linux 시 자동 시도됨.
- Step 4: spec §11 Risk 표 갱신 (검증 결과 기록)
docs/superpowers/specs/2026-05-06-v027-cross-platform-design.md 의 §11 표 첫 row 옆에 ✅ 또는 ⚠️ 마킹 + 한 줄 요약.
- Step 5: 커밋
git add docs/superpowers/specs/2026-05-06-v027-cross-platform-design.md
git commit -m "docs(v027): better-sqlite3 linux-x64 prebuild 가용성 검증"
Task 2: package.json — linux target + dist:linux script 추가
Files:
-
Modify:
package.json -
Step 1: package.json 의 build 블록 직전 위치 확인
grep -n '"build"' package.json | head -3
grep -n '"win"' package.json
grep -n '"mac"' package.json
Expected: "build" 키 안에 이미 "win": {...} 와 "mac": {...} 가 있는 구조. linux 는 그 형제로 추가.
- Step 2: linux target 추가
package.json 의 build 블록 안, mac 다음에 추가:
"linux": {
"target": [
{ "target": "AppImage", "arch": ["x64"] },
{ "target": "deb", "arch": ["x64"] }
],
"category": "Utility",
"synopsis": "로컬 메모 캡처 + AI 태그",
"description": "Inkling — 잠깐 스친 생각을 잡아두는 로컬-우선 메모 도구."
}
- Step 3: scripts 블록에 dist:linux 추가
package.json 의 scripts 블록 안 dist:mac 다음에 추가:
"predist:linux": "npm run rebuild:electron && npm run build",
"dist:linux": "electron-builder --linux --x64",
- Step 4: typecheck (json 파일이라 syntax 만 검증)
node -e "JSON.parse(require('fs').readFileSync('package.json','utf8'))"
Expected: 출력 없음 (parse 성공).
- Step 5: 커밋
git add package.json
git commit -m "feat(v027): electron-builder linux target (AppImage + deb x64)"
Task 3: macOS 호스트에서 dist:linux 빌드 실행
Files:
-
빌드 산출물 검증 only — 코드 변경 없음.
-
Step 1: 빌드 실행
npm run dist:linux
Expected (성공 시): dist/Inkling-0.2.7.AppImage + dist/inkling_0.2.7_amd64.deb 산출. 빌드 시간 약 1-3분.
- Step 2: 산출물 존재 확인
ls -la dist/*.AppImage dist/*.deb
Expected: 두 파일 모두 표시 + AppImage size 약 100-150MB, deb size 약 80-120MB.
- Step 3: AppImage 실행 권한 확인 + 헤더 검증
file dist/Inkling-0.2.7.AppImage
Expected: ELF 64-bit LSB executable.
- Step 4: deb 메타데이터 확인
dpkg-deb -I dist/inkling_0.2.7_amd64.deb
Expected: Architecture: amd64, Package: inkling, version 0.2.7.
- Step 5: 빌드 실패 시 fallback 결정
Step 1 실패 시: error 메시지를 spec §11 의 첫 row 에 추가 + 이 task 를 Docker fallback (docker run --rm -v $(pwd):/project electronuserland/builder npm run dist:linux) 으로 재시도. 그래도 실패면 v0.2.7 scope 조정 — AppImage 만 (deb 제거) 또는 v0.2.8 로 deb 미루기.
- Step 6: 커밋 (빌드 산출물은 커밋 X —
.gitignoredist/ 확인)
grep -E '^dist/?$' .gitignore || echo "WARNING: dist not in .gitignore"
git status --short # 변경 없어야 함
빌드 검증 자체는 커밋 대상 아님. 다만 spec 변경이 있다면:
git add docs/superpowers/specs/2026-05-06-v027-cross-platform-design.md
git commit -m "docs(v027): dist:linux 빌드 검증 — fallback 결정 기록" || echo "no spec change"
Task 4: AppImage Linux VM smoke test
Files:
-
수동 검증 — 코드 변경 없음.
-
Step 1: Linux VM 준비
WSL2 Ubuntu 22.04 또는 별도 VM 사용. AppImage 를 VM 안으로 복사 (scp 또는 mount).
# VM 안에서:
chmod +x ~/Inkling-0.2.7.AppImage
sudo apt-get install -y libfuse2 # AppImage 실행 의존성
- Step 2: AppImage 실행
~/Inkling-0.2.7.AppImage --no-sandbox 2>&1 | head -30
Expected: Inkling 윈도우 등장 + 콘솔에 migration 로그 없거나 applied m003 정상 출력. 에러 (예: dlopen: undefined symbol) 시 — better-sqlite3 ABI 이슈 → spec §11 첫 row 에 기록 + Task 3 Step 5 fallback 진입.
- Step 3: 마이그레이션 + 캡처 1회
inbox 윈도우 → "한 줄 적기" → 임의 텍스트 입력 → 저장. 캡처된 노트가 inbox 에 표시되는지 확인.
- Step 4: Ollama 연결 시도
설정이 아직 없으니 INKLING_OLLAMA_ENDPOINT=http://192.168.0.47:11434 env 로 실행 (LAN 서버 - memory 의 dogfood 환경):
INKLING_OLLAMA_ENDPOINT=http://192.168.0.47:11434 ~/Inkling-0.2.7.AppImage --no-sandbox
기존 캡처 노트의 ai_status 가 pending → complete 로 전이되며 tag 가 표시되는지 확인.
- Step 5: recall 또는 trash 한 사이클
inbox 에서 노트 휴지통 → 휴지통 탭 → restore. 동작 정상이면 SQLite write 검증 통과.
- Step 6: 결과 spec §11 갱신 + 커밋
성공/실패 + 발견된 이슈를 spec §11 끝 "Linux smoke test 결과" 섹션 (신규) 에 기록.
git add docs/superpowers/specs/2026-05-06-v027-cross-platform-design.md
git commit -m "docs(v027): Linux AppImage smoke test 결과 기록"
Task 5: deb Ubuntu VM smoke test
Files:
-
수동 검증 — 코드 변경 없음.
-
Step 1: Ubuntu VM 에 deb 설치
sudo dpkg -i ~/inkling_0.2.7_amd64.deb
sudo apt-get install -f # 의존성 자동 해결
Expected: 설치 성공 + /usr/bin/inkling 존재. desktop entry (/usr/share/applications/inkling.desktop) 확인.
- Step 2: 데스크탑에서 launcher 통해 실행
GUI 가능한 VM 이면 application launcher 에서 "Inkling" 검색 → 클릭. 또는 터미널:
inkling &
- Step 3: AppImage 와 동일한 검증 (Task 4 Step 3~5 반복)
마이그레이션 / 캡처 / Ollama / restore 한 사이클.
- Step 4: 제거 가능 여부 확인
sudo dpkg -r inkling
ls /usr/bin/inkling # 없어야 함
데이터는 사용자 홈 (~/.config/Inkling) 에 잔류 — 의도된 동작.
- Step 5: 결과 spec §11 갱신 + 커밋
git add docs/superpowers/specs/2026-05-06-v027-cross-platform-design.md
git commit -m "docs(v027): Linux deb smoke test 결과 기록"
Phase 1 종료. 만약 Task 1~5 중 어디든 실패면 spec §11 fallback 적용 + Phase 2 진입 보류 결정.
Phase 2: 설정 페이지 + IPC
Task 6: store.ts — showSettings boolean state + action
Files:
-
Modify:
src/renderer/inbox/store.ts -
Test:
src/renderer/inbox/store.test.ts(없으면 신규 — 다만 기존 store 테스트가 컴포넌트 테스트 안에서 간접 검증되는 패턴이면 SettingsPage 테스트 안에서 함께 검증해도 OK) -
Step 1: store 테스트 신규 작성
// src/renderer/inbox/store.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { useInbox } from './store';
describe('inbox store — showSettings', () => {
beforeEach(() => {
useInbox.setState({ showSettings: false });
});
it('initial state has showSettings=false', () => {
expect(useInbox.getState().showSettings).toBe(false);
});
it('setShowSettings(true) sets state', () => {
useInbox.getState().setShowSettings(true);
expect(useInbox.getState().showSettings).toBe(true);
});
it('setShowSettings(false) toggles back', () => {
useInbox.getState().setShowSettings(true);
useInbox.getState().setShowSettings(false);
expect(useInbox.getState().showSettings).toBe(false);
});
});
- Step 2: 테스트 실행 → fail 확인
npm run rebuild:node && npx vitest run src/renderer/inbox/store.test.ts
Expected: 3 tests fail with setShowSettings is not a function.
- Step 3: store.ts 에 state + action 추가
InboxState 인터페이스에:
showSettings: boolean;
setShowSettings: (open: boolean) => void;
create<InboxState> 안 초기 state:
showSettings: false,
action:
setShowSettings(open) {
set({ showSettings: open });
},
- Step 4: 테스트 통과 확인
npx vitest run src/renderer/inbox/store.test.ts
Expected: 3 tests pass.
- Step 5: typecheck
npm run typecheck
Expected: 0 errors.
- Step 6: 커밋
git add src/renderer/inbox/store.ts src/renderer/inbox/store.test.ts
git commit -m "feat(v027): inbox store 에 showSettings state + setShowSettings action"
Task 7: SettingsPage.tsx scaffold + 빈 4 섹션 placeholder
Files:
-
Create:
src/renderer/inbox/components/SettingsPage.tsx -
Create:
src/renderer/inbox/components/SettingsPage.test.tsx -
Step 1: failing test 작성
// src/renderer/inbox/components/SettingsPage.test.tsx
import { describe, it, expect, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { SettingsPage } from './SettingsPage';
import { useInbox } from '../store';
describe('SettingsPage', () => {
beforeEach(() => {
useInbox.setState({ showSettings: true });
});
it('renders header with "← 돌아가기" button', () => {
render(<SettingsPage />);
expect(screen.getByRole('button', { name: /돌아가기/ })).toBeInTheDocument();
});
it('renders 4 section headings', () => {
render(<SettingsPage />);
expect(screen.getByText('AI 제공자')).toBeInTheDocument();
expect(screen.getByText('자동 실행')).toBeInTheDocument();
expect(screen.getByText('백업 / 복원')).toBeInTheDocument();
expect(screen.getByText('정보')).toBeInTheDocument();
});
it('clicking "← 돌아가기" sets showSettings to false', () => {
render(<SettingsPage />);
fireEvent.click(screen.getByRole('button', { name: /돌아가기/ }));
expect(useInbox.getState().showSettings).toBe(false);
});
});
- Step 2: 테스트 실행 → fail
npx vitest run src/renderer/inbox/components/SettingsPage.test.tsx
Expected: import error (SettingsPage 미존재).
- Step 3: SettingsPage.tsx 작성 (4 섹션 placeholder + 헤더)
// src/renderer/inbox/components/SettingsPage.tsx
import React from 'react';
import { useInbox } from '../store';
export function SettingsPage(): React.ReactElement {
const setShowSettings = useInbox((s) => s.setShowSettings);
return (
<div style={{ padding: 16, maxWidth: 720, margin: '0 auto' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16 }}>
<button
onClick={() => setShowSettings(false)}
style={{
background: 'transparent',
border: 'none',
fontSize: 14,
cursor: 'pointer',
color: '#0a4b80'
}}
>
← 돌아가기
</button>
<h1 style={{ fontSize: 18, margin: 0 }}>설정</h1>
</div>
<section style={{ marginBottom: 24 }}>
<h2 style={{ fontSize: 14, marginBottom: 8 }}>AI 제공자</h2>
{/* AiProviderSection — Task 8 */}
</section>
<section style={{ marginBottom: 24 }}>
<h2 style={{ fontSize: 14, marginBottom: 8 }}>자동 실행</h2>
{/* AutostartSection — Task 9 + Task 23/24 */}
</section>
<section style={{ marginBottom: 24 }}>
<h2 style={{ fontSize: 14, marginBottom: 8 }}>백업 / 복원</h2>
{/* BackupSection — Task 10 */}
</section>
<section style={{ marginBottom: 24 }}>
<h2 style={{ fontSize: 14, marginBottom: 8 }}>정보</h2>
{/* InfoSection — Task 11 */}
</section>
</div>
);
}
- Step 4: 테스트 통과 확인
npx vitest run src/renderer/inbox/components/SettingsPage.test.tsx
Expected: 3 tests pass.
- Step 5: typecheck
npm run typecheck
- Step 6: 커밋
git add src/renderer/inbox/components/SettingsPage.tsx src/renderer/inbox/components/SettingsPage.test.tsx
git commit -m "feat(v027): SettingsPage scaffold — 4 섹션 placeholder + 돌아가기"
Task 8: AiProviderSection.tsx — OllamaSettingsModal 흡수
Files:
- Create:
src/renderer/inbox/components/settings/AiProviderSection.tsx - Create:
src/renderer/inbox/components/settings/AiProviderSection.test.tsx - Modify:
src/renderer/inbox/components/SettingsPage.tsx(placeholder → 실 import)
선행 참조: src/renderer/inbox/components/OllamaSettingsModal.tsx 의 endpoint zod 검증 + model 입력 + 저장 로직을 그대로 흡수. 차이점: modal frame (overlay) 제거 + section 형태 + "지금 재확인" 버튼 추가.
- Step 1: failing test 작성
// src/renderer/inbox/components/settings/AiProviderSection.test.tsx
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { AiProviderSection } from './AiProviderSection';
vi.mock('../../api', () => ({
inboxApi: {
getOllamaSettings: vi.fn(async () => ({ endpoint: 'http://localhost:11434', model: 'gemma2:2b' })),
saveOllamaSettings: vi.fn(async () => ({ ok: true })),
ollamaRecheck: vi.fn(async () => ({ ok: true }))
}
}));
describe('AiProviderSection', () => {
beforeEach(() => { vi.clearAllMocks(); });
it('loads current settings on mount', async () => {
render(<AiProviderSection />);
expect(await screen.findByDisplayValue('http://localhost:11434')).toBeInTheDocument();
expect(screen.getByDisplayValue('gemma2:2b')).toBeInTheDocument();
});
it('rejects invalid endpoint URL', async () => {
render(<AiProviderSection />);
await screen.findByDisplayValue('http://localhost:11434');
const input = screen.getByLabelText(/Endpoint/);
fireEvent.change(input, { target: { value: 'not-a-url' } });
fireEvent.click(screen.getByRole('button', { name: /저장/ }));
expect(await screen.findByText(/올바른 URL/)).toBeInTheDocument();
});
it('"지금 재확인" calls ollamaRecheck and shows result', async () => {
const { inboxApi } = await import('../../api');
render(<AiProviderSection />);
await screen.findByDisplayValue('http://localhost:11434');
fireEvent.click(screen.getByRole('button', { name: /지금 재확인/ }));
expect(inboxApi.ollamaRecheck).toHaveBeenCalled();
});
});
- Step 2: 테스트 실행 → fail
npx vitest run src/renderer/inbox/components/settings/AiProviderSection.test.tsx
Expected: import error.
- Step 3: AiProviderSection.tsx 작성
기존 OllamaSettingsModal.tsx 의 form 로직 (endpoint zod safeParse, model trim, save) 흡수 + modal overlay 제거. 추가 기능: "지금 재확인" 버튼 → inboxApi.ollamaRecheck() 결과 inline 표시.
// src/renderer/inbox/components/settings/AiProviderSection.tsx
import React, { useEffect, useState } from 'react';
import { z } from 'zod';
import { inboxApi } from '../../api';
const endpointSchema = z.string().url();
export function AiProviderSection(): React.ReactElement {
const [endpoint, setEndpoint] = useState('');
const [model, setModel] = useState('');
const [error, setError] = useState<string | null>(null);
const [saveResult, setSaveResult] = useState<string | null>(null);
const [recheckResult, setRecheckResult] = useState<string | null>(null);
useEffect(() => {
void (async () => {
const s = await inboxApi.getOllamaSettings();
setEndpoint(s.endpoint);
setModel(s.model);
})();
}, []);
async function onSave(): Promise<void> {
const r = endpointSchema.safeParse(endpoint);
if (!r.success) { setError('올바른 URL 형식이 아닙니다'); return; }
if (model.trim() === '') { setError('모델 이름을 입력해주세요'); return; }
setError(null);
const result = await inboxApi.saveOllamaSettings({ endpoint, model });
setSaveResult(result.ok ? '저장됨' : '저장 실패');
}
async function onRecheck(): Promise<void> {
setRecheckResult('확인 중...');
const r = await inboxApi.ollamaRecheck();
setRecheckResult(r.ok ? '✅ 연결됨' : `⚠️ ${r.reason ?? '연결 실패'}`);
}
return (
<div>
<label style={{ display: 'block', marginBottom: 8 }}>
Endpoint
<input
type="text"
value={endpoint}
onChange={(e) => setEndpoint(e.target.value)}
style={{ display: 'block', width: '100%', padding: 4, marginTop: 2 }}
/>
</label>
<label style={{ display: 'block', marginBottom: 8 }}>
Model
<input
type="text"
value={model}
onChange={(e) => setModel(e.target.value)}
style={{ display: 'block', width: '100%', padding: 4, marginTop: 2 }}
/>
</label>
{error && <div style={{ color: '#c33', fontSize: 12, marginBottom: 8 }}>{error}</div>}
{saveResult && <div style={{ fontSize: 12, marginBottom: 8 }}>{saveResult}</div>}
<div style={{ display: 'flex', gap: 8 }}>
<button onClick={onSave}>저장</button>
<button onClick={onRecheck}>지금 재확인</button>
</div>
{recheckResult && <div style={{ fontSize: 12, marginTop: 8 }}>{recheckResult}</div>}
</div>
);
}
- Step 4: SettingsPage.tsx 의 AI 섹션 placeholder 를 실 import 로 교체
{/* AiProviderSection — Task 8 */} 를 <AiProviderSection /> 로 교체. import 추가.
- Step 5: 테스트 + typecheck
npx vitest run src/renderer/inbox/components/settings/AiProviderSection.test.tsx
npm run typecheck
Expected: 3 tests pass + 0 typecheck errors.
inboxApi.getOllamaSettings / saveOllamaSettings 가 미존재면 추가 필요. 기존 modal 이 호출하던 IPC channel 을 확인해 같은 채널 사용. 채널이 없으면 src/renderer/inbox/api.ts + src/main/ipc/inboxApi.ts 에 추가 (별도 step 으로 분리하지 않고 같은 task 안에서).
- Step 6: 커밋
git add src/renderer/inbox/components/settings/AiProviderSection.tsx src/renderer/inbox/components/settings/AiProviderSection.test.tsx src/renderer/inbox/components/SettingsPage.tsx src/renderer/inbox/api.ts src/main/ipc/inboxApi.ts
git commit -m "feat(v027): AiProviderSection — OllamaSettingsModal 흡수 + 지금 재확인"
Task 9: AutostartSection.tsx — 토글만 (진단 패널은 Task 23 에서)
Files:
-
Create:
src/renderer/inbox/components/settings/AutostartSection.tsx -
Create:
src/renderer/inbox/components/settings/AutostartSection.test.tsx -
Modify:
src/renderer/inbox/components/SettingsPage.tsx -
Modify:
src/renderer/inbox/api.ts(autostart get/set wrapper) -
Modify:
src/preload/index.ts(채널 expose) -
Step 1: failing test 작성
// src/renderer/inbox/components/settings/AutostartSection.test.tsx
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { AutostartSection } from './AutostartSection';
vi.mock('../../api', () => ({
inboxApi: {
getAutostart: vi.fn(async () => ({ openAtLogin: true })),
setAutostart: vi.fn(async (open: boolean) => ({ openAtLogin: open }))
}
}));
describe('AutostartSection', () => {
beforeEach(() => { vi.clearAllMocks(); });
it('renders toggle reflecting current state', async () => {
render(<AutostartSection />);
const toggle = await screen.findByRole('checkbox');
expect(toggle).toBeChecked();
});
it('clicking toggle calls setAutostart', async () => {
const { inboxApi } = await import('../../api');
render(<AutostartSection />);
const toggle = await screen.findByRole('checkbox');
fireEvent.click(toggle);
await waitFor(() => expect(inboxApi.setAutostart).toHaveBeenCalledWith(false));
});
});
- Step 2: 테스트 실행 → fail
npx vitest run src/renderer/inbox/components/settings/AutostartSection.test.tsx
- Step 3: AutostartSection.tsx 작성
// src/renderer/inbox/components/settings/AutostartSection.tsx
import React, { useEffect, useState } from 'react';
import { inboxApi } from '../../api';
export function AutostartSection(): React.ReactElement {
const [openAtLogin, setOpenAtLogin] = useState<boolean | null>(null);
useEffect(() => {
void (async () => {
const s = await inboxApi.getAutostart();
setOpenAtLogin(s.openAtLogin);
})();
}, []);
async function onToggle(e: React.ChangeEvent<HTMLInputElement>): Promise<void> {
const next = e.target.checked;
const r = await inboxApi.setAutostart(next);
setOpenAtLogin(r.openAtLogin);
}
if (openAtLogin === null) return <div style={{ fontSize: 12 }}>로딩 중...</div>;
return (
<div>
<label style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<input type="checkbox" checked={openAtLogin} onChange={onToggle} />
앱 시작 시 자동으로 실행
</label>
</div>
);
}
- Step 4: api.ts 에 wrapper 추가
// src/renderer/inbox/api.ts 의 inboxApi 객체에 추가:
async getAutostart() {
return await window.inkling.invoke('settings:get-autostart');
},
async setAutostart(open: boolean) {
return await window.inkling.invoke('settings:set-autostart', open);
},
- Step 5: preload + main IPC handler 추가
src/preload/index.ts 의 invoke 화이트리스트에 'settings:get-autostart', 'settings:set-autostart' 추가. main 측 IPC 는 Task 22 에서 본격 구현 — 여기선 임시 stub:
// src/main/ipc/settingsApi.ts (NEW or 기존 파일에 append)
import electron from 'electron';
const { ipcMain, app } = electron;
export function registerSettingsApi(): void {
ipcMain.handle('settings:get-autostart', () => {
const r = app.getLoginItemSettings({ args: ['--hidden'] });
return { openAtLogin: r.openAtLogin };
});
ipcMain.handle('settings:set-autostart', (_e, open: boolean) => {
app.setLoginItemSettings({ openAtLogin: open, args: ['--hidden'] });
const r = app.getLoginItemSettings({ args: ['--hidden'] });
return { openAtLogin: r.openAtLogin };
});
}
src/main/index.ts whenReady 안에 registerSettingsApi() 호출 추가.
- Step 6: SettingsPage placeholder 교체 + 테스트 + 커밋
npx vitest run src/renderer/inbox/components/settings/AutostartSection.test.tsx
npm run typecheck
git add -A src/renderer/inbox/ src/main/ipc/settingsApi.ts src/main/index.ts src/preload/
git commit -m "feat(v027): AutostartSection 토글 (진단 패널은 후속 task)"
Task 10: BackupSection.tsx — 5 버튼
Files:
- Create:
src/renderer/inbox/components/settings/BackupSection.tsx - Create:
src/renderer/inbox/components/settings/BackupSection.test.tsx - Modify:
src/renderer/inbox/components/SettingsPage.tsx - Modify:
src/renderer/inbox/api.ts - Modify:
src/main/ipc/settingsApi.ts - Modify:
src/preload/index.ts
선행 참조: src/main/index.ts 의 트레이 callback (runBackup, runExport, runImport, runSync, runExportTelemetry) 5개를 IPC 핸들러로 전환. 핸들러 본문 = 기존 callback 본문 그대로 이동.
- Step 1: failing test
// src/renderer/inbox/components/settings/BackupSection.test.tsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { BackupSection } from './BackupSection';
vi.mock('../../api', () => ({
inboxApi: {
runBackup: vi.fn(async () => ({ ok: true })),
runExport: vi.fn(async () => ({ ok: true })),
runImport: vi.fn(async () => ({ ok: true })),
runSync: vi.fn(async () => ({ ok: true })),
runExportTelemetry: vi.fn(async () => ({ ok: true }))
}
}));
describe('BackupSection', () => {
it('renders 5 buttons', () => {
render(<BackupSection />);
expect(screen.getByRole('button', { name: /지금 백업/ })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /내보내기/ })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /백업에서 복원/ })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /지금 동기화/ })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /사용 로그/ })).toBeInTheDocument();
});
it('each button triggers corresponding api call', async () => {
const { inboxApi } = await import('../../api');
render(<BackupSection />);
fireEvent.click(screen.getByRole('button', { name: /지금 백업/ }));
expect(inboxApi.runBackup).toHaveBeenCalled();
});
});
- Step 2: 테스트 실행 → fail
npx vitest run src/renderer/inbox/components/settings/BackupSection.test.tsx
- Step 3: BackupSection.tsx 작성
// src/renderer/inbox/components/settings/BackupSection.tsx
import React, { useState } from 'react';
import { inboxApi } from '../../api';
export function BackupSection(): React.ReactElement {
const [status, setStatus] = useState<string | null>(null);
async function run(label: string, fn: () => Promise<unknown>): Promise<void> {
setStatus(`${label}: 진행 중...`);
try {
await fn();
setStatus(`${label}: 완료`);
} catch (e) {
setStatus(`${label}: 실패 — ${(e as Error).message}`);
}
}
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<button onClick={() => run('지금 백업', () => inboxApi.runBackup())}>지금 백업</button>
<button onClick={() => run('내보내기', () => inboxApi.runExport())}>내보내기...</button>
<button onClick={() => run('백업에서 복원', () => inboxApi.runImport())}>백업에서 복원...</button>
<button onClick={() => run('지금 동기화', () => inboxApi.runSync())}>지금 동기화</button>
<button onClick={() => run('사용 로그 내보내기', () => inboxApi.runExportTelemetry())}>사용 로그 내보내기...</button>
{status && <div style={{ fontSize: 12 }}>{status}</div>}
</div>
);
}
- Step 4: IPC 핸들러 5개를 settingsApi.ts 에 추가
src/main/ipc/settingsApi.ts 의 registerSettingsApi 안에:
ipcMain.handle('settings:run-backup', async () => {
// 기존 src/main/index.ts 의 runBackup callback 본문 이동
return await BackupService.run();
});
ipcMain.handle('settings:run-export', async () => {
return await BackupService.export();
});
ipcMain.handle('settings:run-import', async () => {
return await BackupService.import();
});
ipcMain.handle('settings:run-sync', async () => {
return await BackupService.sync();
});
ipcMain.handle('settings:run-export-telemetry', async () => {
return await TelemetryService.export();
});
실제 BackupService / TelemetryService import 와 시그니처는 src/main/index.ts 의 기존 callback 본문 그대로 복사. 시그니처가 callback 안에 inline 으로 짜여있다면 그 inline 코드를 핸들러 본문으로 이동. src/main/index.ts 의 runBackup 등 ref 는 Task 17 에서 정리.
- Step 5: api.ts wrapper + preload 채널 + SettingsPage 교체 + 테스트
// src/renderer/inbox/api.ts inboxApi 객체에 추가:
async runBackup() { return await window.inkling.invoke('settings:run-backup'); },
async runExport() { return await window.inkling.invoke('settings:run-export'); },
async runImport() { return await window.inkling.invoke('settings:run-import'); },
async runSync() { return await window.inkling.invoke('settings:run-sync'); },
async runExportTelemetry() { return await window.inkling.invoke('settings:run-export-telemetry'); },
src/preload/index.ts 화이트리스트에 5채널 추가. SettingsPage.tsx 의 placeholder 교체.
npx vitest run src/renderer/inbox/components/settings/BackupSection.test.tsx
npm run typecheck
- Step 6: 커밋
git add -A src/renderer/inbox/ src/main/ipc/settingsApi.ts src/preload/
git commit -m "feat(v027): BackupSection — 5 버튼 + IPC 핸들러"
Task 11: InfoSection.tsx — 버전 정보 + 데이터 위치
Files:
- Create:
src/renderer/inbox/components/settings/InfoSection.tsx - Create:
src/renderer/inbox/components/settings/InfoSection.test.tsx - Modify:
src/renderer/inbox/components/SettingsPage.tsx - Modify:
src/renderer/inbox/api.ts - Modify:
src/main/ipc/settingsApi.ts - Modify:
src/preload/index.ts
선행 참조: src/main/tray.ts 의 showAboutDialog 함수의 detail 문자열 + clipboard.writeText / shell.openPath 로직을 IPC 핸들러로 추출.
- Step 1: failing test
// src/renderer/inbox/components/settings/InfoSection.test.tsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { InfoSection } from './InfoSection';
vi.mock('../../api', () => ({
inboxApi: {
getAppInfo: vi.fn(async () => ({
version: '0.2.7',
electron: '41.3.0',
node: '22.x',
os: 'darwin 23.6.0',
profileDir: '/Users/u/Library/Application Support/Inkling'
})),
openProfileDir: vi.fn(async () => undefined),
copyAppInfo: vi.fn(async () => undefined)
}
}));
describe('InfoSection', () => {
it('renders version, electron, node, OS, profileDir', async () => {
render(<InfoSection />);
expect(await screen.findByText(/0\.2\.7/)).toBeInTheDocument();
expect(screen.getByText(/41\.3\.0/)).toBeInTheDocument();
expect(screen.getByText(/22\.x/)).toBeInTheDocument();
expect(screen.getByText(/darwin/)).toBeInTheDocument();
expect(screen.getByText(/Library\/Application Support\/Inkling/)).toBeInTheDocument();
});
it('"데이터 위치 열기" calls openProfileDir', async () => {
const { inboxApi } = await import('../../api');
render(<InfoSection />);
await screen.findByText(/0\.2\.7/);
fireEvent.click(screen.getByRole('button', { name: /데이터 위치 열기/ }));
expect(inboxApi.openProfileDir).toHaveBeenCalled();
});
it('"정보 복사" calls copyAppInfo', async () => {
const { inboxApi } = await import('../../api');
render(<InfoSection />);
await screen.findByText(/0\.2\.7/);
fireEvent.click(screen.getByRole('button', { name: /정보 복사/ }));
expect(inboxApi.copyAppInfo).toHaveBeenCalled();
});
});
-
Step 2: 테스트 실행 → fail
-
Step 3: InfoSection.tsx 작성
// src/renderer/inbox/components/settings/InfoSection.tsx
import React, { useEffect, useState } from 'react';
import { inboxApi } from '../../api';
interface AppInfo {
version: string;
electron: string;
node: string;
os: string;
profileDir: string;
}
export function InfoSection(): React.ReactElement {
const [info, setInfo] = useState<AppInfo | null>(null);
useEffect(() => {
void (async () => {
setInfo(await inboxApi.getAppInfo());
})();
}, []);
if (!info) return <div style={{ fontSize: 12 }}>로딩 중...</div>;
return (
<div>
<dl style={{ fontSize: 12, lineHeight: 1.6 }}>
<dt style={{ fontWeight: 600 }}>버전</dt><dd>{info.version}</dd>
<dt style={{ fontWeight: 600 }}>Electron</dt><dd>{info.electron}</dd>
<dt style={{ fontWeight: 600 }}>Node</dt><dd>{info.node}</dd>
<dt style={{ fontWeight: 600 }}>OS</dt><dd>{info.os}</dd>
<dt style={{ fontWeight: 600 }}>데이터 위치</dt><dd style={{ wordBreak: 'break-all' }}>{info.profileDir}</dd>
</dl>
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
<button onClick={() => inboxApi.openProfileDir()}>데이터 위치 열기</button>
<button onClick={() => inboxApi.copyAppInfo()}>정보 복사</button>
</div>
</div>
);
}
- Step 4: IPC 핸들러 추가
src/main/ipc/settingsApi.ts 의 registerSettingsApi 안에:
import { platform, release, EOL } from 'node:os';
const { shell, clipboard } = electron;
ipcMain.handle('settings:get-app-info', () => {
return {
version: app.getVersion(),
electron: process.versions.electron ?? '?',
node: process.versions.node ?? '?',
os: `${platform()} ${release()}`,
profileDir: app.getPath('userData')
};
});
ipcMain.handle('settings:open-profile-dir', async () => {
await shell.openPath(app.getPath('userData'));
});
ipcMain.handle('settings:copy-app-info', () => {
const v = app.getVersion();
const detail = [
`버전: ${v}`,
`Electron: ${process.versions.electron}`,
`Node: ${process.versions.node}`,
`OS: ${platform()} ${release()}`,
`데이터 위치: ${app.getPath('userData')}`
].join(EOL);
clipboard.writeText(`Inkling ${v}${EOL}${detail}`);
});
- Step 5: api wrapper + preload + SettingsPage 교체 + 테스트
// src/renderer/inbox/api.ts:
async getAppInfo() { return await window.inkling.invoke('settings:get-app-info'); },
async openProfileDir() { return await window.inkling.invoke('settings:open-profile-dir'); },
async copyAppInfo() { return await window.inkling.invoke('settings:copy-app-info'); },
preload 화이트리스트 + SettingsPage placeholder 교체.
npx vitest run src/renderer/inbox/components/settings/InfoSection.test.tsx
npm run typecheck
- Step 6: 커밋
git add -A src/renderer/inbox/ src/main/ipc/settingsApi.ts src/preload/
git commit -m "feat(v027): InfoSection — 버전/데이터 위치/복사 + IPC"
Task 12: App.tsx — showSettings 분기 + 헤더 톱니바퀴
Files:
-
Modify:
src/renderer/inbox/App.tsx -
Modify:
src/renderer/inbox/App.test.tsx(없으면 신규) -
Step 1: failing test (showSettings=true 시 SettingsPage 렌더 + 톱니바퀴 클릭 시 setShowSettings(true))
// src/renderer/inbox/App.test.tsx (해당 부분 추가)
import { describe, it, expect, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { App } from './App';
import { useInbox } from './store';
describe('App — settings view', () => {
beforeEach(() => {
useInbox.setState({ showSettings: false, notes: [], trashNotes: [], trashCount: 0 });
});
it('renders SettingsPage when showSettings=true', () => {
useInbox.setState({ showSettings: true });
render(<App />);
expect(screen.getByText('설정')).toBeInTheDocument();
expect(screen.getByText('AI 제공자')).toBeInTheDocument();
});
it('header gear icon click sets showSettings=true', () => {
render(<App />);
fireEvent.click(screen.getByLabelText('설정 열기'));
expect(useInbox.getState().showSettings).toBe(true);
});
});
- Step 2: 테스트 실행 → fail
npx vitest run src/renderer/inbox/App.test.tsx
- Step 3: App.tsx 수정
src/renderer/inbox/App.tsx 의 return 직전:
const showSettings = useInbox((s) => s.showSettings);
const setShowSettings = useInbox((s) => s.setShowSettings);
if (showSettings) return <SettingsPage />;
SettingsPage import 추가. 기존 OllamaSettingsModal 관련 state (ollamaSettingsOpen) 는 Task 25 에서 제거.
헤더 우측 (ContinuityBadge / IdentityCounter 옆) 에 톱니바퀴 추가:
<button
aria-label="설정 열기"
onClick={() => setShowSettings(true)}
style={{
background: 'transparent',
border: 'none',
cursor: 'pointer',
padding: 4,
fontSize: 16
}}
>
⚙
</button>
- Step 4: 테스트 + typecheck
npx vitest run src/renderer/inbox/App.test.tsx
npm run typecheck
- Step 5: 수동 launch 검증
npm run rebuild:electron && npm run start
inbox 윈도우 → 헤더 톱니바퀴 클릭 → SettingsPage 등장 → "← 돌아가기" → inbox 복귀.
- Step 6: 커밋
git add src/renderer/inbox/App.tsx src/renderer/inbox/App.test.tsx
git commit -m "feat(v027): App.tsx 헤더 톱니바퀴 + showSettings 분기"
Task 13: IPC inbox:navigate — 트레이/외부에서 설정 진입
Files:
-
Modify:
src/main/ipc/settingsApi.ts(또는 별도 navigationApi) -
Modify:
src/preload/index.ts -
Modify:
src/renderer/inbox/api.ts -
Modify:
src/renderer/inbox/App.tsx(navigate 이벤트 구독) -
Step 1: failing test (renderer 측)
// src/renderer/inbox/App.test.tsx 에 추가:
it('inbox:navigate "settings" event sets showSettings=true', async () => {
// mock: api.onNavigate registers a listener that App calls
const navHandlers: Array<(view: string) => void> = [];
vi.mocked(inboxApi.onNavigate).mockImplementation((cb) => {
navHandlers.push(cb);
return () => { navHandlers.splice(navHandlers.indexOf(cb), 1); };
});
render(<App />);
navHandlers.forEach((h) => h('settings'));
await waitFor(() => expect(useInbox.getState().showSettings).toBe(true));
});
(상위 vi.mock 블록에 onNavigate: vi.fn(() => () => undefined) 추가)
-
Step 2: 테스트 실행 → fail (onNavigate 미존재)
-
Step 3: api.ts + preload 에 onNavigate 추가
// src/renderer/inbox/api.ts:
onNavigate(cb: (view: 'inbox' | 'trash' | 'settings') => void) {
return window.inkling.on('inbox:navigate', cb);
}
src/preload/index.ts 의 on whitelist 에 'inbox:navigate' 추가.
- Step 4: main 측 sender 함수
src/main/ipc/settingsApi.ts 의 export 에 추가:
import { getInboxWindow } from '../windows/inboxWindow';
export function navigateInbox(view: 'inbox' | 'trash' | 'settings'): void {
const win = getInboxWindow();
if (win && !win.isDestroyed()) {
if (!win.isVisible()) win.show();
win.focus();
win.webContents.send('inbox:navigate', view);
}
}
(트레이 "설정..." 클릭 시 호출 — Task 16 에서 wiring.)
- Step 5: App.tsx 에 navigate 이벤트 구독 추가
useEffect 안에:
const unsubNav = inboxApi.onNavigate((view) => {
if (view === 'settings') useInbox.getState().setShowSettings(true);
else if (view === 'inbox') useInbox.getState().setShowSettings(false);
});
// cleanup return 에 unsubNav() 추가
- Step 6: 테스트 + typecheck + 커밋
npx vitest run src/renderer/inbox/App.test.tsx
npm run typecheck
git add -A src/renderer/inbox/ src/main/ipc/ src/preload/
git commit -m "feat(v027): IPC inbox:navigate — 외부에서 설정 페이지 진입"
Phase 2 종료. 이 시점에 inbox 안에서 톱니바퀴 진입 + 4 섹션 (자동 실행은 토글만, 진단은 후속) 모두 동작.
Phase 3: 트레이 슬림
Task 14: TrayCallbacks 인터페이스 슬림 (showSettings 추가, 8개 제거)
Files:
-
Modify:
src/main/tray.ts -
Modify:
src/main/tray.test.ts(없으면 신규) -
Step 1: failing test (4 항목 검증 + 제거된 항목 부재 검증)
// src/main/tray.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
// electron mocked at top-level vitest setup OR per-test module mock
vi.mock('electron', () => ({
default: {
app: { on: vi.fn(), getPath: vi.fn(), getVersion: vi.fn(() => '0.2.7'), isPackaged: false, getLoginItemSettings: vi.fn(() => ({ openAtLogin: false })) },
Tray: vi.fn().mockImplementation(() => ({
setToolTip: vi.fn(),
setContextMenu: vi.fn(),
on: vi.fn()
})),
Menu: { buildFromTemplate: vi.fn((items) => ({ items })) },
nativeImage: { createEmpty: vi.fn() },
dialog: {},
shell: {},
clipboard: {}
}
}));
import { createTray, type TrayCallbacks } from './tray';
describe('tray menu — slim 4 items', () => {
beforeEach(() => { vi.clearAllMocks(); });
function makeCallbacks(): TrayCallbacks {
return {
showInbox: vi.fn(),
showCapture: vi.fn(),
showSettings: vi.fn()
};
}
it('builds menu with 4 click items + 2 separators', async () => {
createTray(makeCallbacks());
const electron = (await import('electron')).default;
const calls = (electron.Menu.buildFromTemplate as any).mock.calls;
const items = calls[calls.length - 1][0];
const labels = items.filter((i: any) => i.type !== 'separator').map((i: any) => i.label);
expect(labels).toEqual(['한 줄 적기', '보관한 메모 보기', '설정...', '종료']);
});
it('does not include removed items (백업/내보내기/Ollama 재확인 등)', async () => {
createTray(makeCallbacks());
const electron = (await import('electron')).default;
const calls = (electron.Menu.buildFromTemplate as any).mock.calls;
const items = calls[calls.length - 1][0];
const labels = items.filter((i: any) => i.type !== 'separator').map((i: any) => i.label);
expect(labels).not.toContain('지금 백업');
expect(labels).not.toContain('내보내기...');
expect(labels).not.toContain('Ollama 재확인');
expect(labels).not.toContain('지금 AI 처리');
expect(labels).not.toContain('Ollama 설정...');
});
it('"설정..." click invokes showSettings callback', async () => {
const cb = makeCallbacks();
createTray(cb);
const electron = (await import('electron')).default;
const calls = (electron.Menu.buildFromTemplate as any).mock.calls;
const items = calls[calls.length - 1][0];
const settingsItem = items.find((i: any) => i.label === '설정...');
settingsItem.click();
expect(cb.showSettings).toHaveBeenCalled();
});
});
- Step 2: 테스트 실행 → fail
npm run rebuild:node && npx vitest run src/main/tray.test.ts
Expected: TypeScript 또는 runtime fail (TrayCallbacks 가 아직 옛 인터페이스).
- Step 3: tray.ts 의 인터페이스 + state 슬림화
// src/main/tray.ts (인터페이스 부분만 교체)
export interface TrayCallbacks {
showInbox: () => void;
showCapture: () => void;
showSettings: () => void;
}
export interface TrayState {
todayCount: number;
}
let _state: TrayState = { todayCount: 0 };
기존 ollamaOk, failedCount 필드 + runBackup/runExport/runImport/runSync/runExportTelemetry/runOllamaRecheck/runRetryAllFailed/runOpenOllamaSettings callback 제거.
- Step 4: buildMenu 4 항목 본문
function buildMenu(): electron.Menu {
const items: MenuItemConstructorOptions[] = [];
const cb = _callbacks;
if (!cb) return Menu.buildFromTemplate([{ label: '로딩 중...', enabled: false }]);
if (_state.todayCount > 0) {
items.push({ label: `오늘 ${_state.todayCount}번 잡아둠`, enabled: false });
items.push({ type: 'separator' });
}
items.push({ label: '한 줄 적기', click: cb.showCapture });
items.push({ label: '보관한 메모 보기', click: cb.showInbox });
items.push({ type: 'separator' });
items.push({ label: '설정...', click: cb.showSettings });
items.push({ type: 'separator' });
items.push({ label: '종료', click: () => { app.isQuitting = true; app.quit(); } });
return Menu.buildFromTemplate(items);
}
기존 if (app.isPackaged) 의 자동 실행 checkbox 분기 + Inkling 정보 항목 + Ollama 재확인/AI 재처리/Ollama 설정 항목 모두 제거. showAboutDialog 함수도 제거 (Task 25 cleanup).
- Step 5: refreshTray signature 유지 (Partial) — todayCount 만 영향
export function refreshTray(state: Partial<TrayState>): void {
_state = { ..._state, ...state };
if (tray === null) return;
tray.setToolTip(`Inkling — 오늘 ${_state.todayCount}`);
tray.setContextMenu(buildMenu());
}
- Step 6: 테스트 + typecheck + 커밋
npx vitest run src/main/tray.test.ts
npm run typecheck
이 시점 typecheck 는 src/main/index.ts 의 createTray 호출부와 HealthChecker/AiWorker 의 refreshTray 호출부에서 type error 발생 — Task 17 에서 정리. Task 14 만 단독 commit:
git add src/main/tray.ts src/main/tray.test.ts
git commit -m "feat(v027): TrayCallbacks/TrayState 슬림 + buildMenu 4 항목"
Task 15: tray.ts — showAboutDialog / 자동실행 checkbox / Ollama 분기 코드 제거
Files:
- Modify:
src/main/tray.ts
이 task 는 Task 14 와 묶여 있지만, 분리 commit 으로 변경 의도 명확화.
- Step 1: showAboutDialog 함수 + 관련 import 제거
src/main/tray.ts 상단 import 에서 dialog, shell, clipboard, platform, release, EOL 제거 (이제 settings:get-app-info IPC 가 정보 dialog 역할).
showAboutDialog 함수 전체 삭제.
- Step 2: typecheck
npm run typecheck
이 시점에서 tray.ts 자체는 통과 — index.ts 호출부 에러는 Task 17 에서.
- Step 3: 커밋
git add src/main/tray.ts
git commit -m "feat(v027): tray.ts 의 showAboutDialog + 자동실행 분기 + 미사용 import 제거"
Task 16: index.ts — createTray callback wiring 갱신
Files:
-
Modify:
src/main/index.ts -
Step 1: 기존 트레이 callback wiring 위치 확인
grep -n 'createTray' src/main/index.ts
기존 호출부가 10-positional 이었던 시점부터 v0.2.6 의 객체 형태 (createTray({ showInbox, showCapture, runBackup, ... })) 까지 발전. 현재는 객체 형태.
- Step 2: createTray 호출을 3-callback 객체로 슬림화
기존:
createTray({
showInbox,
showCapture,
runBackup,
runExport,
runImport,
runSync,
runExportTelemetry,
runOllamaRecheck,
runRetryAllFailed,
runOpenOllamaSettings
});
변경 후:
import { navigateInbox } from './ipc/settingsApi';
createTray({
showInbox,
showCapture,
showSettings: () => navigateInbox('settings')
});
- Step 3: 미사용 callback 함수 제거
runBackup, runExport, runImport, runSync, runExportTelemetry, runOllamaRecheck, runRetryAllFailed, runOpenOllamaSettings 함수 정의 (또는 inline 람다) 가 index.ts 에 남아있다면 제거. 본문 로직은 이미 Task 10 (BackupSection) 의 IPC 핸들러로 이동됨.
- Step 4: refreshTray 호출부 슬림화
grep -n 'refreshTray' src/main/index.ts src/main/health/*.ts src/main/ai/*.ts 로 호출부 모두 찾기. refreshTray({ ollamaOk }) / refreshTray({ failedCount }) 호출은 모두 제거 (해당 메뉴 항목이 사라져 무의미). refreshTray({ todayCount }) 만 잔류.
- Step 5: typecheck + 단위 테스트 회귀
npm run typecheck
npx vitest run
Expected: 0 errors + 모든 단위 테스트 pass. tray.test.ts 에서 회귀 없는지 + HealthChecker/AiWorker 테스트가 refreshTray 모킹에 의존했다면 mock 갱신 필요.
- Step 6: 커밋
git add src/main/index.ts src/main/health/ src/main/ai/
git commit -m "feat(v027): createTray wiring 3-callback + refreshTray 호출부 슬림"
Task 17: 트레이 회귀 단위 테스트 + 수동 검증
Files:
-
검증 only.
-
Step 1: 전체 단위 테스트 + e2e 실행
npm run rebuild:node
npm test
npm run rebuild:electron
npm run test:e2e
Expected: 모두 pass. e2e 가 트레이 메뉴를 검증하지 않으면 회귀 risk 낮음.
- Step 2: 수동 launch 검증 (Win + macOS)
npm run start
트레이 우클릭 → 4 항목 (한 줄 적기 / 보관한 메모 보기 / 설정... / 종료) 만 표시. "설정..." 클릭 → inbox 윈도우 등장 + SettingsPage 표시.
- Step 3: 결과 spec §11 또는 별도 줄에 기록
문제 없으면 commit 없이 마무리. 회귀 발견 시 fix 후 commit.
Phase 3 종료.
Phase 4: F14 macOS dock 클릭 fix
Task 18: index.ts activate 핸들러 5줄 수정
Files:
- Modify:
src/main/index.ts:411-413
mocking 비용 높아 단위 테스트 X — manual macOS dogfood 검증.
- Step 1: 현재 activate 핸들러 위치 확인
grep -n "app.on('activate'" src/main/index.ts
Expected: 라인 ~411. 직접 라인 번호는 Task 16 변경 후 달라질 수 있음.
- Step 2: 핸들러 본문 교체
기존:
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createInboxWindow();
});
변경 후:
app.on('activate', () => {
const win = getInboxWindow();
if (win && !win.isDestroyed()) {
if (!win.isVisible()) win.show();
win.focus();
} else {
createInboxWindow();
}
});
getInboxWindow import 가 이미 있는지 확인 — 없으면 추가:
import { createInboxWindow, getInboxWindow } from './windows/inboxWindow';
- Step 3: typecheck
npm run typecheck
- Step 4: macOS 수동 검증
macOS 호스트에서:
npm run start
inbox 윈도우 → 빨간 신호등 (close) → dock 의 Inkling 아이콘 클릭 → inbox 윈도우 즉시 등장 + focus.
- Step 5: 검증 결과 dogfood-feedback.md F14 entry 갱신 (🚀 promoted 마킹) — 일괄 처리는 Task 27 에서
지금은 commit 만:
- Step 6: 커밋
git add src/main/index.ts
git commit -m "fix(v027): F14 — macOS dock 클릭 시 hidden inbox 창 show/focus"
Phase 4 종료.
Phase 5: F12 deeper fix — 자동 실행 진단 노출
Task 19: AutostartDiagnostic 서비스 — withArgs/noArgs/execPath 수집
Files:
-
Create:
src/main/services/AutostartDiagnostic.ts -
Create:
src/main/services/AutostartDiagnostic.test.ts -
Step 1: failing test
// src/main/services/AutostartDiagnostic.test.ts
import { describe, it, expect, vi } from 'vitest';
const mockApp = {
getLoginItemSettings: vi.fn()
};
vi.mock('electron', () => ({ default: { app: mockApp } }));
const mockExecFile = vi.fn();
vi.mock('node:child_process', () => ({ execFile: mockExecFile }));
import { collectAutostartState } from './AutostartDiagnostic';
describe('AutostartDiagnostic — collectAutostartState', () => {
it('returns withArgs / noArgs / execPath structure', async () => {
mockApp.getLoginItemSettings
.mockReturnValueOnce({ openAtLogin: true, executableWillLaunchAtLogin: true })
.mockReturnValueOnce({ openAtLogin: false, executableWillLaunchAtLogin: true });
Object.defineProperty(process, 'platform', { value: 'darwin' });
const state = await collectAutostartState();
expect(state.withArgs).toEqual({ openAtLogin: true, executableWillLaunchAtLogin: true });
expect(state.noArgs).toEqual({ openAtLogin: false, executableWillLaunchAtLogin: true });
expect(state.execPath).toBe(process.execPath);
expect(state.registryPath).toBeUndefined();
});
});
- Step 2: 테스트 실행 → fail (모듈 미존재)
npx vitest run src/main/services/AutostartDiagnostic.test.ts
- Step 3: AutostartDiagnostic.ts 작성 (Win registry 부분은 Task 20 에서 — 여기선 stub)
// src/main/services/AutostartDiagnostic.ts
import electron from 'electron';
const { app } = electron;
export interface AutostartState {
withArgs: { openAtLogin: boolean; executableWillLaunchAtLogin: boolean };
noArgs: { openAtLogin: boolean; executableWillLaunchAtLogin: boolean };
execPath: string;
registryPath?: string;
registryValue?: string | null;
}
export async function collectAutostartState(): Promise<AutostartState> {
const w = app.getLoginItemSettings({ args: ['--hidden'] });
const n = app.getLoginItemSettings();
const state: AutostartState = {
withArgs: { openAtLogin: w.openAtLogin, executableWillLaunchAtLogin: w.executableWillLaunchAtLogin },
noArgs: { openAtLogin: n.openAtLogin, executableWillLaunchAtLogin: n.executableWillLaunchAtLogin },
execPath: process.execPath
};
// Win registry 조회는 Task 20 에서 추가
return state;
}
- Step 4: 테스트 통과 + typecheck + 커밋
npx vitest run src/main/services/AutostartDiagnostic.test.ts
npm run typecheck
git add src/main/services/AutostartDiagnostic.ts src/main/services/AutostartDiagnostic.test.ts
git commit -m "feat(v027): AutostartDiagnostic — withArgs/noArgs/execPath 수집"
Task 20: AutostartDiagnostic — Windows registry 조회
Files:
-
Modify:
src/main/services/AutostartDiagnostic.ts -
Modify:
src/main/services/AutostartDiagnostic.test.ts -
Step 1: failing test (Win 분기 — registry 조회 + null fallback)
테스트 파일에 추가:
import { execFile } from 'node:child_process';
it('Windows: returns registryPath + registryValue when reg.exe succeeds', async () => {
mockApp.getLoginItemSettings.mockReturnValue({ openAtLogin: true, executableWillLaunchAtLogin: true });
Object.defineProperty(process, 'platform', { value: 'win32' });
mockExecFile.mockImplementation((_cmd, _args, cb) => {
cb(null, '\r\n Inkling REG_SZ "C:\\\\Users\\\\u\\\\Inkling.exe" --hidden\r\n', '');
});
const state = await collectAutostartState();
expect(state.registryPath).toBe('HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\\Inkling');
expect(state.registryValue).toContain('Inkling.exe');
});
it('Windows: returns null registryValue on reg.exe error (silent fallback)', async () => {
Object.defineProperty(process, 'platform', { value: 'win32' });
mockExecFile.mockImplementation((_cmd, _args, cb) => {
cb(new Error('not found'), '', '');
});
const state = await collectAutostartState();
expect(state.registryValue).toBeNull();
});
-
Step 2: 테스트 실행 → fail
-
Step 3: collectAutostartState 에 Win 분기 추가
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
const execFileAsync = promisify(execFile);
export async function collectAutostartState(): Promise<AutostartState> {
const w = app.getLoginItemSettings({ args: ['--hidden'] });
const n = app.getLoginItemSettings();
const state: AutostartState = {
withArgs: { openAtLogin: w.openAtLogin, executableWillLaunchAtLogin: w.executableWillLaunchAtLogin },
noArgs: { openAtLogin: n.openAtLogin, executableWillLaunchAtLogin: n.executableWillLaunchAtLogin },
execPath: process.execPath
};
if (process.platform === 'win32') {
state.registryPath = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\\Inkling';
state.registryValue = await readRegistrySilent(state.registryPath);
}
return state;
}
async function readRegistrySilent(path: string): Promise<string | null> {
try {
const { stdout } = await execFileAsync('reg', ['query', path, '/v', 'Inkling']);
const m = stdout.match(/REG_SZ\s+(.+)/);
return m ? m[1].trim() : null;
} catch {
return null;
}
}
- Step 4: 테스트 + typecheck + 커밋
npx vitest run src/main/services/AutostartDiagnostic.test.ts
npm run typecheck
git add src/main/services/AutostartDiagnostic.ts src/main/services/AutostartDiagnostic.test.ts
git commit -m "feat(v027): AutostartDiagnostic — Windows registry 조회 + silent fallback"
Task 21: IPC settings:autostart-state 핸들러
Files:
-
Modify:
src/main/ipc/settingsApi.ts -
Create:
src/main/ipc/settingsApi.test.ts -
Step 1: failing test (핸들러가 collectAutostartState 결과 그대로 반환)
// src/main/ipc/settingsApi.test.ts
import { describe, it, expect, vi } from 'vitest';
const handlers: Record<string, Function> = {};
vi.mock('electron', () => ({
default: {
ipcMain: { handle: (ch: string, fn: Function) => { handlers[ch] = fn; } },
app: { /* stubs */ },
shell: {}, clipboard: {}
}
}));
vi.mock('../services/AutostartDiagnostic', () => ({
collectAutostartState: vi.fn(async () => ({
withArgs: { openAtLogin: true, executableWillLaunchAtLogin: true },
noArgs: { openAtLogin: false, executableWillLaunchAtLogin: true },
execPath: '/path/to/exe'
}))
}));
import { registerSettingsApi } from './settingsApi';
describe('settings:autostart-state IPC', () => {
it('returns AutostartState from collectAutostartState', async () => {
registerSettingsApi();
const state = await handlers['settings:autostart-state']();
expect(state.withArgs.openAtLogin).toBe(true);
expect(state.noArgs.openAtLogin).toBe(false);
});
});
-
Step 2: 테스트 실행 → fail
-
Step 3: settingsApi.ts 에 핸들러 추가
registerSettingsApi 안에:
import { collectAutostartState } from '../services/AutostartDiagnostic';
ipcMain.handle('settings:autostart-state', () => collectAutostartState());
- Step 4: 테스트 + typecheck + 커밋
npx vitest run src/main/ipc/settingsApi.test.ts
npm run typecheck
git add src/main/ipc/settingsApi.ts src/main/ipc/settingsApi.test.ts
git commit -m "feat(v027): settings:autostart-state IPC 핸들러"
Task 22: settings:autostart-set 핸들러 정식 구현
Files:
- Modify:
src/main/ipc/settingsApi.ts - Modify:
src/main/ipc/settingsApi.test.ts
Task 9 에서 임시 stub 으로 등록한 settings:set-autostart 를 settings:autostart-set 으로 rename + 결과로 AutostartState 전체 반환하도록 갱신.
- Step 1: failing test
it('settings:autostart-set: calls setLoginItemSettings + returns updated state', async () => {
const mockSet = vi.fn();
// electron mock 의 app 에 setLoginItemSettings 추가 — top-level mock 갱신 필요
registerSettingsApi();
await handlers['settings:autostart-set'](null, true);
expect(mockSet).toHaveBeenCalledWith({ openAtLogin: true, args: ['--hidden'] });
});
(top-level vi.mock 의 app 에 setLoginItemSettings: mockSet 추가)
-
Step 2: 테스트 → fail
-
Step 3: 핸들러 갱신
ipcMain.handle('settings:autostart-set', async (_e, open: boolean) => {
app.setLoginItemSettings({ openAtLogin: open, args: ['--hidden'] });
return await collectAutostartState();
});
기존 Task 9 의 settings:set-autostart (또는 settings:get-autostart) 임시 stub 은 채널 이름 통일 — settings:get-autostart → settings:autostart-state, settings:set-autostart → settings:autostart-set. AutostartSection.tsx 의 호출부도 동시 갱신:
// src/renderer/inbox/api.ts
async getAutostart() { return await window.inkling.invoke('settings:autostart-state'); },
async setAutostart(open: boolean) { return await window.inkling.invoke('settings:autostart-set', open); },
preload 화이트리스트 갱신.
- Step 4: AutostartSection.test 회귀 확인
npx vitest run src/renderer/inbox/components/settings/
npm run typecheck
- Step 5: 커밋
git add -A src/main/ipc/ src/renderer/inbox/ src/preload/
git commit -m "feat(v027): settings:autostart-set 정식 구현 + 채널 이름 통일"
Task 23: AutostartSection — 진단 패널 UI
Files:
-
Modify:
src/renderer/inbox/components/settings/AutostartSection.tsx -
Modify:
src/renderer/inbox/components/settings/AutostartSection.test.tsx -
Step 1: failing test (진단 패널 펼치기 + mismatch ⚠️ 표시)
it('renders diagnostic panel when expanded, shows ⚠️ on mismatch', async () => {
const { inboxApi } = await import('../../api');
vi.mocked(inboxApi.getAutostart).mockResolvedValue({
openAtLogin: true,
diagnostic: {
withArgs: { openAtLogin: true, executableWillLaunchAtLogin: true },
noArgs: { openAtLogin: false, executableWillLaunchAtLogin: true }, // mismatch
execPath: '/path/to/Inkling.exe'
}
});
render(<AutostartSection />);
await screen.findByRole('checkbox');
fireEvent.click(screen.getByRole('button', { name: /진단 정보/ }));
expect(await screen.findByText(/⚠️/)).toBeInTheDocument();
expect(screen.getByText('/path/to/Inkling.exe')).toBeInTheDocument();
});
-
Step 2: 테스트 → fail
-
Step 3: AutostartSection 확장
import React, { useEffect, useState } from 'react';
import { inboxApi } from '../../api';
interface AutostartFull {
openAtLogin: boolean;
diagnostic: {
withArgs: { openAtLogin: boolean; executableWillLaunchAtLogin: boolean };
noArgs: { openAtLogin: boolean; executableWillLaunchAtLogin: boolean };
execPath: string;
registryPath?: string;
registryValue?: string | null;
};
}
export function AutostartSection(): React.ReactElement {
const [data, setData] = useState<AutostartFull | null>(null);
const [expanded, setExpanded] = useState(false);
useEffect(() => {
void (async () => setData(await inboxApi.getAutostart()))();
}, []);
async function onToggle(e: React.ChangeEvent<HTMLInputElement>): Promise<void> {
const r = await inboxApi.setAutostart(e.target.checked);
setData(r);
}
if (!data) return <div style={{ fontSize: 12 }}>로딩 중...</div>;
const d = data.diagnostic;
const mismatch = d.withArgs.openAtLogin !== d.noArgs.openAtLogin
|| !d.withArgs.executableWillLaunchAtLogin;
return (
<div>
<label style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<input type="checkbox" checked={data.openAtLogin} onChange={onToggle} />
앱 시작 시 자동으로 실행
</label>
{mismatch && <div style={{ color: '#c33', fontSize: 12, marginTop: 4 }}>⚠️ 등록 상태 불일치 감지</div>}
<button
onClick={() => setExpanded(!expanded)}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, color: '#0a4b80', marginTop: 4 }}
>
{expanded ? '▾' : '▸'} 진단 정보
</button>
{expanded && (
<div style={{ fontSize: 11, lineHeight: 1.6, marginTop: 4, fontFamily: 'monospace', background: '#f5f5f5', padding: 8 }}>
<div>표준 (--hidden 인자): openAtLogin={String(d.withArgs.openAtLogin)}, willLaunch={String(d.withArgs.executableWillLaunchAtLogin)}</div>
<div>비교 (인자 없이): openAtLogin={String(d.noArgs.openAtLogin)}, willLaunch={String(d.noArgs.executableWillLaunchAtLogin)}</div>
<div>실행 파일 경로: {d.execPath}</div>
{d.registryPath && <div>registry 경로: {d.registryPath}</div>}
{d.registryValue !== undefined && <div>registry 값: {d.registryValue ?? '(없음)'}</div>}
</div>
)}
</div>
);
}
(getAutostart 의 반환 타입이 AutostartFull 로 확장됨 — main IPC 핸들러도 collectAutostartState 결과를 diagnostic 으로 wrap 하여 반환하도록 갱신.)
main 측 settings:autostart-state 핸들러 갱신:
ipcMain.handle('settings:autostart-state', async () => {
const diag = await collectAutostartState();
return { openAtLogin: diag.withArgs.openAtLogin, diagnostic: diag };
});
- Step 4: 테스트 + typecheck + 커밋
npx vitest run src/renderer/inbox/components/settings/
npm run typecheck
git add -A src/renderer/inbox/ src/main/ipc/
git commit -m "feat(v027): AutostartSection 진단 패널 + mismatch 경고"
Task 24: AutostartSection — "재등록" 버튼
Files:
-
Modify:
src/renderer/inbox/components/settings/AutostartSection.tsx -
Modify:
src/renderer/inbox/components/settings/AutostartSection.test.tsx -
Step 1: failing test
it('"재등록" button calls setAutostart with current value', async () => {
const { inboxApi } = await import('../../api');
vi.mocked(inboxApi.getAutostart).mockResolvedValue({
openAtLogin: true,
diagnostic: { /* ... */ }
});
render(<AutostartSection />);
await screen.findByRole('checkbox');
fireEvent.click(screen.getByRole('button', { name: /재등록/ }));
expect(inboxApi.setAutostart).toHaveBeenCalledWith(true);
});
-
Step 2: 테스트 → fail
-
Step 3: AutostartSection 에 버튼 추가
async function onReregister(): Promise<void> {
if (!data) return;
const r = await inboxApi.setAutostart(data.openAtLogin);
setData(r);
}
// JSX 에 버튼 추가 (mismatch 경고 옆 또는 아래):
<button onClick={onReregister} style={{ marginTop: 8 }}>재등록</button>
- Step 4: 테스트 + typecheck + 커밋
npx vitest run src/renderer/inbox/components/settings/
npm run typecheck
git add src/renderer/inbox/components/settings/AutostartSection.tsx src/renderer/inbox/components/settings/AutostartSection.test.tsx
git commit -m "feat(v027): AutostartSection 재등록 버튼"
Phase 5 종료.
Phase 6: Cleanup + verification
Task 25: OllamaSettingsModal.tsx 제거 + 관련 import 정리
Files:
-
Delete:
src/renderer/inbox/components/OllamaSettingsModal.tsx -
Modify:
src/renderer/inbox/App.tsx(import + state 제거) -
Modify:
src/renderer/inbox/api.ts(onOpenOllamaSettings 미사용 시 제거) -
Step 1: 사용처 확인
grep -rn 'OllamaSettingsModal\|onOpenOllamaSettings' src/
Expected: src/renderer/inbox/App.tsx 에 import + ollamaSettingsOpen state. 다른 곳 0건이면 안전 제거.
- Step 2: App.tsx 정리
App.tsx 에서:
-
import { OllamaSettingsModal } from './components/OllamaSettingsModal.js';제거 -
const [ollamaSettingsOpen, setOllamaSettingsOpen] = useState(false);제거 -
useEffect안inboxApi.onOpenOllamaSettings(...)구독 + cleanup 제거 -
JSX 에서
<OllamaSettingsModal ... />렌더 제거 -
Step 3: api.ts 의 onOpenOllamaSettings 제거 (사용처 없음 확인 후)
grep -rn 'onOpenOllamaSettings' src/
App.tsx 에서 제거됐으면 src 전체 0건. api.ts + preload 의 채널 expose 제거.
main 측 runOpenOllamaSettings callback (Task 16 에서 이미 제거됐을 것) 도 잔재 없는지 재확인.
- Step 4: 파일 삭제
rm src/renderer/inbox/components/OllamaSettingsModal.tsx
- Step 5: 전체 테스트 + typecheck
npm run rebuild:node
npm test
npm run typecheck
- Step 6: 커밋
git add -A
git commit -m "refactor(v027): OllamaSettingsModal 제거 + onOpenOllamaSettings 채널 cleanup"
Task 26: 전체 회귀 검증 + e2e
Files:
-
검증 only.
-
Step 1: 단위 + e2e + typecheck 일괄
npm run rebuild:node
npm test
npm run typecheck
npm run rebuild:electron
npm run test:e2e
Expected: 모두 pass. 단위 426 → 약 450 (기준선 메모 project_inkling_status.md).
- Step 2: Win 수동 launch 검증
npm run start
체크리스트:
-
트레이 4 항목만 표시
-
트레이 "설정..." → SettingsPage 진입 → 4 섹션 (AI / 자동 실행 / 백업 / 정보) 모두 표시
-
AI 섹션 endpoint 변경 → 저장 → "지금 재확인" → 결과 표시
-
자동 실행 토글 → 진단 패널 펼침 → withArgs/noArgs 표시
-
백업 섹션 5 버튼 동작
-
정보 섹션 버전/데이터 위치 + "정보 복사" → clipboard 검증
-
Step 3: macOS 수동 launch 검증
npm run start
체크리스트:
-
F14 fix 검증: 빨간 신호등 (close) → dock 클릭 → inbox 재등장
-
macOS 메뉴바 트레이 4 항목 (Win 동일)
-
설정 페이지 동일 동작
-
Step 4: Linux smoke 재검증 (Phase 1 후 변경 사항 영향 확인)
npm run dist:linux
산출 AppImage 를 Linux VM 에서 실행 → 트레이 (있는 DE) 또는 inbox 톱니바퀴 → 설정 페이지 동작 확인.
- Step 5: 결과 spec §11 갱신 + 커밋 (필요 시)
git add docs/superpowers/specs/2026-05-06-v027-cross-platform-design.md
git commit -m "docs(v027): 전체 회귀 검증 + 3-OS smoke 결과 기록"
Task 27: dogfood-feedback.md F12/F14/F15/F16 promoted 마킹 + version bump + release prep
Files:
-
Modify:
docs/superpowers/specs/2026-04-25-dogfood-feedback.md(F12/F14/F16 entry 🚀 promoted 마킹) -
Modify:
docs/superpowers/v024-backlog.md(#45 자동 실행 처리 갱신) -
Modify:
package.json(version 0.2.6 → 0.2.7) -
Step 1: F12 entry 갱신
F12. 윈도우 자동 실행 옵션이 재시작 후 풀려있는 버그 의 진행 상태 헤더를 🚀 promoted → docs/superpowers/specs/2026-05-06-v027-cross-platform-design.md §9 로 변경. v0.2.7 진단 노출 적용 사실 추가.
- Step 2: F14 entry 갱신
F14. macOS dock 클릭 시 hidden 창 재현 안 됨 진행 상태를 🚀 promoted → docs/superpowers/specs/2026-05-06-v027-cross-platform-design.md §8 로 변경.
- Step 3: F16 entry 갱신
F16. 트레이 의존도 ↓ + 별도 설정 페이지 진행 상태를 🚀 promoted → docs/superpowers/specs/2026-05-06-v027-cross-platform-design.md §6, §7 로 변경.
- Step 4: backlog #45 처리 갱신
docs/superpowers/v024-backlog.md 의 처리 이력 표 #45 row 의 상태를 ✅ 처리 (진단 노출 + 재등록 버튼) 로 갱신, Cut 컬럼 = v0.2.7. 잔여 카운트 갱신 (24 → 23).
- Step 5: package.json version bump
"version": "0.2.7",
- Step 6: 커밋 + tag
git add docs/superpowers/specs/2026-04-25-dogfood-feedback.md docs/superpowers/v024-backlog.md package.json
git commit -m "chore(release): v0.2.7 — cross-platform 입구 정상화 (F12 deeper + F14 + F15 빌드 + F16)"
git tag v0.2.7
Release 빌드 + Gitea release 는 별도 (PR 머지 후 본인 dogfood 가 gitea-release 스킬 호출). plan 종료 시점 = main 으로 PR open 또는 merge 직전 상태.
Self-Review
Spec coverage check:
| Spec 섹션 | 커버 task |
|---|---|
| §5 Linux 빌드 | Task 1~5 |
| §6 설정 페이지 라우팅 | Task 6, 7, 12, 13 |
| §6-3-1 AI 제공자 섹션 | Task 8 |
| §6-3-2 자동 실행 섹션 (토글) | Task 9 |
| §6-3-3 백업/복원/내보내기 | Task 10 |
| §6-3-4 정보 | Task 11 |
| §7 트레이 슬림 | Task 14, 15, 16, 17 |
| §8 F14 dock fix | Task 18 |
| §9 F12 deeper fix | Task 19, 20, 21, 22, 23, 24 |
| §10 테스트 | 각 task 안 단위 + Task 26 e2e |
| §11 Risk fallback | Task 1, 3, 26 step 5 |
| 정리 (OllamaSettingsModal 제거) | Task 25 |
| Version bump + dogfood entry promoted | Task 27 |
모든 spec 요구가 task 에 매핑됨. 빠진 곳 없음.
Placeholder scan: "TBD" / "TODO" / "implement later" 없음. 각 step 에 코드 또는 명령 직접 포함.
Type consistency:
TrayCallbacks(Task 14): showInbox / showCapture / showSettings — Task 16 wiring + Task 17 검증에서 동일 사용.TrayState(Task 14):{ todayCount: number }— Task 16 의 refreshTray 호출에서 동일.AutostartState(Task 19): withArgs / noArgs / execPath / registryPath?/ registryValue? — Task 21 의 IPC, Task 23 의 UI 에서 일관 사용.AutostartFull(Task 23 renderer): { openAtLogin, diagnostic } — Task 22 main 핸들러 반환 형태와 일치.- IPC 채널 이름 (Task 22 통일):
settings:autostart-state,settings:autostart-set,settings:run-backup등 — 모든 task 에서 동일.
잠재 모순 체크 (수동 검토):
- Task 9 의 임시 stub (
settings:get-autostart/settings:set-autostart) 와 Task 22 의 정식 (settings:autostart-state/settings:autostart-set) 채널 이름 불일치 — Task 22 Step 3 에서 통일하도록 명시. 실행 시 누락하지 말 것. - Task 8 의 IPC 채널 이름 (
getOllamaSettings,saveOllamaSettings,ollamaRecheck) — 기존OllamaSettingsModal.tsx가 사용 중인 채널 확인 필요. 다른 이름이면 Task 8 step 5 에서 정정.
이슈 없음. 실행 가능.
Execution Handoff
Plan 작성 완료, docs/superpowers/plans/2026-05-07-v027-cross-platform.md 저장.
두 가지 실행 옵션:
1. Subagent-Driven (recommended) — fresh subagent per task, two-stage review (spec 준수 + code quality), 빠른 iteration
2. Inline Execution — 본 세션에서 task 일괄 실행 + checkpoint 마다 review
어느 쪽?