# WikiLink Stale ID 자동 복구 스펙

**문서 번호**: SPEC-260223-001
**작성일**: 2026-02-23
**관련 커밋**: `d5844f7`
**상태**: ✅ 구현 완료

---

## 1. 문제 정의 (Problem Statement)

### 1.1 배경

InsuWiki의 위키링크(`[[제목]]`)는 생성 시 해당 문서의 `docId`를 마크다운에 하드코딩하여 저장한다.

```markdown
<!-- 저장 형태 예시 -->
[abc](/docs/MY_ABC_DOCID)
```

### 1.2 문제 시나리오

**Wiki** 탭과 **My** 탭에 동일 제목(`abc`)의 문서가 공존할 경우:

1. `useWikiMap`은 **My 문서를 우선** 등록 (public → my 순서로 `Map.set()` 수행, 나중에 쓴 값이 덮어씀)
2. 다른 문서 `eee`에서 `[[abc]]` 생성 시 → My abc의 `docId`가 마크다운에 저장됨
3. **My abc 삭제 후** `eee`에서 `[[abc]]` 클릭 → 저장된 ID는 여전히 My abc의 ID → `404` 또는 삭제된 문서 표시

### 1.3 영향 범위

| 케이스 | 영향 |
|--------|------|
| My 문서 삭제 | 이전에 해당 문서로 연결된 모든 위키링크가 404 |
| My 문서를 공개 전환한 뒤 재작성 | 이전 링크가 새 ID를 추적하지 못함 |
| 동명 문서 이전(docId 변경) | 동일 |

---

## 2. 해결 방안 (Solution)

### 2.1 설계 원칙

- **마크다운 저장 포맷 변경 없음**: 기존 `[label](/docs/id)` 형태 유지 (하위 호환성)
- **런타임 동적 교정**: 뷰어 접근 시 + 에디터 열었을 때 실시간으로 stale ID를 탐지·교체
- **사용자 게입 불필요**: 자동으로 처리되어야 함

### 2.2 Fix 1 — 뷰어 접근 시 자동 리다이렉트 (DocumentClient)

**파일**: `nextapp/src/app/docs/[id]/DocumentClient.tsx`

**동작**:
1. 문서 로딩 후 `isDeleted === true` 감지
2. `wikiMap`에서 동일 `title`의 살아있는 문서 ID 탐색
3. 존재하면 `router.replace()`로 즉시 전환 (브라우저 히스토리 오염 없음)

```typescript
useEffect(() => {
    if (!document || !wikiMap || wikiMap.size === 0) return;
    if (!document.isDeleted) return;
    const title = document.title?.trim();
    if (!title) return;
    const redirectId = wikiMap.get(title);
    if (redirectId && redirectId !== docId) {
        router.replace(`/docs/${redirectId}`);
    }
}, [document, wikiMap, docId, router]);
```

**커버리지**: 뷰어에서 링크 클릭 시 즉각 복구. 편집자·독자 모두 혜택.

### 2.3 Fix 2 — 에디터 내 Stale 노드 자동 교정 (ReflectEditor)

**파일**: `nextapp/src/components/ReflectEditor.tsx`

**동작**:
1. `wikiMap`이 갱신될 때마다 에디터 내 `wikiLink` 노드 전체 순회
2. `node.attrs.id`가 `validIds`(현재 wikiMap 값들)에 없으면 stale 판정
3. `node.attrs.label`(제목)로 `wikiMap`에서 최신 `id` 조회
4. 유효한 최신 ID가 있으면 해당 노드를 단일 트랜잭션으로 일괄 교정
5. 다음 저장(💾) 시 교정된 ID가 마크다운에 영구 반영

```typescript
useEffect(() => {
    if (!editor || editor.isDestroyed || wikiMap.size === 0) return;
    const validIds = new Set(wikiMap.values());
    const patches = [];

    editor.state.doc.descendants((node, pos) => {
        if (node.type.name !== 'wikiLink') return;
        if (!node.attrs.id || validIds.has(node.attrs.id)) return;
        const freshId = wikiMap.get((node.attrs.label || '').trim());
        if (freshId) patches.push({ pos, newId: freshId, attrs: node.attrs });
    });

    if (!patches.length) return;
    const tr = editor.state.tr;
    patches.reverse().forEach(({ pos, newId, attrs }) =>
        tr.setNodeMarkup(pos, null, { ...attrs, id: newId })
    );
    editor.view.dispatch(tr);
}, [wikiMap, editor]);
```

**커버리지**: 에디터를 열었을 때 자동 교정. 저장하면 파일에 반영.

---

## 3. 복구 흐름 다이어그램

```
[사용자 클릭: [[abc]] → /docs/OLD_ID]
        │
        ▼
DocumentClient 로드
        │
   isDeleted?
    YES │           NO
        │           │
  wikiMap.get('abc')  정상 표시
        │
   새 ID 있음?
    YES │           NO
        │           │
router.replace()    삭제됨 안내
→ 새 문서로 이동
```

```
[에디터 열기 / wikiMap 갱신]
        │
ReflectEditor useEffect 실행
        │
wikiLink 노드 순회
        │
   stale ID 검출?
    YES            NO
        │           │
wikiMap.get(label)  스킵
→ 새 ID로 교정
→ 다음 저장 시 반영
```

---

## 4. 한계 및 미해결 이슈

| 이슈 | 설명 | 향후 방안 |
|------|------|----------|
| 동명 대체 문서 없는 경우 | wikiMap에 해당 제목이 없으면 리다이렉트 불가, 삭제 문서 표시 유지 | "관련 문서 검색" UX 추가 |
| 에디터 저장 없으면 미반영 | Fix 2는 에디터에서만 교정, 저장하지 않으면 DB에 반영 안 됨 | 백그라운드 자동저장 또는 배치 스크립트 고려 |
| 뷰어에서 Fix 2 미적용 | 뷰어는 읽기 전용이므로 에디터 교정 불가 | Fix 1(리다이렉트)이 뷰어 커버 |

---

## 5. 관련 파일

| 파일 | 변경 내용 |
|------|----------|
| `src/app/docs/[id]/DocumentClient.tsx` | `useWikiMap` import·훅 추가, stale 리다이렉트 Effect |
| `src/components/ReflectEditor.tsx` | stale wikiLink 노드 자동 교정 Effect |
| `src/hooks/useWikiMap.ts` | 변경 없음 (기존 캐시·리프레시 동작 재사용) |

---

## 6. 테스트 시나리오

| # | 시나리오 | 기대 결과 |
|---|---------|----------|
| T-1 | My abc 삭제 → eee 문서에서 [[abc]] 클릭 | Wiki abc로 자동 리다이렉트 |
| T-2 | My abc 삭제 → eee 에디터 열기 | [[abc]] 노드 ID가 Wiki abc ID로 교정됨 |
| T-3 | 대체 문서도 없는 경우 | 삭제 문서 안내 유지 (무한 리다이렉트 없음) |
| T-4 | 삭제 안 된 정상 문서 | 리다이렉트 없이 정상 표시 |
