# 옵시디언-식 그래프 구조 적용 플랜

> 옵시디언(Obsidian) 앱을 도입하는 것이 아니라, 옵시디언이 사용하는 **데이터 패턴**(마크다운 노트 + YAML frontmatter + `[[wikilinks]]` + `#tags` + 그래프 탐색)을 현재 RAG 파이프라인 안에 흡수해 P&ID/DXF/PDF만으로 부족한 지식 표현을 보강하는 것이 목적이다.

작성: 2026-05-14 | 대상 스택: vLLM Qwen3.6-27B-FP8 + Ollama nomic-embed-text(768) + Qdrant + PostgreSQL + KbIngestWorker(C#) + MCP(Python)

---

## 1. 배경 — 왜 지금 그래프 구조가 필요한가

### 1.1 현재 한계

| 영역 | 현재 동작 | 한계 |
|------|----------|------|
| P&ID / DXF | `parse_pid_dxf`, `parse_pid_pdf`로 태그/심볼/선 추출 → PostgreSQL `pid_*` 테이블 적재 | 추출 정확도가 도면 품질에 종속. LLM이 그림을 "읽지" 못해 27B로도 흐름/인과 추론이 약함. 누락된 관계는 영원히 누락 |
| PDF/Excel KB | `parse_document`로 텍스트 청크 → 벡터 임베딩 → Qdrant 5개 컬렉션 | 청크는 의미 단위가 아닌 페이지/시트/표 단위. 청크 간 관계(이 절차는 저 알람과 연결됨)는 모델이 매번 "발견"해야 함 |
| 채팅 RAG | `search_kb`/`rag_query`로 top-k 청크 retrieval | 의미적으로 가까운 청크는 잘 찾지만 **명시적 연결**이 없는 항목(같은 루프의 다른 태그, 같은 area의 작년 이벤트)은 안 따라옴 |
| 현장 지식 | 운전원 머릿속 / Excel / Word | 검색·연결·LLM 주입이 불가 |

### 1.2 27B 모델의 현실

`Qwen3.6-27B-FP8` 단일 GPU 서빙. 한 답변에 토큰 4–6k 정도가 안정 구간. 따라서:

- **검색기가 똑똑할수록 LLM 부담이 줄어든다** — 청크 100개 던지고 LLM에게 정리시키면 토큰·지연 폭증. 청크 5–10개 + **관계 그래프 1-hop**을 던지는 게 훨씬 싸다.
- **그래프 탐색은 LLM 추론이 아니라 인덱스 조회로 해결**해야 한다. GraphRAG처럼 LLM으로 엔티티/관계를 추출하는 무거운 방식은 우리 GPU에서 비용이 크다.

### 1.3 옵시디언 패턴이 잘 맞는 이유

- **마크다운 = LLM 친화** — 토큰화 효율, 헤딩 기반 청킹 자연스러움
- **`[[wikilinks]]` = 사람이 손으로 만든 명시적 엣지** — LLM이 추출할 필요 없음. 운전원/엔지니어가 노트 쓰면서 자동으로 그래프가 자란다
- **YAML frontmatter = 구조화 메타데이터** — `area`, `loop`, `vendor`, `range` 같은 필드가 노트 수준에서 일관됨
- **`#tags` = 가로 분류** — 폴더 계층과 무관한 횡적 묶음
- 모두 **로컬 텍스트 파일**로 표현 가능 → 백업/git/diff 자유롭고 KB 파이프라인에 그대로 흘려 넣을 수 있음

---

## 2. 채택할 것 / 채택하지 않을 것 (스코프 선)

### 채택

- 노트 = 마크다운 1개 파일 + YAML frontmatter
- `[[note-id]]` 위키링크 문법 — id는 안정적이고 URL-safe
- `#tag` 인라인 태그 + frontmatter `tags:` 둘 다 인정
- 노트 간 **유향 그래프** (`source_note` → `target_note`, `link_type`)
- 그래프 1–3 hop 탐색 도구 + 벡터 검색과의 하이브리드

### 채택하지 않음

- ❌ Obsidian 앱 실행 / `.obsidian/` 설정 / plugin 호환
- ❌ Obsidian Sync · Publish
- ❌ Canvas, Excalidraw 등 GUI 부속
- ❌ "Vault 폴더 → 자동 동기화" 패턴 (파일 워처) — 현재 KB는 업로드형 큐 모델이고 일관성 유지에 유리. 동기화는 단방향 임포트 도구로 충분
- ❌ LLM으로 자동 엔티티/관계 추출 (GraphRAG 식) — 27B 처리량 부담. 대신 **결정론적 추출**(정규식·frontmatter·기존 P&ID 파서 결과) + 사용자 작성을 1차로 한다

→ 옵시디언으로 노트 편집하고 싶은 사용자는 `.md` 파일을 외부에서 옵시디언으로 열어도 호환된다. 그것은 **부수효과**이지 의존 관계가 아니다.

---

## 3. 데이터 모델

### 3.1 노트 ID 규칙

`{kind}/{slug}` 형식. 예:

```
tag/fic-6113
loop/compression-a
area/unit-a
drawing/pid-105
procedure/start-up-a
vendor/honeywell-c300
event-pattern/surge-recovery
```

- 모두 소문자, kebab-case
- `tag/`, `loop/`, `area/`, `drawing/`, `procedure/`, `vendor/`, `event-pattern/`, `term/`, `kpi/` 9종 (시드, 확장 가능)
- 위키링크 본문에선 `[[tag/fic-6113]]` 또는 별칭 `[[tag/fic-6113|FIC-6113]]`

### 3.2 Frontmatter 스키마 (공통 + kind별)

공통:

```yaml
---
id: tag/fic-6113           # 필수, 위 ID 규칙
title: FIC-6113 (압축단 1열 입구 유량)
kind: tag                  # 필수, 9종 중 하나
tags: [flow, area-a, critical]
aliases: [FIC6113, fic-6113.pv]
created: 2026-05-14
updated: 2026-05-14
sources:                   # 선택 — 이 노트의 근거 문서
  - kb_doc_id: 1f3a...     # 기존 kb_documents UUID
    locator: "p.12 §3.2"
---
```

kind별 추가 필드 (선택, frontmatter라 유연):

| kind | 권장 필드 |
|------|----------|
| `tag` | `pv_tag`, `sp_tag`, `op_tag`, `unit`, `range_min`, `range_max`, `eu`, `area`, `loop` |
| `loop` | `area`, `controllers`, `actuators`, `transmitters`, `setpoint_strategy` |
| `area` | `unit`, `manager`, `process_description` |
| `drawing` | `drawing_number`, `revision`, `pid_doc_id` (기존 pid_drawings.id 연결) |
| `procedure` | `applicable_loops`, `last_review`, `owner` |
| `vendor` | `manufacturer`, `model`, `firmware` |
| `event-pattern` | `event_types`, `area`, `mitigation` |

### 3.3 본문 컨벤션

```markdown
# FIC-6113

압축기 A열 입구 유량 제어. [[loop/compression-a]] 의 1차 제어 변수.

## 정상 운전
- SP: 120 t/h (보통 115–125 범위)
- 알람 상한: 135 t/h → 트립: 145 t/h
- 상위 cascade: [[tag/pic-6101]]

## 관련 이벤트 패턴
- [[event-pattern/surge-recovery]] — 빠르게 떨어지면 ESV 확인
- 정기점검 후 첫 기동 시 잦은 흔들림 → [[procedure/start-up-a]] 단계 7 참조

## 참고
- 도면: [[drawing/pid-105]]
- 벤더: [[vendor/honeywell-c300]]
```

- 헤딩 ≤ H3 권장 → 청킹 단위
- 위키링크 `[[id]]` 또는 `[[id|alias]]`
- 인라인 태그 `#flow #critical` 도 허용 (frontmatter `tags:`에 자동 병합)

### 3.4 PostgreSQL 스키마 (신규 5개 테이블)

기존 KB 파이프라인과 **분리된 네임스페이스**(`vault_*` prefix). `kb_documents`는 원본 파일(PDF/Excel)용 그대로 유지, vault는 별도 트랙.

```sql
-- 노트 본체
CREATE TABLE vault_notes (
  id              TEXT PRIMARY KEY,              -- "tag/fic-6113"
  kind            TEXT NOT NULL,                  -- 9종 중 하나
  title           TEXT NOT NULL,
  body_md         TEXT NOT NULL,                  -- 원본 마크다운
  frontmatter     JSONB NOT NULL DEFAULT '{}',
  content_hash    TEXT NOT NULL,                  -- SHA256(body_md+frontmatter)
  status          TEXT NOT NULL DEFAULT 'pending',-- pending/embedding/indexed/failed
  error_message   TEXT,
  source_doc_id   UUID REFERENCES kb_documents(id),-- 임포트된 경우
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX ON vault_notes (kind);
CREATE INDEX ON vault_notes USING gin (frontmatter);

-- 별칭 (id ↔ alias 양방향)
CREATE TABLE vault_aliases (
  alias       TEXT PRIMARY KEY,                   -- 소문자 정규화
  note_id     TEXT NOT NULL REFERENCES vault_notes(id) ON DELETE CASCADE
);

-- 태그 (frontmatter + 인라인 #tag 병합 결과)
CREATE TABLE vault_tags (
  note_id     TEXT NOT NULL REFERENCES vault_notes(id) ON DELETE CASCADE,
  tag         TEXT NOT NULL,
  PRIMARY KEY (note_id, tag)
);
CREATE INDEX ON vault_tags (tag);

-- 위키링크 (유향 엣지)
CREATE TABLE vault_links (
  src_note_id   TEXT NOT NULL REFERENCES vault_notes(id) ON DELETE CASCADE,
  dst_note_id   TEXT NOT NULL,                    -- 미존재 노트도 허용(빨간 링크)
  dst_resolved  BOOLEAN NOT NULL DEFAULT FALSE,   -- 타겟 노트가 실제 존재?
  link_type     TEXT NOT NULL DEFAULT 'wikilink', -- 향후 'cascade','vendor-of' 등 확장
  occurrences   INT NOT NULL DEFAULT 1,            -- 같은 노트에서 N회 등장
  PRIMARY KEY (src_note_id, dst_note_id, link_type)
);
CREATE INDEX ON vault_links (dst_note_id);

-- 청크 (Qdrant point_id ↔ 노트/헤딩 매핑)
CREATE TABLE vault_chunks (
  point_id      UUID PRIMARY KEY,                  -- Qdrant point_id
  note_id       TEXT NOT NULL REFERENCES vault_notes(id) ON DELETE CASCADE,
  heading_path  TEXT,                              -- "## 정상 운전 > ### 알람"
  chunk_text    TEXT NOT NULL,
  token_count   INT
);
CREATE INDEX ON vault_chunks (note_id);
```

`dst_resolved=false`인 링크는 "아직 노트가 안 만들어진 빨간 링크" — 자라야 할 그래프의 빈자리를 가시화한다.

### 3.5 Qdrant 컬렉션

신규 1개 — `vault` (768-dim cosine, payload만 인덱스: `note_id`, `kind`, `tags`, `area`, `loop`).

기존 `kb_*` 5개와 **공존**. `search_kb`는 그대로, `search_vault`는 신규(아래 §6).

---

## 4. 인덱싱 파이프라인

### 4.1 입력 경로 3가지

| 경로 | 동작 |
|------|------|
| **A. UI에서 마크다운 직접 작성/수정** | 14번 탭 확장. 저장 시 `vault_notes` UPSERT + 인덱싱 큐 적재 |
| **B. zip/폴더 임포트** | `.md` 파일들을 zip으로 업로드 → 일괄 파싱·검증·노트 생성 |
| **C. 기존 데이터 자동 시드** | `node_map_master`/`pid_tags`/`pid_drawings`/`kb_collections` 등에서 자동으로 노트 스켈레톤 생성 (§7.2 별도 절) |

세 경로 모두 동일한 **MarkdownNoteWorker**(C# `BackgroundService`, 기존 `KbIngestWorker` 패턴 차용)로 수렴.

### 4.2 워커 단계 (단일 패스)

```
[큐: vault_notes WHERE status='pending']
  ↓
1. 파싱 (server.py 또는 C# Markdig)
   - frontmatter 추출 → JSON 검증
   - 본문에서 [[id]] / [[id|alias]] / #tag 추출
   - 헤딩 트리 추출 → 청크 분할 (≤512 토큰, 헤딩 경계 우선)
  ↓
2. 정규화
   - aliases 소문자화, 중복 제거
   - 위키링크 id의 alias 해석 (vault_aliases 조회)
   - 자기 자신 링크 제거
  ↓
3. 그래프 갱신 (트랜잭션)
   - vault_links 기존 행 삭제 후 재삽입
   - dst_resolved 계산: dst가 vault_notes에 존재 OR alias에 존재
   - 신규 노트가 들어오면 기존의 'unresolved' 링크들을 다시 resolve 시도
   - vault_tags 동기화
  ↓
4. 임베딩 (Ollama /api/embeddings, 청크별)
   - 실패 청크는 skip + error_message에 누적, attempts<3
  ↓
5. Qdrant upsert
   - 기존 doc의 point들 먼저 delete(filter: note_id)
   - 신규 청크 일괄 upsert
  ↓
6. status='indexed' or 'failed'
```

부분 실패 정책은 기존 KbIngestWorker와 동일(전부 실패 시 failed, 일부 실패 시 indexed + error_message).

### 4.3 파싱 위치 선택

- **C# 측 Markdig + 자체 정규식** — 의존성 단순. frontmatter는 `YamlDotNet`.
- 또는 **MCP server.py에 `parse_markdown_note` 도구 추가** — Python `markdown-it-py` + `python-frontmatter` 사용, 기존 parsers/ 구조와 일관.

권장: **server.py에 추가**. 이유: 기존 `parse_document`/`parse_pid_*`와 같은 파이프라인이고, 위키링크·태그 추출 같은 텍스트 처리는 Python이 더 쉬움. C# Worker는 큐 관리·Qdrant 호출만 담당.

---

## 5. 하이브리드 검색 (핵심 차별점)

### 5.1 단계

```
질의 q
  ↓
A. 벡터 검색 (Qdrant vault collection, top_k=10)
   → 시드 노트 집합 S = { note_id of top hits }
  ↓
B. 그래프 확장 (PostgreSQL, 1-hop, 옵션 2-hop)
   → for each s in S:
        out_links  = vault_links WHERE src=s AND dst_resolved
        in_links   = vault_links WHERE dst=s
   → 확장 집합 E = S ∪ neighbors
  ↓
C. 재순위 (rule-based, LLM 호출 없음)
   - 벡터 점수 + boost(같은 area), boost(같은 loop)
   - kind 우선순위 가중 (질의에 "절차" 포함 시 procedure 가중)
  ↓
D. 청크 선택 + 컨텍스트 패킹
   - 시드 노트는 top-2 청크
   - 이웃 노트는 frontmatter + 첫 문단(요약)만
   - 총 토큰 ≤ 3000 budget
  ↓
LLM에 주입
```

핵심: **그래프 확장은 PostgreSQL JOIN 한두 번**. LLM 호출 0회. 27B GPU 부담 없음.

### 5.2 옵시디언의 "백링크"가 주는 가치

특정 태그 노트(`tag/fic-6113`)에 대한 백링크 목록 = 그 태그를 언급한 모든 절차/이벤트패턴/도면. 운전원이 "FIC-6113 이상해" 라고 물으면:

1. 벡터 검색이 `tag/fic-6113` 노트를 시드로 잡고
2. 백링크로 `[[procedure/start-up-a]]`, `[[event-pattern/surge-recovery]]` 가 따라붙고
3. cascade frontmatter로 `[[tag/pic-6101]]`도 자동 추가

→ 27B에게 던지는 컨텍스트가 **의미 검색만으로는 절대 못 모을 조합**으로 채워진다.

---

## 6. MCP 도구 (신규)

`server.py`에 추가. 모두 PostgreSQL/Qdrant 조회 — LLM 호출 없음.

| 도구 | 시그니처 | 동작 |
|------|---------|------|
| `vault_search` | `(query, kinds?, tags?, top_k=8, expand_hops=1)` | §5의 하이브리드 검색. 시드 + 확장 노트 목록 + 청크 반환 |
| `vault_get_note` | `(note_id, include_body=True)` | 단일 노트 전체. 헤딩 트리·frontmatter·in/out 링크 카운트 |
| `vault_backlinks` | `(note_id, limit=50)` | 이 노트를 가리키는 모든 src 노트 (`vault_links` WHERE dst) |
| `vault_neighbors` | `(note_id, hops=1, kinds?)` | 1–3 hop 이웃. 시각화·운전원 질문 "이 루프에 뭐 있어" |
| `vault_by_tag` | `(tag, kind?, limit=50)` | 태그 매칭 노트 목록 |
| `vault_unresolved` | `(limit=50)` | dst_resolved=false 링크 리스트 — "비어있는 노트" 진단 |
| `vault_update_note` | `(note_id, body_md, frontmatter)` | (admin) 노트 upsert. 큐 적재. 운전원이 채팅 중 "이거 노트로 저장" 흐름 위해 |

채팅 의도 라우터(`_classify_intent`)에도 규칙 추가:

```
"노트|이 태그.*정보|관련.*뭐|연결" → vault_search
"백링크|어디서.*언급" → vault_backlinks
```

`ToolGuideKo`에 위 도구들 추가, `rag_query` 확장 옵션 `use_vault=True` (벡터-only 모드와 토글 가능).

---

## 7. UI (14번 탭 확장 + 신규 카드 / 신규 탭은 보류)

> 신규 탭(15번)을 또 만들기보다 **14번 "RAG 관리" 탭 안에 두 번째 모드 토글**을 두는 게 코드/사용자 인지부하 면에서 깔끔하다.

### 7.1 14번 탭 — 모드 전환

```
[원본 문서] | [Vault 노트]   ← 토글
```

### 7.2 Vault 노트 모드 화면

- 좌측: kind 필터(tag/loop/area/...) + 태그 facet + 빨간 링크 카운트
- 중앙: 노트 목록 (검색·정렬·일괄 선택)
- 우측: 단일 노트 뷰
  - 상단: frontmatter 폼 (수정 가능, 저장 시 큐 적재)
  - 가운데: 마크다운 편집기 (textarea + 프리뷰 토글, [[ 자동완성)
  - 하단: "백링크 (N)" / "이웃 그래프 미니뷰" (SVG 간단한 force-layout — 100 노드 미만, 그 이상은 리스트로 fallback)

### 7.3 채팅 통합

- `vault_search` 결과 카드: 노트 제목 클릭 시 14번 탭의 해당 노트로 점프
- `[[id]]` 토큰이 답변에 등장하면 자동으로 다운로드/노트뷰 링크 치환 (현재 `llmLinkKbCitations`와 동일 패턴 확장)

신규 라이브러리는 도입하지 않는다 — 마크다운 렌더는 기존이 있으면 재사용, 없으면 textarea + 단순 `marked` CDN 한 줄.

---

## 8. P&ID/DXF 한계 해결 — 자동 시드 + 운전원 보강

### 8.1 자동 시드 (1회성 마이그레이션 잡)

기존 PostgreSQL 데이터에서 결정론적으로 노트를 만든다 (LLM 0회):

| 소스 | 생성될 노트 | frontmatter 자동 채움 |
|------|------------|-----------------------|
| `node_map_master` | `tag/{base-tag}` 각 1개 | `pv_tag`, `sp_tag`, `op_tag`, `unit`, `range_*`, `eu` |
| `pid_drawings` | `drawing/{drawing_number}` | `revision`, `pid_doc_id` |
| `pid_tags` (drawing 안의 태그) | drawing 노트 본문에 `[[tag/...]]` 위키링크로 자동 삽입 | — |
| `kb_collections` 시드 5종 | `term/system-instrument` 등 | — |
| `event_history_table` 빈도 상위 패턴 | (보류 — LLM 없이 군집화 어려움) | — |

→ 첫 임포트로 **수천 개 노트가 빨간 링크 없는 상태**로 깔린다. P&ID에서 못 잡은 관계는 운전원이 노트 본문에 `[[tag/...]]` 한 줄 추가로 채운다. 채울수록 그래프가 풍부해진다.

### 8.2 DXF/PDF는 그대로 두되 결과를 노트로 발행

`parse_pid_dxf` 결과는 지금처럼 `pid_*` 테이블에 그대로. 거기에 더해:

- drawing 1개 = 노트 1개 (자동 생성, 본문은 태그 리스트 + 도면 파일 임베드 링크)
- DXF 파싱이 인식한 태그 = 해당 drawing 노트 본문에 위키링크
- 인식 못 한 영역은 노트 본문 하단에 `## TODO` 섹션 자동 생성 → 운전원이 채움

**LLM이 도면을 "읽을" 필요가 없어진다.** 운전원이 노트로 옮긴 정보만 LLM이 본다.

---

## 9. 하드웨어/성능 예측

가정: 자동 시드 후 노트 ~3,000개, 평균 본문 1.5KB, 청크 평균 3개/노트 → 청크 ~9,000개.

| 자원 | 추정 | 비고 |
|------|------|------|
| PostgreSQL 디스크 | < 50 MB | vault_notes(본문) + 인덱스 |
| Qdrant 메모리 | 9,000 × 768 × 4byte ≈ 28 MB + HNSW 오버헤드 ≈ **< 100 MB** | 기존 ws-/experion-opc-docs와 별도 |
| 임베딩 초기 비용 | 9,000 청크 × nomic-embed-text | CPU/GPU에 따라 다르지만 단발성. 이후 변경된 노트만 |
| 검색 (벡터+1hop) | Qdrant 10ms + PG 5ms ≈ **< 50ms** | LLM 호출 없음 |
| 검색 (벡터+2hop) | PG가 노드 수 폭발 가능 → **2-hop은 옵션 + 노드 cap=200** | |
| LLM 컨텍스트 증가 | 시드 청크 5 + 이웃 메타 10개 ≈ **+1.5k 토큰** | 27B 컨텍스트 32k 한계 내 안전 |
| 동시성 | 워커 1개 단일 패스 (기존 정책 유지) | 임베딩 큐가 LLM과 GPU 충돌 시: **임베딩은 Ollama(별도 프로세스/CPU 가능)** 이므로 vLLM과 분리 |

→ **하드웨어 추가 부담 거의 없음**. 가장 큰 비용은 초기 임베딩 1회.

---

## 10. Phase 분할 (현실적 단위)

### Phase A — 데이터 모델 + 워커 골격 (변경 최소, 채팅 영향 없음)
- `vault_*` 테이블 5개 DDL (ExperionDbContext.InitializeAsync에 추가, 기존 KB DDL 패턴 답습 — `NpgsqlConnection.ExecuteNonQueryAsync` 사용해 중괄호 이슈 회피)
- `VaultNote` / `VaultLink` / `VaultTag` / `VaultChunk` 엔티티
- `IVaultService` 인터페이스 + 구현 (CRUD + 큐 적재)
- `MarkdownNoteWorker` BackgroundService (parse → embed → index)
- `server.py`에 `parse_markdown_note` MCP 도구
- 검증: 손으로 `.md` 1개 업로드 → `indexed` 도달 + Qdrant 점 생성 확인

### Phase B — MCP 도구 + 채팅 연결
- `vault_search` / `vault_get_note` / `vault_backlinks` / `vault_neighbors` / `vault_by_tag` / `vault_unresolved`
- `ToolGuideKo` 갱신
- 의도 라우터 규칙 추가
- 채팅 결과 카드에 노트 인용 렌더링 (`llmLinkVaultCitations`)
- 검증: 채팅에서 "FIC-6113 관련 노트 보여줘" → 도구 호출 + 카드 표시

### Phase C — UI (14번 탭 확장)
- 모드 토글, 노트 목록, 노트 뷰/편집, frontmatter 폼
- 빨간 링크 진단 패널
- 이웃 그래프 미니뷰 (간단 SVG)
- zip 임포트 엔드포인트

### Phase D — 자동 시드 마이그레이션
- `node_map_master` → tag 노트 생성 잡
- `pid_drawings` + `pid_tags` → drawing 노트 + 위키링크
- 1회성 admin-only 엔드포인트 `POST /api/vault/seed` (dry-run 옵션 포함)

### Phase E (선택) — 점진적 자동화
- 채팅에서 LLM이 답변하면서 새 fact를 발견하면 "이걸 [[tag/...]] 노트에 추가할까요?" 제안 → 운전원 확인 시 `vault_update_note` 호출
- 운영자가 일정 누르면 "지난주 자주 등장한 unresolved 링크 N개 — 노트 만들기" 작업 큐

Phase A–B는 1–2일, C는 1일, D는 0.5일, E는 보류 가능.

---

## 11. 잔여 결정사항 (사용자 확인 필요)

| 항목 | 옵션 | 코멘트 |
|------|------|--------|
| 노트 ID 충돌 시 | (1) 거부 (2) 버전 suffix | 기본 (1) 권장 |
| `.md` 외부 편집 허용 여부 | (1) UI 전용 (2) 파일 export/import 양방향 | 양방향은 동기화 충돌 비용 큼. (1) 권장. 필요 시 read-only export만 |
| 옵시디언 앱과 호환 (부수효과) | (1) frontmatter 키 호환 (2) Obsidian 전용 키(`cssclasses` 등) 무시 | (1) — 우리가 정의한 키 외엔 그냥 frontmatter에 보존만 하면 됨 |
| 이웃 그래프 시각화 라이브러리 | (1) 직접 SVG (2) Cytoscape.js (3) vis-network | (1) 노드 ≤ 100 가정. 그 이상은 리스트 fallback |
| 자동 시드 후 첫 화면 | (1) 모든 태그 노트가 "빈 본문" (2) 자동으로 한 줄 요약 LLM 생성 | (1) 권장 — LLM 비용 회피 + 운전원이 손으로 채우면서 도메인 정합 |
| 백업/git | `storage/vault-export/` 매일 1회 `.md` 덤프 | DB 백업과 별개로 사람이 읽을 수 있는 형태 보존 |

---

## 12. 핵심 메시지

- **옵시디언 도입이 아니다.** 옵시디언이 검증한 4개 패턴(마크다운 + frontmatter + 위키링크 + 태그)만 빌려와 기존 KB 위에 **그래프 레이어**를 얹는다.
- **27B로 그래프를 "추론"시키지 않는다.** 그래프는 사람·규칙·기존 DB로 만든다. LLM은 만들어진 그래프를 **탐색**한 결과를 받아쓴다.
- **P&ID 한계는 노트로 우회한다.** 도면이 표현 못 한 관계를 운전원이 위키링크 한 줄로 채운다. 채울수록 RAG 품질이 자란다.
- **하드웨어 부담은 임베딩 초기 1회 + Qdrant < 100 MB.** 검색 경로에 LLM 추가 호출이 없다.
- **기존 KB와 공존**한다. PDF/Excel은 그대로, vault는 별도 트랙. 채팅은 두 채널 결과를 모두 본다.
