# v0.2.8 — Cut A Design (이미지 렌더링 + 앱 아이콘)

**작성일:** 2026-05-09
**저자:** 김태현 (dlsrks0734@gmail.com)
**선행 문서:**

- `docs/superpowers/specs/2026-04-25-dogfood-feedback.md` (F22)
- `docs/superpowers/strategy/v028plus-roadmap.md` (Cut A 분할 + 우선순위)

**Cut 라벨:** v0.2.8 (semver patch — bug fix + asset 추가)

---

## 1. Cut 정체성

**"이미지 렌더링 + 앱 아이콘 polish" cut.** 두 작은 항목 묶음:

- **F22 (이미지 렌더링)**: NoteCard 의 회색 placeholder div 를 실제 `<img>` 로 교체. Electron renderer 가 raw `file://` 직접 접근 어려운 보안 정책 우회 — `inkling-media://` custom protocol 등록.
- **chore (앱 아이콘)**: 사용자 첨부 SVG (이미 `assets/icon.svg` 작성·검토 완료) → ICO/ICNS/PNG 다중 size 자동 생성 + electron-builder config 통합.

명확/작은 작업, 의사결정 거의 없음. 빠른 release polish.

---

## 2. 범위

| 항목 | 출처 | 작업 |
|---|---|---|
| **F22** | dogfood F22 | `inkling-media://` protocol + NoteCard `<img>` + 클릭 시 OS viewer (`shell.openPath`) |
| **chore** | roadmap | `electron-icon-builder` devDep + `npm run build:icons` + electron-builder config (`build.win.icon` / `build.mac.icon` / `build.linux.icon`) |

---

## 3. F22 디테일

### 3-1. Custom protocol 등록

`src/main/index.ts` 의 `whenReady` **이전** (top-level) 에 scheme 권한 등록:

```ts
import { protocol } from 'electron';

protocol.registerSchemesAsPrivileged([
  { scheme: 'inkling-media', privileges: { secure: true, supportFetchAPI: true, stream: true } }
]);
```

`whenReady` 안에서 handler 등록:

```ts
import { promises as fs } from 'node:fs';
import { join, normalize, sep } from 'node:path';

protocol.handle('inkling-media', async (req) => {
  const url = new URL(req.url);
  const relPath = decodeURIComponent(url.pathname).replace(/^\//, '');
  const mediaRoot = join(paths.profileDir, 'media');
  const target = normalize(join(mediaRoot, relPath));
  if (!target.startsWith(mediaRoot + sep)) {
    return new Response(null, { status: 403 });
  }
  try {
    const data = await fs.readFile(target);
    return new Response(data, { headers: { 'content-type': inferMime(target) } });
  } catch {
    return new Response(null, { status: 404 });
  }
});
```

`inferMime()` — 파일 확장자 → MIME (png/jpg/jpeg/gif/webp). 작은 함수 (별도 util 또는 inline).

### 3-2. NoteCard 갱신

[src/renderer/inbox/components/NoteCard.tsx:336-338](src/renderer/inbox/components/NoteCard.tsx#L336-L338) 의 회색 div 를 `<img>` 로 교체:

```tsx
{local.media.map((m) => (
  <img
    key={m.id}
    src={`inkling-media://${m.relPath}`}
    alt=""
    title={m.relPath}
    onClick={() => inboxApi.openMedia(m.relPath)}
    style={{
      width: 48,
      height: 48,
      objectFit: 'cover',
      borderRadius: 4,
      cursor: 'pointer',
      border: '1px solid #e0e0e0'
    }}
  />
))}
```

`m.relPath` 형식 = `media/<noteId>/<filename>`. URL 형식: `inkling-media://media/<noteId>/<filename>`. handler 가 prefix 제거 후 `<profileDir>/media/<noteId>/<filename>` 으로 resolve.

### 3-3. IPC `inbox:open-media`

`src/main/ipc/inboxApi.ts` 에 신규 핸들러:

```ts
ipcMain.handle('inbox:open-media', async (_e, relPath: string) => {
  const mediaRoot = join(paths.profileDir, 'media');
  const target = normalize(join(mediaRoot, relPath));
  if (!target.startsWith(mediaRoot + sep)) return { ok: false, reason: 'invalid path' };
  await shell.openPath(target);
  return { ok: true };
});
```

preload 화이트리스트 + `src/shared/types.ts` `InboxApi.openMedia(relPath: string)` 시그니처 + `src/renderer/inbox/api.ts` wrapper.

### 3-4. 보안 검토

- **Path traversal**: protocol handler + IPC 핸들러 모두 `target.startsWith(mediaRoot + sep)` 검사. 통과 못 하면 403/실패.
- **Schemes privileges**: `secure: true` 로 https 동등 권한 — webContents 가 페이지 안에서 `<img src="inkling-media://...">` 정상 로드.
- **CORS**: same-origin 정책 영향 X (custom protocol 이라 별도). webContents 안 동일 origin 으로 인식.
- **인증**: 단일 사용자 desktop app — 추가 인증 X.

---

## 4. chore 디테일

