Skip to main content
Glama
README.md
# OpenSQL 기반 AI 문서 검색·벡터 데이터 플랫폼

> 2026 오픈소스 개발자대회 — 티맥스티베로 지정과제
> 문서를 업로드하면 AI가 자동으로 읽고(임베딩), 바뀌면 알아서 갱신하고(변경 로그 동기화), 어떤 AI든 표준 규격(MCP)으로 검색해 쓸 수 있는 무중단 문서 관리 플랫폼.

## 무엇을 해결하나

기업이 쌓아 둔 문서는 세 가지 이유로 AI 검색에 잘 붙지 않는다.

1. **형식이 제각각이다.** PDF·DOCX·HWP가 섞여 있고, 특히 HWP는 국내 공공·대학 문서의 사실상 표준인데 파싱 도구가 마땅치 않다.
2. **문서는 계속 개정된다.** 규정집 하나가 매년 바뀌는데 벡터 인덱스를 사람이 손으로 다시 맞춰야 한다면 운영이 불가능하다.
3. **검색 결과를 아무 AI나 쓰지 못한다.** 시스템마다 전용 API를 새로 만들어야 한다.

4. **문서가 곧 기밀이다.** 규정집·계약서·인사자료가 DB에 평문으로 쌓이면 DB 파일 유출이 곧 문서 유출이다.

이 프로젝트는 **업로드부터 검색까지 전 과정을 자동화**하고, 검색 인터페이스를 MCP 표준으로 노출한다. 저장소는 고가용성 DB인 Tmax OpenSQL이며, 문서 본문은 국산 표준 암호 **ARIA-256**으로 DB 안에서 암호화한다. 로컬 개발은 동일 계열인 PostgreSQL 16 + pgvector로 한다.

**DB가 죽어도 멈추지 않는다.** Patroni가 PostgreSQL을 자동으로 되살리고(실측 RTO 9.73초), 그 동안 앱은 재접속을 시도하며 버틴다. 서비스는 전 계층이 상시 구동으로 등록돼 재부팅 후 사람 개입 없이 복귀한다.

## 아키텍처

### 데이터 흐름 (CRUD)

문서 하나가 시스템에서 겪는 일을 CRUD로 나눠 보면 이렇다. 네 갈래 모두 **같은 변경 로그(CDC)를 거친다** — 업로드든 개정이든 삭제든 워커가 보는 입구는 하나다.

```mermaid
flowchart LR
    subgraph client["클라이언트"]
        U["사용자"]
        AI["AI 클라이언트<br/>(MCP)"]
    end

    subgraph create["C · 생성"]
        C1["POST /documents"]
        C2["파서<br/>PDF·DOCX·HWP→텍스트"]
        C3["버전 INSERT<br/>status=pending"]
    end

    subgraph update["U · 개정"]
        U1["POST /documents/{id}/versions"]
        U2["내용 해시 비교<br/>같으면 건너뜀"]
        U3["새 버전 INSERT<br/>이전 버전 보존"]
    end

    subgraph delete["D · 삭제"]
        D1["DELETE /documents/{id}"]
        D2["CASCADE<br/>버전·청크 제거"]
    end

    subgraph read["R · 조회"]
        R1["GET /search<br/>MCP search_documents"]
        R2["질의 임베딩 → 벡터 검색<br/>절대 하한 + 상대 컷"]
        R3["GET /documents · /changelog"]
    end

    CDC[["변경 로그 (CDC)<br/>insert · update · delete"]]
    W["임베딩 워커<br/>청킹 → BGE-M3 → 청크 저장"]
    DB[("OpenSQL<br/>pgvector · ARIA 암호문")]

    U --> C1 --> C2 --> C3 --> CDC
    U --> U1 --> U2 --> U3 --> CDC
    U --> D1 --> D2 --> CDC
    CDC --> W --> DB
    AI --> R1 --> R2 --> DB
    U --> R3 --> DB
    DB -.->|"본문 변경 감지"| CDC
```

