## 2026-05-05 — Opt-in SVN 작업 사본 패널: 파일 상태 / 로그 / 커밋 UI

### 배경

CLITrigger는 처음부터 git-only로 작성됐지만, 실제 사내·게임 업계엔 여전히 SVN 워킹 카피로 굴러가는 레포가 많다. 단일 워크플로우에 묶어둔 `is_git_repo` 분기와 worktree 격리 가정이 SVN 사용자가 들어올 자리를 막고 있었음. SVN 지원을 추가하되 다음 두 제약을 강하게 둠 — (1) **opt-in per project**: SVN을 안 쓰는 사용자는 추가 UI/라우트/`svn` 프로세스 스폰을 절대 보지 않음 (기본 OFF), (2) **컴포넌트 재사용**: `DiffViewer` / `CommitFileList` 같은 리프 UI는 git/SVN 공통, SVN 매니저가 svn `--xml` 출력을 git porcelain 형태(`index`/`working_dir`)로 매핑해 분기 0.

### 주요 변경

#### 1. DB 스키마: `vcs_type` + `svn_enabled` + `is_svn_wc` (`d872ebf`, `4c935e4`)

기존 `is_git_repo` 단일 boolean을 `vcs_type` (`'git'` | `'svn'` | `null`)로 일반화. 호환성을 위해 `is_git_repo`는 그대로 두고 `vcs_type`을 새로 추가.

- **`src/server/db/schema.ts`**:
  - `projects.vcs_type TEXT` — git/svn 식별자.
  - `projects.svn_enabled INTEGER DEFAULT 0` — opt-in 플래그(기본 OFF, 사용자가 명시적으로 켜야 detection 실행).
  - `projects.is_svn_wc INTEGER DEFAULT 0` — SVN 워킹 카피 마커(후속 커밋 `4c935e4`로 migration 배열 누락분 보정).
  - `is_git_repo=1`인 행을 `vcs_type='git'`로 backfill (NULL 행만 idempotent하게).
- **`src/server/db/queries.ts`** — `Project` 타입에 `vcs_type`/`svn_enabled` 필드 + `createProject()`에 `vcsType` 파라미터 + `updateProject` 화이트리스트에 두 컬럼 추가.

#### 2. `lib/svn.ts` 검출 헬퍼 + `services/svn-manager.ts` (`d872ebf`)

- **`src/server/lib/svn.ts`** (NEW):
  - `isSvnRepository(dir)` — `.svn/` 디렉터리 stat이 1차, `svn info --non-interactive` exit code가 fallback. `svn`이 PATH에 없는 환경에서 안전하게 false 반환.
  - `runSvn(args)` — UTF-8 env (`LC_ALL=en_US.UTF-8`)과 `--non-interactive`를 항상 적용. CJK 파일명 status/log/diff 파싱 보장 + 자격증명 프롬프트로 hang되는 사고 차단. `windowsHide:true`로 Windows에서 콘솔 창 깜빡임 방지.
- **`src/server/services/svn-manager.ts`** (NEW, 329 lines):
  - `getStatus()` / `getInfo()` / `getLog()` / `getDiff()` / `getCommitFiles()` / `getCommitDiff()` — 조회 계열. `svn --xml` 정규식 파싱(별도 XML 의존성 추가 없음).
  - `add()` / `revert()` / `remove()` / `resolve()` / `commit()` / `update()` / `cleanup()` — 변경 계열.
  - **시그니처 호환**: status 결과는 `{path, index: ' ', working_dir: <char>}` 모양으로 내려가서 기존 git porcelain 소비자들이 분기 없이 그대로 렌더 가능. SVN엔 staging이 없으므로 index 슬롯은 항상 공백, status char만 working_dir 슬롯에 들어감.
  - 로그도 `{hash, parentHashes, refs, message, author, date}` 모양으로 매핑(SVN revision number를 hash 슬롯에 문자열로). `refs`는 SVN에 없으므로 빈 배열.

#### 3. `routes/svn.ts` 13개 엔드포인트 + opt-in 게이트 (`d872ebf`)

`POST /api/projects/:id/svn-*` 네임스페이스에 status/info/log/commit-files/commit-diff/diff/add/revert/delete/resolve/commit/update/cleanup 13개 라우트.

- **`src/server/routes/svn.ts`** (NEW, 228 lines):
  - 모든 라우트는 `resolveSvnPath()` 게이트 통과 — `project.svn_enabled && project.vcs_type === 'svn'`이 아니면 400.
  - `?wcPath=` 쿼리는 phase-2 multi-working-copy 격리용 reserve. `nodePath.resolve()` 기반 traversal 가드 — 프로젝트의 `.worktrees` 디렉터리 prefix 안으로만 허용.
  - `revision` 파라미터는 정규식(`/^\d+$/`)으로 검증해 svn 명령어 인젝션 방지.
