## 2026-05-05 — Electron 데스크톱 폴리시: 한글 IME focus 핸드오프 + 잠금화면 복귀 + 자동 업데이트

### 배경

Windows EXE 빌드 사용자 보고로 세 갈래 결함이 한꺼번에 드러났다 — (1) 세션을 한 번 사용하고 나면 `xterm`의 helper textarea가 native HWND 키보드 포커스를 점유한 채 놓아주지 않아서, 인라인 `SessionForm`의 입력 필드를 클릭해도 캐럿이 활성화되지 않음(한글 IME가 죽고 사용자가 alt-tab으로 다른 앱 갔다 돌아와야 복구). (2) 잠금화면/스크린세이버 해제 후 BrowserWindow는 활성화되지만 `webContents`로 native 포커스가 돌아오지 않아 모든 입력(SessionForm, SessionTerminal)이 데드. (3) 데스크톱 빌드는 자동 업데이트 경로가 없어서 사용자가 매번 setup.exe를 수동 다운로드해야 하고, stale한 설치본이 조용히 계속 돌아감. 세 건 모두 Electron main 프로세스 레벨에서 정리.

### 주요 변경

#### 1. 한글 IME focus 핸드오프 — `ime:reset` IPC 브리지 (`6c63ed0`)

`xterm.js`의 보조 textarea가 native HWND 포커스를 잡고 있어 React `autoFocus`로는 DOM 포커스만 옮기고 OS 레벨 캐럿은 안 따라옴. main 프로세스의 `webContents.focus()`를 IPC로 호출해서 OS 레벨 포커스를 강제 회수.

- **`electron/main.cjs`**:
  - `ipcMain.on('ime:reset', ...)` 핸들러 추가 — `mainWindow.webContents.focus()` 호출.
- **`electron/preload.cjs`**:
  - `contextBridge.exposeInMainWorld('electronAPI', { imeReset: () => ipcRenderer.send('ime:reset') })` — 렌더러 측 entry point.
- **`src/client/src/components/SessionForm.tsx`**:
  - mount 시점 `useEffect`에서 (1) 이전 active element `blur()`, (2) `electronAPI.imeReset()` 호출, (3) RAF 두 번 거친 뒤 `titleRef.current?.focus()` — xterm 자체의 포커스 복구가 settle된 후에 캐럿을 옮기기 위함.
  - 폼 lifetime 동안 모든 `.xterm-helper-textarea` 요소를 `tabIndex=-1` + `aria-hidden=true`로 포커스 traversal에서 park, unmount 시 원복. 기존 `autoFocus` 어트리뷰트는 제거(이제 useEffect가 책임).
  - mount-time only effect — `initial` 변경에 재실행하면 사용자가 폼 편집 중일 때 포커스를 뺏는 사고가 나기 때문에 빈 deps 배열로 못박음.

#### 2. 잠금화면/스크린세이버 복귀 시 webContents 재포커스 (`75cda44`)

- **`electron/main.cjs`** — `mainWindow.on('focus', () => mainWindow.webContents.focus())` 한 줄 추가. lock resume / alt-tab / minimize→restore 어떤 활성화 경로에서도 OS 포커스를 webContents로 routing.

#### 3. `electron-updater` GitHub Releases 자동 업데이트 (`aa97f07`)

릴리스 워크플로는 이미 `electron-builder --publish always`로 `latest.yml` + blockmap을 업로드하고 있어, 런타임 측 `electron-updater`만 와이어링하면 자동 업데이트가 동작.

- **`package.json`** — `electron-updater@^6.8.3` 추가. electron-builder가 prod-deps를 자동으로 asar에 번들.
- **`electron/main.cjs`**:
  - `setupAutoUpdater()` — `app.isPackaged`일 때만 동작(개발 모드 NOOP).
    - `autoDownload = true`, `autoInstallOnAppQuit = true`.
    - 윈도우 생성 5초 후 첫 체크(`setTimeout(checkForUpdates, 5000)`) — 런치 UX 비차단.
    - `update-downloaded` 이벤트에서 `dialog.showMessageBox`로 "지금 재시작 / 나중에" 프롬프트. 나중에 선택 시 `autoInstallOnAppQuit` 폴백.
    - `error` / `update-available` / `update-not-available` / `download-progress` 콘솔 로깅.
  - `checkForUpdates({silent})` — in-flight 가드(`updateCheckInFlight`)로 동시 호출 차단, non-silent 모드에선 에러 시 dialog 노출.
  - Help 메뉴에 "업데이트 확인" 아이템 추가 — 수동 체크용.
  - `bootServer()` 후 `setupAutoUpdater()` 호출.