**읽기만 워커를 거치지 않는다.** C·U·D는 변경 로그를 남기고 워커가 비동기로 처리하며, R은 DB를 직접 조회한다. 그래서 업로드 응답이 임베딩을 기다리지 않는다 — 버전 행을 `pending`으로 넣고 즉시 응답하고, 워커가 끝내면 `ready`가 된다.

| | 진입점 | 워커가 하는 일 | 걸리는 시간 |
|---|---|---|---|
| **C** 생성 | `POST /documents` | 청킹 → 임베딩 → 청크 저장 | 응답 즉시 / 임베딩 수십 초 |
| **R** 조회 | `GET /search`, MCP | — (직접 조회) | 0.25초 |
| **U** 개정 | `POST /documents/{id}/versions` | 재임베딩 (이전 버전은 보존) | 동일 |
| **D** 삭제 | `DELETE /documents/{id}` | 로그만 완료 처리 (청크는 CASCADE로 이미 삭제) | 즉시 |

### 무중단 구조

```mermaid
flowchart TB
    subgraph mac["개발·앱 계층 (macOS, LaunchAgent 상시 구동)"]
        API2["FastAPI :8000<br/>/health 자기 진단"]
        WK["임베딩 워커<br/>BGE-M3 (Apple GPU)"]
    end

    subgraph vm["DB 계층 (Rocky Linux 9.7 VM, systemd 상시 구동)"]
        PAT["Patroni<br/>장애 감지·자동 재기동"]
        PG[("PostgreSQL 17.8<br/>+ pgvector · opencrypto")]
        ETCD["etcd<br/>클러스터 상태"]
        PROXY["OpenProxy<br/>커넥션 풀"]
    end

    API2 -->|"재접속 재시도<br/>지수 백오프"| PG
    WK -->|"동일"| PG
    PAT -->|감시·복구| PG
    PAT <--> ETCD
    PROXY -.->|"HA 전환 시 사용 예정"| PG
```

DB가 죽어도 앱이 함께 죽지 않는다. Patroni가 PostgreSQL을 되살리는 동안(실측 **9.73초**) 앱은 지수 백오프로 재접속을 시도하며 버틴다. 임베딩 모델은 재접속과 무관하게 메모리에 남아 재로딩 비용이 없다.

## 현재 상태

**OpenSQL 위에서 전 구간이 동작한다.** 2026-07-30 기준 실제 OpenSQL(single 모드)에 배포해 파이프라인·암호화·자가복구를 모두 실측 검증했다.

| 기능 | 상태 | 구현 |
|---|---|---|
| 문서 파싱 (PDF · DOCX · HWP) | 완료 | `parser/__init__.py` |
| 업로드·개정·삭제 API + 버전 관리 | 완료 | `core/api.py` |
| 청킹 · BGE-M3 임베딩 | 완료 | `core/chunking.py`, `core/embedding.py` |
| 변경 로그(CDC) 자동 재임베딩 | 완료 | `core/schema.sql` 트리거 + `core/worker.py` |
| 벡터 유사도 검색 (절대 하한 + 상대 컷) | 완료 | `core/search.py` |
| MCP 검색 서버 | 완료 | `core/mcp_server.py` |
| 검색 데모 UI | 완료 | `core/static/demo.html` (`/demo`) |
| **OpenSQL 실배포** | **완료** | Rocky Linux 9.7 VM, single 모드 |
| **ARIA-256 문서 암호화** | **완료** | `core/crypto.py` (opencrypto) |
| **감사 로그 (pgaudit)** | **완료** | `write, ddl, role` 선별 기록 |
| **DB 장애 자가복구** | **완료** | Patroni + `core/db.py` 재접속 |
| **헬스체크** | **완료** | `GET /health` |
| 권한 반영 검색 | 미착수 | — |

### 검증 결과

```
uv run pytest
114 passed, 6 skipped
```

스킵 6건은 실모델(`RUN_MODEL_TESTS=1`)·코퍼스·opencrypto 게이트로, 해당 리소스가 있는 환경에서만 실행된다.

**실측값** (OpenSQL VM, 2026-07-30)