- **`src/server/index.ts`** — `/api/projects` 라우터에 `svnRouter` 마운트.

#### 4. 프로젝트 라우트: SVN 토글 + lazy detection (`d872ebf`)

- **`src/server/routes/projects.ts`**:
  - `POST /api/projects` — 생성 시점엔 SVN detection을 안 돌림. `vcs_type`은 git이면 `'git'`, 아니면 NULL.
  - `GET /api/projects/:id` — `vcs_type`이 비어있고 `svn_enabled=1`이면 그제서야 `isSvnRepository()` 호출, 결과를 `vcs_type='svn'`로 persist. SVN을 안 쓰는 사용자는 `svn` 프로세스 스폰을 절대 트리거하지 않음.
  - PUT 토글: `svn_enabled` off→on 전환 시 detection 실행, on→off 전환 시 `vcs_type` 초기화.

#### 5. 오케스트레이터: SVN은 max_concurrent=1 강제 (`d872ebf`)

- **`src/server/services/orchestrator.ts`** — `getMaxConcurrent()`에 `if (project.vcs_type === 'svn') return 1` 추가. SVN은 phase 1에서 단일 공유 워킹 카피만 지원하므로 동시 task 실행을 직렬화. session/discussion 경로는 기존 `!is_git_repo` 게이트가 이미 SVN-safe라 추가 변경 불요.

#### 6. CLI 상태: `svn --version` 프로브 + `isVcs` 플래그 (`d872ebf`)

- **`src/server/services/cli-status.ts`** — `VCS_TOOLS = [{tool: 'svn', command: 'svn'}]` 배열 추가. SVN 프로젝트가 하나라도 있을 때만 프로브하고, 응답에서 `svn` 엔트리 포함. `isVcs=true`인 도구는 `model-sync.ts` 트리거를 스킵 (SVN은 모델 베어러가 아님).

#### 7. 클라이언트 UI: `SvnStatusPanel` + 헤더 토글 (`d872ebf`)

- **`src/client/src/components/SvnStatusPanel.tsx`** (NEW, 530 lines):
  - **File Status 뷰**: 멀티 셀렉트 체크박스 + Refresh/Update/Add/Revert/Delete/Resolve 액션 + working diff + Cmd/Ctrl+Enter 커밋 텍스트영역. 컴포넌트 레벨에서는 `DiffViewer` / `CommitFileList`를 그대로 재사용 (git porcelain 모양으로 들어오므로).
  - **History 뷰**: revision 목록 + 커밋 상세 file list + diff. revision은 hash 슬롯에 문자열 형태로 들어가 같은 컴포넌트가 처리.
  - `svn --version`이 실패하면(=SVN 미설치) install 안내 배너.
- **`src/client/src/api/svn.ts`** (NEW) — 타입 래퍼, 기존 `CommitFile`/`GitLogResult` 타입 재사용.
- **`src/client/src/components/ProjectHeader.tsx`** — `!project.is_git_repo`인 경우에만 SVN 설정 카드 노출 + Enable 체크박스.
- **`src/client/src/components/ProjectDetail.tsx`** — 탭 조건 `svn_enabled && vcs_type === 'svn'`일 때만 SVN 탭 렌더.
- **`src/client/src/i18n.tsx`** — `tabs.svn`, `svn.*` 라벨, `header.svnTitle/svnHint/svnEnable` EN/KO 키.

#### 8. is_svn_wc migration entry 누락 보정 (`4c935e4`)

`d872ebf`에서 `projects.is_svn_wc` 컬럼이 코드에선 등록됐지만 schema.ts의 `migrations` 배열에 빠져 있어 기존 DB 업그레이드 시 컬럼이 안 만들어짐. 후속 fix 커밋으로 한 줄 추가.

- **`src/server/db/schema.ts`** — `migrations` 배열에 `{ table: 'projects', column: 'is_svn_wc', definition: 'INTEGER DEFAULT 0' }` 추가.

### 수정된 주요 파일

