## 2026-05-11 — 인앱 프로젝트 파일 탐색기 (Files 탭) — 트리/인라인 미리보기 + 트래버설 가드

### 배경

프로젝트의 폴더 트리를 확인하거나 README/이미지/PDF/스크립트 파일을 빠르게 살펴보려고 매번 OS 파일 매니저로 alt-tab 하는 번거로움이 있었다. Git 탭은 수정/스테이징 UI라 단순 열람에는 무겁고, Wiki 탭은 큐레이션된 노드만 다뤄 raw 디스크 트리를 보여주지 않는다. 읽기 전용 인앱 탐색기를 추가해 텍스트/이미지/PDF/오디오/비디오를 인라인으로 미리보기 가능하게 했다. 또한 추가 직후 탭 위치가 Git/SVN과 Analytics 사이 후반부에 있어 좁은 뷰포트에서 가로 스크롤 없이는 보이지 않아 탭 순서를 맨 앞으로 재배치했다.

### 주요 변경

#### 1. Files 탭 추가 + 서버 라우트 + 인라인 미리보기 (`5709a84`)

3개 신규 엔드포인트를 가진 `/api/projects/:id/files*` 라우트군과 좌측 트리 + 우측 미리보기 분할 패널 컴포넌트를 추가. 모두 읽기 전용 (mutation은 기존 Git 탭이 담당).

- **서버 — `src/server/routes/files.ts`** (신규, 230줄):
  - `GET /api/projects/:id/files?path=&showHidden=` — lazy 디렉토리 리스팅. `resolveSafe()`로 경로 트래버설 가드 (`abs.startsWith(rootWithSep)` 검사). `.git` / `node_modules` / `.worktrees` / `.DS_Store`는 `DEFAULT_HIDDEN`에 들어 있어 `showHidden=1` 없이는 노출 안 됨. dot-prefix 파일은 hidden 플래그만 달고 항상 반환 (사용자가 직접 토글). 디렉토리 우선 + 케이스 인센시티브 이름 정렬
  - `GET /api/projects/:id/files/content?path=` — 텍스트 미리보기 (≤2MB cap, 첫 8KB에 NUL byte 발견 시 binary로 폴백). 확장자별 MIME 매핑 + `image/svg+xml`은 텍스트로 취급
  - `GET /api/projects/:id/files/binary?path=` — 바이너리 스트리밍 (≤50MB cap, `Content-Type` 헤더 + `res.sendFile`로 `<img>` / `<video>` / `<audio>` / `<iframe>` 인라인 렌더링 가능)
  - `src/server/index.ts`에서 `app.use('/api/projects', filesRouter)`로 마운트

- **클라이언트 — `src/client/src/components/FileExplorer.tsx`** (신규, 524줄):
  - 좌측 lazy 트리 (확장 시 자식 fetch + 메모이즈), 우측 미리보기 pane, 가운데 드래그 가능한 `Resizer` (localStorage 너비 영속화)
  - 확장자별 아이콘 (`lucide-react`의 FileText / FileImage / FileCode / FileVideo / FileAudio / Folder / FolderOpen)
  - 이미지/PDF/오디오/비디오는 `getBinaryFileUrl()` URL을 `<img>` / `<iframe>` / `<audio>` / `<video>`에 직접 바인딩
  - 알 수 없는 바이너리는 다운로드 링크 폴백
  - `Show hidden files` 토글, `Copy path` 액션, 새로고침 버튼
  - `src/client/src/api/files.ts` (신규) — `listFiles` / `getFileContent` / `getBinaryFileUrl` 클라이언트 API
  - i18n: `tabs.files` + `files.*` 키 EN/KO 추가

- **`electron/main.cjs`**: `ipcMain.on('ime:reset', ...)` 핸들러 추가 — 이전 커밋(`2026-05-05`)의 IME focus 핸드오프 인프라가 이번 변경과 함께 메인 프로세스에 정착

#### 2. 탭 순서를 맨 앞으로 재배치 (`2c930d7`)

추가 시점 위치(Git/SVN과 Analytics 사이)가 overflow 영역에 묻혀, 좁은 뷰포트에서 가로 스크롤 없이는 Files 탭이 안 보였다. 프로젝트 콘텐츠 탐색 진입점으로서 기본 내비게이션 어포던스로 읽히도록 plugin 탭들보다도 앞으로 이동.

- **`src/client/src/components/ProjectDetail.tsx`**: 탭 배열의 Files 엔트리를 plugin 탭(Wiki/Planner/Tasks/Sessions/Discussions/Schedules/Git/SVN/Analytics)들 앞으로 이동

### 수정된 주요 파일

| 파일 | 변경 내용 |
|------|----------|
| `src/server/routes/files.ts` | 신규 — `GET /files` (lazy 디렉토리) / `/files/content` (텍스트 ≤2MB + NUL 휴리스틱) / `/files/binary` (≤50MB 스트리밍 + Content-Type) |
| `src/server/index.ts` | filesRouter 마운트 (`/api/projects`) |
| `src/client/src/components/FileExplorer.tsx` | 신규 — 좌 트리 + 우 미리보기 + 드래그 리사이저 + 숨김 토글 + 인라인 image/video/audio/pdf 렌더링 |
| `src/client/src/api/files.ts` | 신규 — listFiles / getFileContent / getBinaryFileUrl 클라이언트 API |
| `src/client/src/components/ProjectDetail.tsx` | Files 탭 등록 → plugin 탭 앞으로 재배치 |
| `src/client/src/i18n.tsx` | EN/KO `tabs.files` + `files.*` 키 |
| `electron/main.cjs` | `ime:reset` ipcMain 핸들러 (이전 IME 인프라 정착) |

### 아키텍처 결정

1. **읽기 전용으로 한정**: 인앱 탐색기에 수정/삭제 기능을 넣지 않은 이유는 mutation 경로가 이미 Git 탭(stage/commit/discard)으로 일원화돼 있기 때문. Files 탭이 raw 파일 시스템을 직접 수정하기 시작하면 Git 상태와 어긋날 위험이 있다 — 익숙한 OS 파일 매니저의 단순 열람 대체 용도로만 정의.
2. **텍스트 vs 바이너리 2-엔드포인트 분리**: 텍스트는 JSON body로 인라인 응답해 React state로 직접 표시; 바이너리는 별도 `/binary` 엔드포인트로 `Content-Type` 헤더 + `res.sendFile`을 통해 브라우저 네이티브 디코더(`<img>` / `<video>` / `<audio>` / `<iframe>`)에 직접 위임. 이렇게 분리해야 JSON wrapper의 base64 오버헤드도 피하고 비디오/오디오의 progressive streaming도 그대로 동작한다.
3. **트래버설 가드는 prefix 매칭으로 통일**: `resolveSafe()`는 `path.resolve()` 후 `abs === root || abs.startsWith(root + sep)` 검사로 트래버설을 차단. Wiki raw-files 라우트의 가드와 동일한 패턴 — 모든 경로 입력 라우트가 같은 모양을 갖도록 유지.
4. **탭 위치는 plugin 탭들보다 앞**: Files는 "프로젝트 컨텐츠를 탐색" 용도라 plugin(Wiki/Planner/Tasks/Sessions/Discussions/Schedules)들보다 더 보편적인 진입점. 모바일/좁은 뷰포트에서도 가로 스크롤 없이 닿도록 최상단 배치.