| 항목 | 측정 |
|---|---|
| 검색 응답 | **0.25초** |
| 문서 업로드 → 검색 가능 | 30~50초 (임베딩 포함) |
| **Patroni 자가복구 RTO** | **9.73초** (`kill -9` → 자동 부활, 데이터 무손실) |
| DB 재시작 중 앱 생존 | 워커 PID 유지 확인 |
| pgaudit 감사 로그 비용 | 측정 한계 내 **0** (켬 45.83초 vs 끔 45.81초) |
| **barman 전체 백업** | **5초** (50.4 MiB, 스트리밍) |
| **PITR 복구 준비** | **4초** (백업 → 복구본 디렉토리) |

**회귀 방지 테스트** — 모두 실제로 발생했던 문제를 막기 위해 추가됐다.

| 테스트 | 무엇을 보증하나 |
|---|---|
| `test_db_isolation.py` | 테스트가 데모 DB를 건드리지 않음 |
| `test_parser.py` | 한글 파일명 왕복 시 바이트 보존, HWP 탭 컨트롤 처리 |
| `test_search.py` | 무관한 질의에 오답을 주지 않음 (절대 하한) |
| `test_worker.py` | CDC 이벤트 1건 = 트랜잭션 1건, 실패 격리, 다중 워커 중복 방지 |
| `test_worker_reconnect.py` | DB 단절에서 재접속, 예산 소진 시 포기 |
| `test_crypto.py` | 알고리즘 화이트리스트, ARIA 왕복 |
| `test_health.py` | DB 장애 시 503, 워커 정체 감지, 에러에 접속정보 미노출 |

### 검색 품질

두 단계로 거른다.

1. **절대 하한 0.47** — 질의 자체가 문서와 무관하면 빈 결과를 준다. 없는 내용에 그럴듯한 오답을 주는 것보다 없다고 답하는 게 정직하다.
2. **상대 컷 80%** — 1위와 격차가 큰 꼬리를 잘라낸다.

하한값은 질의 30개로 측정해 정했다.

| | 값 |
|---|---|
| 문서에 있는 내용 질의 15개 | 최저 **0.4841** |
| 문서와 무관한 질의 15개 | 최고 **0.4682** |

0.47~0.48이 완전 분리 구간이며 정상 질의 쪽에 여유를 둬 0.47을 택했다. 코퍼스가 바뀌면 분포도 바뀌므로 `SEARCH_MIN_SCORE`로 조정할 수 있다.

상대 컷만으로는 무관 질의를 막을 수 없다 — 무관 질의도 `top_k`를 채우고 그 안에서는 점수가 서로 비슷해 비율 검사를 통과한다("김철수 연락처"가 0.2550/0.2479로 97% 비율을 만족해 2건 반환됐다).

### OpenSQL 고유 기능 활용

순정 PostgreSQL로 대체할 수 없는 것을 쓴다.

| 기능 | 무엇을 하나 | 왜 OpenSQL이어야 하나 |
|---|---|---|
| **opencrypto (ARIA-256)** | 문서 본문·청크를 DB 안에서 암호화 | pgcrypto에는 **ARIA·SEED가 없다**. 국산 표준 암호는 OpenSQL 전용 |
| **Patroni** | 장애 감지·자동 재기동 | OpenSQL 기본 포함. 자가복구의 실행 주체 |
| pgaudit | 문서 변경·스키마·권한 감사 기록 | 오픈소스지만 OpenSQL은 기본 포함·자동 로드 |

암호화는 검색을 막지 않는다. 유사도 계산은 `embedding` 컬럼만 보므로 본문이 암호문이어도 벡터 검색이 정상 동작한다.

**한계 (과장하지 않는다)**: 주장할 수 있는 것은 "DB가 유출돼도 문서 *내용*을 읽을 수 없다"까지다. 임베딩 벡터·파일명·내용 해시는 평문으로 남으므로 "어떤 문서가 있는지 알 수 없다"는 성립하지 않는다. 상세는 [설계 문서](docs/superpowers/specs/2026-07-29-aria-문서암호화-design.md)에 적었다.

## 기술 스택