### 4-1. 의존성 + scripts

`package.json`:

```json
"devDependencies": {
  "electron-icon-builder": "^2.0.1"
},
"scripts": {
  "build:icons": "electron-icon-builder --input=assets/icon.svg --output=build --flatten"
}
```

`--flatten` 옵션 = output 을 `build/icon.ico`, `build/icon.icns`, `build/icon.png` (1024x1024) 평면 배치. nested `build/icons/png/<size>.png` 도 함께.

### 4-2. electron-builder config

`package.json` 의 `build` 블록 갱신:

```json
"win": { "icon": "build/icon.ico", ... },
"mac": { "icon": "build/icon.icns", ... },
"linux": { "icon": "build/icon.png", ..., "target": [ ... ] }
```

기존 win/mac/linux 블록에 `"icon"` 키만 추가 (다른 설정 그대로).

### 4-3. 산출물 git 추적

`build/` 가 `.gitignore` 에 있다면 — 두 옵션:

- (a) **`build/icon.*` 만 ignore 풀고 commit** (size 약 200KB-1MB 작음 — 바이너리 commit 일반적). SVG 갱신 시 `npm run build:icons` 후 commit.
- (b) **모두 ignore 유지** + `prebuild` script 등으로 빌드 시 매번 재생성. dist 빌드 시 자동 — 그러나 dev 환경 (npm start) 에서 아이콘 미생성 시 fallback 필요.

추천: **(a)** — 단순, 빌드 시간 ↓, dev 환경 문제 X.

`.gitignore` 갱신 예:

```
build/
!build/icon.ico
!build/icon.icns
!build/icon.png
```

### 4-4. SVG 가 input 으로 바로 가능?

`electron-icon-builder` v2.0.1 docs 검토 — PNG 1024x1024 입력 권장, SVG 는 `librsvg` 등 의존. SVG 직접 안 되면 `sharp` 로 SVG → PNG 1024 변환 후 input.

대안 (SVG 직접 안 될 시):

```json
"build:icons:png": "node scripts/svg-to-png.mjs assets/icon.svg build/icon-source.png 1024",
"build:icons": "npm run build:icons:png && electron-icon-builder --input=build/icon-source.png --output=build --flatten"
```

`scripts/svg-to-png.mjs` — `sharp` 활용 ~10줄 스크립트.

---

## 5. 테스트 전략

| 영역 | 단위 | 수동 |
|---|---|---|
| protocol handler — path traversal | mock fs + URL 입력 (`../etc/passwd` 형태) → 403 | - |
| protocol handler — 정상 200 | mock fs.readFile → bytes + content-type 검증 | - |
| protocol handler — 404 | fs.readFile reject → 404 | - |
| `inferMime` | 확장자별 정확 mapping | - |
| NoteCard `<img>` 렌더 | media 배열 길이 N → `<img>` N 개 (jsdom mock) | - |
| `<img>` 클릭 → IPC | onClick stub → `inboxApi.openMedia` 호출 | - |
| IPC `inbox:open-media` | path traversal mock → 'invalid path' 반환 | - |
| 아이콘 빌드 | - | `npm run build:icons` → `build/icon.ico` `build/icon.icns` `build/icon.png` 존재 확인 |
| Win exe 아이콘 | - | `npm run dist:win` → `Inkling Setup 0.2.8.exe` 우클릭 → properties → 아이콘 = 새 디자인 |
| dogfood image flow | - | inbox 의 thumbnail 클릭 → OS viewer 열림 (Win + macOS) |

**목표**: 단위 460 → 약 467 (+7), typecheck 0.

---

## 6. Risk + Known unknowns

| Risk | 발생 시 대응 |
|---|---|
| `electron-icon-builder` SVG 직접 미지원 | `sharp` 로 SVG → PNG 1024 변환 (4-4 대안 적용) |
| `protocol.handle` 가 Electron 41 미지원 (deprecated `protocol.registerFileProtocol` 만 있는 경우) | Electron 41 docs 확인 후 deprecated API 사용 또는 newer API |
| `<img>` 가 inkling-media:// 로드 실패 (CSP 차단 등) | webContents 의 contentSecurityPolicy 검토. v0.2.5/6 의 single-instance lock + B4 #46 hidden flag 와 무관 |
| Win/Mac dogfood 시 OS viewer 가 default 미설정 | 사용자 OS settings — Inkling 책임 외 (그러나 안내 메시지 가능) |

---

## 7. v0.2.8 후

**다음**: Cut B (v0.2.9) — F17 status 분기 + F18 사유 + F23 Ollama-less. 데이터 모델 정비 cut.

**Cut A 머지 후 dogfood verify 항목**:

1. inbox 의 capture-with-image 흐름 — 캡처 → 이미지 thumbnail 표시 → 클릭 → OS viewer
2. 새 아이콘이 트레이 / Windows taskbar / dock 모두 정확 표시
3. 다중 이미지 (capture 가 N개 첨부) 의 grid layout — flex-wrap 적용 시 N row 자연스러운지

이슈 발견 시 dogfood-feedback.md F26 부터 누적.