> 워크플로 변경 없음. 다음 릴리스부터 효력 발생, 0.2.4 사용자는 이번 한 번만 수동 setup.exe를 받아야 이후 자동 업데이트 와이어링이 들어감.

### 수정된 주요 파일

| 파일 | 변경 내용 |
|------|----------|
| `electron/main.cjs` | `ime:reset` IPC 핸들러 + window focus → webContents.focus() + `electron-updater` `setupAutoUpdater()` + Help 메뉴 "업데이트 확인" |
| `electron/preload.cjs` | `contextBridge.exposeInMainWorld('electronAPI', {imeReset})` 노출 |
| `src/client/src/components/SessionForm.tsx` | mount-time focus 댄스(blur → imeReset → 2x RAF → focus titleRef) + xterm helper textarea park |
| `package.json` | `electron-updater@^6.8.3` 추가 |

### 아키텍처 결정

1. **OS 레벨 포커스는 OS 레벨 API로**: 렌더러의 React `autoFocus`나 `element.focus()`는 DOM 포커스만 다룸. native HWND 포커스가 다른 곳(xterm helper textarea)에 잡혀 있으면 캐럿이 시각적으로 깜빡이지 않고 키 입력이 안 들어감. main 프로세스의 `webContents.focus()`만이 OS-level focus를 BrowserWindow의 webContents로 강제 회수 가능. IPC 브리지(`ime:reset`)로 이를 렌더러에서 호출 가능하게 만들었고, 같은 패턴이 향후 다른 native focus 회복 시나리오(클립보드 페이스트 후, 모달 dismiss 후 등)에도 재사용 가능.
2. **잠금화면 복귀는 window.on('focus') 한 줄로 충분**: BrowserWindow의 `focus` 이벤트는 lock resume / alt-tab / minimize→restore 모든 활성화 경로에서 발화. `webContents.focus()`를 무조건 다시 호출하는 비용은 거의 0이므로 분기 없이 모든 focus event에 바인딩하는 게 가장 안전.
3. **자동 업데이트는 5초 지연 첫 체크**: 부트 시점에 update check를 즉시 돌리면 GitHub API 콜이 윈도우 첫 페인트와 경쟁해 launch UX가 끊겨 보일 수 있음. 5초 지연으로 사용자가 UI를 본 뒤 백그라운드에서 체크가 시작됨. `autoDownload + autoInstallOnAppQuit`이라 사용자는 다음 종료 시점에 자연스럽게 새 버전을 받음. "지금 재시작" 다이얼로그는 download가 완료된 시점에만 노출되므로 사용자 동의 없이 강제 재시작은 없음.
4. **Help 메뉴 수동 체크 + in-flight 가드**: 백그라운드 체크가 실패해도 사용자가 명시적으로 다시 트리거할 수 있어야 함. `updateCheckInFlight` 플래그로 동시 호출을 막고, non-silent 모드에선 에러를 dialog로 노출(silent 모드는 console.error만).
5. **`autoFocus` 제거 + `useEffect`로 일원화**: React `autoFocus`는 DOM 포커스만 다루고 OS 포커스 핸드오프를 못 함. 동일 effect 안에서 blur → imeReset → 2x RAF → focus를 순서대로 실행해서 단일 책임 지점을 가짐. mount-time only 보장(`[]` deps + lint disable comment)으로 사용자가 폼 편집 중일 때 포커스 도둑질되는 사고를 차단.

### 후속 검증

- `setup.exe` 0.2.4 사용자는 다음 릴리스를 한 번만 수동 설치해야 자동 업데이트 와이어링이 들어감(이미 안내). 그 이후 릴리스부터 자동 적용.
- 잠금화면 복귀 fix는 모든 Windows 환경에 적용되지만 Linux/macOS에는 영향 없음(둘 다 native focus를 자동으로 webContents로 routing).

---