| 구성 | 기술 | 비고 |
|---|---|---|
| DB | Tmax OpenSQL (시연) / PostgreSQL 16 + pgvector (로컬 개발) | 고가용성·벡터 검색 |
| 임베딩 모델 | BGE-M3 (`BAAI/bge-m3`, 1024차원) | 오픈웨이트·로컬 구동, 대회 운영규정 제9조 준수 |
| 백엔드 | Python 3.12+ · FastAPI · uv | |
| 문서 파서 | pypdf · python-docx · olefile | PDF · DOCX · HWP |
| 검색 인터페이스 | MCP (Model Context Protocol) Python SDK | Claude 등 MCP 클라이언트 연동 |
| 벡터 인덱스 | HNSW (`vector_cosine_ops`) | OpenSQL이 미지원 시 ivfflat으로 대체 |
| 테스트 | pytest | |

## 시작하기

### 1. 의존성 설치

```bash
git clone https://github.com/4thIS/opensource_contest_tmax.git
cd opensource_contest_tmax
uv sync                      # uv 필요: https://docs.astral.sh/uv/
```

### 2. 환경 설정 — 암호화 키 (필수)

문서 본문은 암호화해 저장하므로 **키 없이는 업로드가 실패한다.**

```bash
cp .env.example .env
openssl rand -base64 32        # 출력값을 .env의 DOC_ENC_KEY에 넣는다
```

> **키를 잃어버리면 저장된 모든 문서를 영원히 복구할 수 없다.** 백업도 소용없다 — 암호문만 남기 때문이다. 키는 DB에 저장되지 않으며, 그래야 DB가 통째로 유출돼도 복호화가 불가능하다. 안전한 곳에 따로 보관할 것.

`.env`는 `.gitignore`가 커밋을 차단하고, pre-commit 훅이 키 문자열을 감지해 한 번 더 막는다. 훅은 클론마다 한 번 활성화해야 한다.

```bash
git config core.hooksPath .githooks
```

### 3. 로컬 개발 DB 기동

호스트 포트는 **5436**을 쓴다. 5432는 다른 PostgreSQL과 충돌하기 쉽다.

```bash
docker run -d --name opensql-dev \
  -e POSTGRES_PASSWORD=dev -p 5436:5432 \
  pgvector/pgvector:pg16

docker exec opensql-dev psql -U postgres -c "CREATE DATABASE opensql_doc"
```

로컬은 pgcrypto + AES-256으로 동작한다. OpenSQL 배포에서는 `DOC_CIPHER_ALGO=aria256`으로 국산 표준 암호로 전환한다 — 함수 시그니처가 같아 코드 변경이 없다.

### 4. 스키마 적용

```bash
export DATABASE_URL="postgresql://postgres:dev@localhost:5436/opensql_doc"
uv run python -c "from core.db import get_conn, init_schema; init_schema(get_conn())"
```

`core/schema.sql`은 멱등이라 반복 적용해도 안전하다.

> **이전 버전으로 만든 DB가 있으면 재시딩이 필요하다.** 스키마가 `CREATE TABLE IF NOT EXISTS`라 이미 있는 테이블의 컬럼 타입(`TEXT` → `BYTEA`)이 바뀌지 않는다. 그 상태로 쓰면 업로드는 성공하는데 워커만 조용히 죽는다.
>
> ```bash
> psql "$DATABASE_URL" -c "DROP VIEW IF EXISTS latest_ready_versions;
>   DROP TABLE IF EXISTS change_log, chunks, document_versions, documents CASCADE;"
> ```

### 5. 테스트

```bash
uv run pytest
```

Windows 초기 세팅은 [docs/PROMPTS.md](docs/PROMPTS.md) 하단을 참조한다.

## 실행

세 프로세스를 각각 띄운다. `.env`에 `DATABASE_URL`과 `DOC_ENC_KEY`를 넣어 두면 `--env-file`로 한 번에 넘길 수 있다 — 키를 명령줄에 노출하지 않는 방법이기도 하다.

**백엔드 API** — `http://127.0.0.1:8000`

```bash
uv run --env-file .env uvicorn core.api:app --host 127.0.0.1 --port 8000
```

**임베딩 워커** — 변경 로그를 폴링한다 (기본 3초, `WORKER_INTERVAL`로 조정)

```bash
uv run --env-file .env python -m core.worker
```