| 파일 | 변경 내용 |
|------|----------|
| `src/server/db/schema.ts` | `vcs_type` + `svn_enabled` + `is_svn_wc` migration entry; `is_git_repo=1` → `vcs_type='git'` backfill |
| `src/server/db/queries.ts` | `Project` 타입 + `createProject(vcsType)` 파라미터 + `updateProject` 화이트리스트 |
| `src/server/lib/svn.ts` | NEW — `isSvnRepository()` (.svn dir + `svn info` fallback) + `runSvn` UTF-8/`--non-interactive` 헬퍼 |
| `src/server/services/svn-manager.ts` | NEW — 13개 메서드, `svn --xml` 정규식 파싱, git porcelain 모양으로 매핑 |
| `src/server/routes/svn.ts` | NEW — 13 엔드포인트, opt-in 게이트, traversal 가드, revision 정규식 검증 |
| `src/server/routes/projects.ts` | 생성 시 SVN detection 스킵; `GET`에서 svn_enabled=1 + vcs_type 미설정 시 lazy detect |
| `src/server/services/orchestrator.ts` | SVN 프로젝트는 `max_concurrent=1` 강제 |
| `src/server/services/cli-status.ts` | `VCS_TOOLS` 배열 + `isVcs` 플래그(model-sync 스킵), 프로젝트가 svn_enabled일 때만 프로브 |
| `src/client/src/components/SvnStatusPanel.tsx` | NEW — File Status / History 뷰, `DiffViewer`/`CommitFileList` 재사용, svn 미설치 배너 |
| `src/client/src/components/ProjectHeader.tsx` | `!is_git_repo`인 프로젝트에만 SVN Enable 체크박스 카드 |
| `src/client/src/components/ProjectDetail.tsx` | SVN 탭 조건부 렌더 |
| `src/client/src/api/svn.ts` | NEW — typed wrapper |

### 아키텍처 결정

1. **opt-in per project**: SVN을 안 쓰는 사용자가 압도적 다수이므로 detection을 lazy하게(`svn_enabled=1` 토글 후에만) 돌림. 프로젝트 생성 시점엔 git만 검사. 라우트도 `svn_enabled && vcs_type==='svn'` 게이트로 닫혀 있어 SVN을 안 켠 프로젝트엔 svn 라우트 자체가 400을 반환하고 `svn` 프로세스가 절대 스폰되지 않음.
2. **git porcelain 모양으로 정규화**: `svn-manager`가 `--xml` 출력을 파싱하면서 `{path, index: ' ', working_dir: <char>}` 형태로 매핑. SVN에 staging이 없으므로 index는 공백 고정, working_dir 슬롯에 SVN status char가 들어감. 기존 `DiffViewer`/`CommitFileList`/`GitStatusPanel` 패턴이 SVN에도 그대로 통하므로 리프 UI 분기 0. 로그도 revision number를 hash 슬롯에 문자열로 매핑해서 같은 컴포넌트가 처리.
3. **`svn --xml` + 정규식 vs XML 파서 의존성**: SVN은 `--xml` 출력 스키마가 매우 안정적이고 변경이 거의 없어 별도 XML 파서 의존성을 추가하기엔 비용이 크다. 정규식으로 충분히 처리 가능.
4. **`is_git_repo` 그대로 유지**: 기존 코드의 `if (!project.is_git_repo)` 분기가 worktree-manager / orchestrator / 라우트에 박혀 있어 한 번에 vcs_type으로 마이그레이션하면 위험. `vcs_type`을 추가만 하고 `is_git_repo`는 git 식별용으로 그대로 둠. 새 코드(SVN 라우트, orchestrator의 SVN 분기)에서만 `vcs_type` 사용.
5. **SVN은 직렬 실행**: SVN에 git worktree에 해당하는 격리 메커니즘이 없어(checkout copy는 phase-2로 미룸) phase 1은 단일 공유 워킹 카피에서 직렬 실행. orchestrator의 `getMaxConcurrent`에 한 줄 분기로 처리.
6. **`isVcs` 플래그로 model-sync 분리**: `cli-status.ts`가 동시에 도구 설치 체크 + 모델 동기화 트리거 두 가지 책임을 가짐. SVN은 모델 베어러가 아니므로 model-sync는 스킵해야 하는데, 이를 도구 메타데이터에 `isVcs` 플래그로 표현. 향후 다른 VCS(Mercurial, Fossil 등)을 추가할 때 같은 플래그 재사용 가능.

### 후속 검증 필요

- SVN 1.7+ working-copy 레이아웃(`.svn/` 단일 루트 디렉터리)만 지원. 1.6 이하의 per-directory `.svn/`는 detection은 통과해도 일부 명령이 다르게 동작할 수 있음 — 실 사용자 신고 시 보강.
- Multi-working-copy(`.worktrees/<wc>/` 격리)는 phase-2로 분리. 현재 라우트의 `?wcPath=` 쿼리는 검증 로직만 들어가 있고 클라이언트 UI는 단일 워킹 카피만 지원.

---