DB가 재시작되거나 페일오버가 일어나도 워커는 죽지 않는다. 지수 백오프로 재접속하며 버티고, 임베딩 모델은 메모리에 남아 재로딩하지 않는다.

여러 개를 띄워도 안전하다 — `FOR UPDATE SKIP LOCKED`로 같은 이벤트를 중복 처리하지 않는다(테스트로 검증). 다만 워커마다 임베딩 모델을 로드하므로 메모리 여유를 확인할 것.

**MCP 검색 서버** — stdio

```bash
claude mcp add opensql-doc \
  -- uv run --env-file .env python -m core.mcp_server
```

**상태 확인**

```bash
curl -s http://127.0.0.1:8000/health | python3 -m json.tool
```

DB가 죽었으면 `503 unavailable`, 워커가 멈췄으면 `503 degraded`를 준다 — 둘을 구분해 알려주므로 무엇을 재기동해야 하는지 알 수 있다.

**CDC 시연 스크립트** — 업로드 → 개정본 반영 → 검색 결과 변화를 6단계로 보여준다

`--env-file .env`가 필수다 — `uv`는 `.env`를 자동으로 읽지 않는다. 빼면 `DOC_ENC_KEY` 미설정으로 업로드 단계에서 죽고, `DATABASE_URL` 대신 스크립트 기본 DSN(로컬 5436)에 붙는다.

```bash
uv run --env-file .env python scripts/demo_cdc.py                         # 내장 샘플 문서
uv run --env-file .env python scripts/demo_cdc.py 규정.hwp 규정_개정.hwp --no-pause
```

## API

| | 메서드 | 경로 | 설명 |
|---|---|---|---|
| **C** | `POST` | `/documents` | 문서 업로드 → 버전 1 생성 (`pending`) |
| **U** | `POST` | `/documents/{id}/versions` | 개정본 업로드. 내용 해시가 같으면 건너뜀 |
| **D** | `DELETE` | `/documents/{id}` | 문서 삭제. 버전·청크가 CASCADE로 함께 제거되고 변경 로그에 `delete` 이벤트가 남는다 |
| **R** | `GET` | `/documents` | 문서 목록 + 최신 버전 상태 |
| **R** | `GET` | `/documents/{id}` | 문서 메타 + 버전 이력 |
| **R** | `GET` | `/documents/{id}/diff?from_version=&to_version=` | 두 버전 비교 — 추가·삭제된 줄. 미지정 시 최신 vs 직전 |
| **R** | `GET` | `/search?q=&top_k=` | 벡터 유사도 검색 |
| **R** | `GET` | `/changelog?limit=` | 변경 로그 이벤트 |
| — | `GET` | `/health` | 시스템 상태 (DB 도달·워커 정체·문서 통계) |
| — | `GET` | `/demo` | 검색 데모 UI |

업로드는 50MB 상한, 확장자 화이트리스트(`pdf`·`docx`·`hwp`·`txt`), 경로 조작 문자 차단을 거친다. 원본 파일은 파싱 직후 삭제하며 보관하지 않는다.

삭제는 하드 삭제다. 다만 **변경 로그는 남는다** — `change_log`에 `version_id` 외래키를 걸지 않은 것이 이 때문이며, 이미 삭제된 버전 id를 감사 기록으로 보존한다.

### MCP 도구

| 도구 | 설명 |
|---|---|
| `search_documents(query, top_k)` | 자연어 질의로 의미 기반 검색 |
| `get_document_info(document_id)` | 검색 결과 출처 문서의 메타·버전 이력 |
| `get_document_diff(document_id, from_version, to_version)` | 두 버전 비교 — 개정본에서 무엇이 바뀌었는지 |

## 테스트

테스트 픽스처는 테이블을 DROP하므로 **데모와 분리된 전용 DB**를 쓴다. `tests/conftest.py`가 `TEST_DATABASE_URL` 기본값을 `opensql_test`로 선점하고, 그 DB가 없으면 자동 생성한다.

| 용도 | DB |
|---|---|
| 시연·개발 | `opensql_doc` |
| 테스트 | `opensql_test` (자동 생성) |

다른 DB를 쓰려면 환경변수로 덮어쓴다. 데모 DB를 가리키면 `test_db_isolation.py`가 실패한다.

```bash
TEST_DATABASE_URL="postgresql://postgres:dev@localhost:5436/other" uv run pytest
```

DB 서버가 떠 있지 않으면 DB 의존 테스트는 자동으로 skip된다.

## OpenSQL 배포

**배포 완료.** Rocky Linux 9.7 VM에 single 모드로 설치해 운영 중이다.

| 구성 | 버전 |
|---|---|
| PostgreSQL | 17.8 |
| pgvector | 0.8.1 |
| opencrypto | 1.0 (ARIA·SEED) |
| Patroni / etcd / OpenProxy | 4.0.5 / 3.6.5 / v1.1.3 |

- **지원 환경은 x86_64 Linux 전용**이다 (Rocky·RHEL·Oracle·AlmaLinux 8·9, Ubuntu 22.04·24.04). 대회 지급 패키지는 **Rocky Linux 9.7 전용 빌드**다. macOS·ARM은 지원 목록에 없어 Apple Silicon 개발 머신에서는 x86_64 리눅스 VM을 거친다.
- etcd·Patroni·OpenProxy를 systemd 서비스로 등록해 **VM 재부팅 후 자동 기동**한다. 통합 인스톨러 기본값은 etcd만 systemd로 등록하므로 별도 설정이 필요하다.
- 라이선스는 **Patroni가 도는 노드마다 1개**가 필요하고 hostname·CPU 수에 묶인다. 대회 지급분이 1개이므로 single 모드이며, 따라서 **노드 간 자동 페일오버는 검증하지 못했다** — 증명한 것은 프로세스 수준 자가복구(`kill -9` → Patroni 자동 재기동, RTO 9.73초)까지다.
- 앱은 현재 PostgreSQL에 직결한다. OpenProxy 경유는 HA 구성(2노드 이상) 시점에 전환할 예정이다.

### 백업·복구 (barman PITR)

자가복구는 "프로세스가 죽어도 살아난다"까지다. **사람이 지운 데이터는 되살리지 못한다** — 그래서 시점 복구를 붙였다. 패키지에 포함된 barman 3.11.1을 쓴다.

**스트리밍 방식을 택했다.** rsync·archive 방식은 `archive_command`를 바꿔야 하는데 그 파일은 Patroni가 관리한다. 스트리밍은 복제 슬롯만 쓰므로 클러스터 설정을 건드리지 않는다. (현재 `archive_command`는 Patroni 기본값 `/bin/true`, 즉 실제 보관은 barman의 `pg_receivewal`이 전담한다.)

```
barman check opensql        # 20개 항목 전부 OK
barman backup opensql       # 50.4 MiB / 5초
barman recover --target-name <복구지점> opensql latest <디렉토리>
```

**실제로 복구되는지 확인했다.** 복구 지점을 만들고 → `DROP TABLE documents CASCADE`로 파괴 → 백업에서 별도 디렉토리로 복구 → 격리 포트(5433)로 기동 → 문서 1건·버전 1건·청크 15건이 그대로 돌아왔고, 본문은 **여전히 ARIA 암호문**이었다(복구가 암호화를 훼손하지 않는다). 복구본의 `archive recovery complete` LSN이 목표 지점과 일치했다.

**Patroni와 함께 쓸 때 걸린 것 두 가지** — 둘 다 재기동 시 조용히 사라져서, 백업이 도는 줄 알았는데 안 도는 상태가 된다.

| 증상 | 원인 | 해결 |
|---|---|---|
| 복제 슬롯 `barman`이 사라짐 | Patroni는 자기가 모르는 슬롯을 정리한다 | DCS 설정에 `slots.barman.type=physical` 등록 |
| `pg_hba.conf`의 barman 규칙이 사라짐 | Patroni가 기동 때 **로컬 `patroni.yml`**의 `pg_hba`로 덮어쓴다 | DCS(`edit-config`)가 아니라 로컬 `patroni.yml`을 고치고 **patroni 서비스**를 재시작 (`patronictl restart`는 PostgreSQL만 재시작하므로 반영 안 됨) |

조치 후 `kill -9`로 다시 검증했다 — 슬롯·pg_hba·`barman check` 모두 유지됐고 RTO는 4.86초였다.

복구본을 OpenSQL 바이너리로 기동하려면 `OPENSQL_LICENSE_PATH` 환경변수가 필요하다(라이선스 검증 모듈이 `shared_preload_libraries`에 있다). 이걸 빠뜨리면 `license is invalid`로 기동이 실패한다.

임베딩 모델(BGE-M3)은 VM이 아니라 **개발 머신에서 돌린다.** VM은 x86 에뮬레이션이라 연산이 네이티브의 1/1000 수준(실측 0.77 GFLOPS vs 837 GFLOPS)이어서 모델 추론에 적합하지 않다. 라이선스가 묶는 대상은 DB뿐이므로 이 분리는 규정상 문제가 없다.

## 디렉토리 구조

```
core/       임베딩 파이프라인 · DB 스키마 · CDC 동기화 · 암호화 · 검색 API · MCP 서버
parser/     문서(PDF/DOCX/HWP) → 텍스트 추출 모듈
docs/       모듈 계약(INTERFACES) · 프롬프트 가이드(PROMPTS) · 라이선스 목록(THIRD_PARTY)
  superpowers/  설계 문서(specs) · 구현 계획서(plans)
scripts/    CDC 시연 스크립트
tests/      pytest 테스트
.githooks/  pre-commit — 비밀정보·벤더 패키지 커밋 차단
```

모듈 간 계약은 [docs/INTERFACES.md](docs/INTERFACES.md)에 정의한다. `core/` 내부 설계는 `core/SCHEMA.md`, `core/EMBEDDING.md`, `core/API.md`, `core/MCP.md`에 있다.

## 로드맵

- [x] 팀 규칙·모듈 계약·개발 가이드 문서화
- [x] 문서 파서 (parser/) — PDF · DOCX · HWP
- [x] 업로드 API + 자동 임베딩 파이프라인 (core/)
- [x] 메타데이터·버전 스키마 + pgvector 저장
- [x] 변경 로그(CDC) 기반 재임베딩 동기화
- [x] MCP 검색 서버 + 검색 데모 UI
- [x] 테스트 DB 격리 — 데모 데이터 보호
- [x] **OpenSQL 배포** — Rocky 9.7 VM, single 모드
- [x] **ARIA-256 문서 암호화** — opencrypto (순정 PostgreSQL로 재현 불가)
- [x] **감사 로그** — pgaudit (`write, ddl, role`)
- [x] **자가복구** — Patroni 자동 재기동 + 앱 재접속 (RTO 9.73초 실측)
- [x] **CRUD 완성** — 삭제 API + 헬스체크
- [x] **검색 품질** — 무관 질의에 오답 반환 차단
- [ ] 하이브리드 검색 — 정확 참조("제25조")·고유명사 보강. **본문이 암호화돼 키워드 검색이 불가능**하므로 BGE-M3 sparse 벡터 + pgvector `sparsevec`이 유일한 경로다
- [ ] 재색인 — 모델·청킹 전략 변경 시 기존 문서 재처리
- [x] **버전 diff** — 개정본에서 무엇이 바뀌었는지 제시 (API + MCP 도구)
- [ ] 노드 간 페일오버 — 라이선스 추가 확보 시 2node-witness 전환
- [x] **백업·복구** — barman PITR (`DROP TABLE` → 시점 복구 실증, 백업 5초·복구 4초)
- [ ] 권한 반영 검색 — 부서·기밀등급별 결과 차등

## 팀

| 이름 | 담당 |
|---|---|
| 봉준표 | core/ — 임베딩 파이프라인, DB 스키마, CDC 동기화, MCP 서버 |
| 김태경 | parser/ · docs/ — 문서 파서, 테스트 데이터, 검색 품질 QA, 문서화 |

## 라이선스

[MIT](LICENSE). 사용한 외부 라이브러리와 라이선스는 [docs/THIRD_PARTY.md](docs/THIRD_PARTY.md)에 기록한다.
탑재 AI 모델은 오픈웨이트(가중치 공개·로컬 구동) 모델만 사용한다.