- Integrate ollama + ai-gateway into root docker-compose.yml (NVIDIA GPU runtime, single compose for all services) - Change NAS mount from SMB (NAS_SMB_PATH) to NFS (NAS_NFS_PATH) Default: /mnt/nas/Document_Server (fstab registered on GPU server) - Update config.yaml AI endpoints: primary → Mac mini MLX via Tailscale (100.76.254.116:8800) fallback/embedding/vision/rerank → ollama (same Docker network) gateway → ai-gateway (same Docker network) - Update credentials.env.example (remove GPU_SERVER_IP, add NFS path) - Mark gpu-server/docker-compose.yml as deprecated - Update CLAUDE.md network diagram and AI model config - Update architecture.md, deploy.md, devlog.md for GPU server as main - Caddyfile: auto_https off, HTTP only (TLS at upstream proxy) - Caddy port: 127.0.0.1:8080:80 (localhost only) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1152 lines
46 KiB
Markdown
1152 lines
46 KiB
Markdown
# PKM 웹앱 아키텍처 v2 — DEVONthink 탈피, 자체 시스템 구축
|
|
|
|
## 1. 설계 배경 및 동기
|
|
|
|
### 왜 DEVONthink을 버리는가
|
|
|
|
v1 시스템(DEVONthink 중심)의 6단계 개발 과정에서 드러난 구조적 한계:
|
|
|
|
- AppleScript가 유일한 프로그래밍 인터페이스. 16개 커밋 중 절반 이상이 AppleScript 실행 관련 버그 수정(POSIX path, f-string 이스케이프, -e 분할 등).
|
|
- DEVONthink의 실제 역할은 "파일 저장소 + Smart Rule 트리거"뿐. 분류/태그/요약/임베딩 등 지능은 전부 외부(MLX, GPU 서버)에 존재.
|
|
- macOS GUI 의존성으로 서버 이전 불가. OmniFocus도 동일한 문제.
|
|
- 13개 DB 관리의 복잡성, 웹 접근 불가, 모바일 접근 제한.
|
|
|
|
### 설계 원칙
|
|
|
|
1. **macOS 의존성 제거** — AppleScript, DEVONthink, OmniFocus 전부 탈피
|
|
2. **컨테이너 기반** — 모든 서비스를 Docker로. 서버 간 이동이 `docker compose up` 한 번
|
|
3. **AI 추상화** — OpenAI 호환 API 인터페이스로 통일. MLX/vLLM/Ollama 무관
|
|
4. **Synology 서비스 활용 극대화** — 직접 만들지 않아도 되는 것은 만들지 않는다
|
|
5. **원본 단일 원칙** — 같은 정보가 두 곳에 존재하지 않는다
|
|
|
|
---
|
|
|
|
## 2. 기술 스택
|
|
|
|
| 영역 | 기술 | 선정 근거 |
|
|
|------|------|-----------|
|
|
| 백엔드 | FastAPI (Python) | 기존 Python 코드 1,500줄 재활용. 비동기 지원. OpenAPI 자동 문서화 |
|
|
| 데이터베이스 | PostgreSQL + pgvector | 메타데이터 + 전문검색 + 벡터 검색 통합. JSONB로 유연한 스키마 |
|
|
| 프론트엔드 | SvelteKit | 1인 개발에 적합한 낮은 보일러플레이트. 가벼운 빌드. SSR 지원 |
|
|
| 문서 파싱 | kordoc (Node.js) | HWP/HWPX/PDF → Markdown. MIT 라이선스. MCP 서버 지원 |
|
|
| 리버스 프록시 | Caddy | 자동 HTTPS, 설정 단순 |
|
|
| 인증 | JWT + TOTP 2FA | 1인 사용에 적합한 경량 인증. FastAPI 내장 |
|
|
| 검색 강화 | Meilisearch (선택) | 한국어 검색 품질 향상 필요시 Docker로 추가 |
|
|
| 컨테이너 | Docker Compose | 전 서비스 컨테이너화. 서버 이전 용이 |
|
|
|
|
---
|
|
|
|
## 3. 인프라 역할 분담
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ Mac mini M4 Pro (애플리케이션 서버) │
|
|
│ │
|
|
│ ┌──────────────────────────────────────────────────────┐ │
|
|
│ │ Docker Compose │ │
|
|
│ │ │ │
|
|
│ │ ┌───────────┐ ┌────────────┐ ┌──────────────────┐ │ │
|
|
│ │ │ FastAPI │ │ PostgreSQL │ │ kordoc-service │ │ │
|
|
│ │ │ (백엔드) │ │ + pgvector │ │ (문서 파싱) │ │ │
|
|
│ │ └─────┬─────┘ └────────────┘ └──────────────────┘ │ │
|
|
│ │ │ │ │
|
|
│ │ ┌─────▼─────┐ ┌────────────┐ │ │
|
|
│ │ │ SvelteKit │ │ Caddy │ │ │
|
|
│ │ │ (프론트) │ │ (프록시) │ │ │
|
|
│ │ └───────────┘ └────────────┘ │ │
|
|
│ └──────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ ┌──────────────────────┐ ┌────────────────┐ │
|
|
│ │ MLX Server │ │ Roon Core │ │
|
|
│ │ Qwen3.5-35B-A3B │ │ (음악 서버) │ │
|
|
│ │ localhost:8800 │ │ │ │
|
|
│ └──────────────────────┘ └────────────────┘ │
|
|
└───────────────────────────┬─────────────────────────────────┘
|
|
│ Tailscale
|
|
┌───────────────────┼───────────────────┐
|
|
┌───────▼─────────────┐ ┌──────────▼──────────────┐
|
|
│ Synology DS1525+ │ │ GPU 서버 (리눅스) │
|
|
│ │ │ │
|
|
│ ▸ 파일 원본 저장 │ │ ▸ nomic-embed-text │
|
|
│ (Document_Server) │ │ (벡터 임베딩) │
|
|
│ ▸ Synology Office │ │ ▸ Qwen2.5-VL-7B │
|
|
│ (문서 편집/미리보기)│ │ (이미지/도면 OCR) │
|
|
│ ▸ Synology Drive │ │ ▸ bge-reranker-v2-m3 │
|
|
│ (파일 동기화) │ │ (RAG 리랭킹) │
|
|
│ ▸ Synology Calendar │ │ ▸ Plex, Komga │
|
|
│ (태스크 - CalDAV) │ │ │
|
|
│ ▸ MailPlus │ │ │
|
|
│ (이메일 + 알림) │ │ │
|
|
│ ▸ Synology Chat │ │ │
|
|
└─────────────────────┘ └──────────────────────────┘
|
|
```
|
|
|
|
### Mac mini 역할
|
|
|
|
- FastAPI 웹앱 서버 (API + 프론트엔드 서빙)
|
|
- PostgreSQL (메타데이터, 전문검색 인덱스, 벡터 임베딩)
|
|
- kordoc 마이크로서비스 (HWP/PDF 텍스트 추출)
|
|
- AI 추론 (MLX Qwen3.5-35B-A3B, 분류/태그/요약)
|
|
- Caddy 리버스 프록시 (외부 접근 + 자동 HTTPS)
|
|
- Roon Core (음악, PKM과 무관)
|
|
|
|
### Synology NAS 역할
|
|
|
|
- 파일 원본 저장소 (`/volume4/Document_Server/`)
|
|
- Synology Office → 문서 편집 및 미리보기 (DOCX/XLSX/PPTX 대체)
|
|
- Synology Drive → `Document_Server/` 전체 관리 + 버전 이력 + `Technicalkorea Document/` 동기화
|
|
- Synology Calendar → 태스크/일정 관리 (CalDAV VTODO, OmniFocus 대체)
|
|
- MailPlus → 이메일 수집 (IMAP) + 알림 발송 (SMTP)
|
|
|
|
### GPU 서버 역할
|
|
|
|
- **AI Gateway** (모델 라우팅, 폴백 처리, 비용 제어, 요청 로깅)
|
|
- 벡터 임베딩 (nomic-embed-text)
|
|
- 이미지/도면 OCR (Qwen2.5-VL-7B)
|
|
- RAG 리랭킹 (bge-reranker-v2-m3)
|
|
- NanoClaw (선택적, 대화형 PKM 인터페이스 — Synology Chat/Telegram 연동)
|
|
- Plex, Komga (미디어, PKM과 무관)
|
|
|
|
---
|
|
|
|
## 4. NAS 파일 구조
|
|
|
|
```
|
|
/volume4/
|
|
├── Document_Server/ ← Synology Drive 전체 관리
|
|
│ ├── PKM/ ← FastAPI가 관리하는 영역
|
|
│ │ ├── Inbox/ ← 미분류 자료 진입점
|
|
│ │ ├── Archive/ ← 이메일, 채팅, 수신 문서 원본
|
|
│ │ │ └── imports/ ← 변환 전 교환 형식 원본 (일정 기간 보관)
|
|
│ │ ├── Projects/ ← 진행 중 프로젝트 문서
|
|
│ │ ├── Knowledge/ ← 도메인별 지식 (v1의 13개 DB 매핑)
|
|
│ │ │ ├── Philosophy/
|
|
│ │ │ ├── Language/
|
|
│ │ │ ├── Engineering/
|
|
│ │ │ ├── Industrial_Safety/
|
|
│ │ │ │ ├── Legislation/ ← 법령, 고시, 지침
|
|
│ │ │ │ ├── Standards/ ← 안전보건 기준
|
|
│ │ │ │ └── Cases/ ← 사고 사례, 판례
|
|
│ │ │ ├── Programming/
|
|
│ │ │ └── General/
|
|
│ │ └── Reference/ ← 도면, 참고자료
|
|
│ │
|
|
│ └── DEVONThink/ ← 기존 WebDAV 폴더 (마이그레이션 후 정리)
|
|
│
|
|
└── Technicalkorea Document/ ← Synology Drive 동기화 (회사 문서)
|
|
└── (회사 문서들) ← PKM이 읽기 전용으로 인덱싱
|
|
```
|
|
|
|
### DEVONthink 13개 DB → 폴더 매핑
|
|
|
|
| v1 (DEVONthink DB) | v2 (NAS 폴더) |
|
|
|---------------------|----------------|
|
|
| Inbox | `PKM/Inbox/` |
|
|
| Archive | `PKM/Archive/` |
|
|
| Projects | `PKM/Projects/` |
|
|
| 00_Note_BOX | `PKM/Knowledge/` (루트의 노트 파일) |
|
|
| 01_Philosophie | `PKM/Knowledge/Philosophy/` |
|
|
| 02_Language | `PKM/Knowledge/Language/` |
|
|
| 03_Engineering | `PKM/Knowledge/Engineering/` |
|
|
| 04_Industrial safety | `PKM/Knowledge/Industrial_Safety/` |
|
|
| 05_Programming | `PKM/Knowledge/Programming/` |
|
|
| 07_General Book | `PKM/Knowledge/General/` |
|
|
| 97_Production drawing | `PKM/Reference/` |
|
|
| 99_Reference Data | `PKM/Reference/` |
|
|
| 99_Technicalkorea | `/volume4/Technicalkorea Document/` (별도 경로) |
|
|
|
|
---
|
|
|
|
## 5. 문서 생애주기 및 "원본" 정의
|
|
|
|
### 문서 분류 체계
|
|
|
|
모든 문서는 성격에 따라 세 가지로 나뉜다:
|
|
|
|
| 유형 | 정의 | 원본 = | 예시 |
|
|
|------|------|--------|------|
|
|
| **immutable** | 수신/생성 후 변경하지 않는 문서 | 수신한 그 파일 | PDF, 수신 HWP, 법령, 스캔 이미지, 계약서 |
|
|
| **editable** | 내가 편집하는 살아있는 문서 | Synology Office 포맷 (.odoc, .osheet) | 작성 중인 보고서, 스프레드시트 |
|
|
| **note** | 직접 작성하는 텍스트 | Markdown 파일 | 노트, 메모, 일지 |
|
|
|
|
### 교환 형식 처리 원칙
|
|
|
|
`.docx`, `.xlsx`, `.pptx`는 **교환 형식**이지, 원본이 아니다.
|
|
|
|
- **수신 시**: 교환 형식으로 들어오면 → Synology Office 포맷으로 변환 (이것이 원본이 됨) → 교환 형식 원본은 `Archive/imports/`에 일정 기간 보관 후 정리
|
|
- **발신 시**: Synology Office 원본 → `.docx`/`.xlsx`로 내보내기 → 임시 파일로 생성, 다운로드 후 자동 삭제
|
|
- **예외**: 외부에서 받은 문서를 편집 없이 보관만 해야 하는 경우 (예: 계약서 원본) → `immutable`로 분류, 원본 형식 그대로 보관
|
|
|
|
### 데이터 3계층
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────┐
|
|
│ 1계층: 원본 파일 (NAS) │
|
|
│ 위치: /volume4/Document_Server/PKM/ │
|
|
│ 내용: 사용자가 보고 편집하는 실제 파일 │
|
|
│ 관리: Synology Drive (버전 이력 자동) │
|
|
│ 원칙: 포맷별 단일 원본만 존재 │
|
|
└──────────────────────────┬──────────────────────────────┘
|
|
│ 텍스트 추출 (kordoc 등)
|
|
┌──────────────────────────▼──────────────────────────────┐
|
|
│ 2계층: 가공 데이터 (PostgreSQL) │
|
|
│ 내용: │
|
|
│ - extracted_text (Markdown 변환 텍스트) │
|
|
│ - AI 메타데이터 (태그, 요약, 분류) │
|
|
│ - 전문검색 인덱스 (GIN + pg_trgm) │
|
|
│ 원칙: 원본에서 파생, 언제든 재생성 가능 │
|
|
│ 버전 추적: extractor_version, ai_model_version │
|
|
└──────────────────────────┬──────────────────────────────┘
|
|
│ 벡터 변환 (GPU 서버)
|
|
┌──────────────────────────▼──────────────────────────────┐
|
|
│ 3계층: 파생물 (pgvector + 캐시) │
|
|
│ 내용: │
|
|
│ - 벡터 임베딩 (vector(768)) │
|
|
│ - 미리보기 캐시 (Markdown 렌더링 결과) │
|
|
│ - 썸네일 (이미지/PDF 첫 페이지) │
|
|
│ 원칙: 2계층에서 파생, 캐시 성격, 삭제해도 무관 │
|
|
└─────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
**핵심 원칙**: 1계층(NAS)이 삭제되면 복구 불가. 2~3계층은 1계층에서 언제든 재생성 가능. 따라서 백업 우선순위는 1계층 > 2계층 >> 3계층.
|
|
|
|
---
|
|
|
|
## 6. 데이터 모델 (PostgreSQL 스키마)
|
|
|
|
### documents 테이블
|
|
|
|
```sql
|
|
CREATE EXTENSION IF NOT EXISTS vector;
|
|
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
|
|
|
CREATE TYPE doc_type AS ENUM ('immutable', 'editable', 'note');
|
|
CREATE TYPE source_channel AS ENUM (
|
|
'law_monitor', 'devonagent', 'email', 'web_clip',
|
|
'tksafety', 'inbox_route', 'manual', 'drive_sync'
|
|
);
|
|
CREATE TYPE data_origin AS ENUM ('work', 'external');
|
|
|
|
CREATE TABLE documents (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
|
|
-- 1계층: 원본 파일 참조
|
|
file_path TEXT NOT NULL UNIQUE, -- NAS 상의 원본 경로
|
|
file_hash CHAR(64) NOT NULL, -- SHA-256 (변경 감지 + 중복 방지)
|
|
file_format VARCHAR(20) NOT NULL, -- hwp, pdf, odoc, osheet, md, ...
|
|
file_size BIGINT,
|
|
file_type doc_type NOT NULL DEFAULT 'immutable',
|
|
import_source TEXT, -- 교환 형식 유입 기록 (nullable)
|
|
-- 예: "email:msg-id-xxx, 원본:보고서.docx"
|
|
|
|
-- 2계층: 텍스트 추출
|
|
extracted_text TEXT, -- Markdown 변환 전문
|
|
extracted_at TIMESTAMPTZ,
|
|
extractor_version VARCHAR(50), -- "kordoc@1.7.1"
|
|
|
|
-- 2계층: AI 가공
|
|
ai_summary TEXT, -- AI 요약 (500자 이내)
|
|
ai_tags JSONB DEFAULT '[]', -- ["산업안전", "법령개정", ...]
|
|
ai_domain VARCHAR(100), -- Knowledge/Industrial_Safety
|
|
ai_sub_group VARCHAR(100), -- Legislation
|
|
ai_model_version VARCHAR(50), -- "qwen3.5-35b-a3b"
|
|
ai_processed_at TIMESTAMPTZ,
|
|
|
|
-- 3계층: 벡터 임베딩
|
|
embedding vector(768), -- nomic-embed-text 출력
|
|
embed_model_version VARCHAR(50), -- "nomic-embed-text-v1.5"
|
|
embedded_at TIMESTAMPTZ,
|
|
|
|
-- 메타데이터
|
|
source_channel source_channel,
|
|
data_origin data_origin,
|
|
title TEXT, -- 문서 제목 (AI 추출 또는 파일명)
|
|
|
|
-- 타임스탬프
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- 전문검색 인덱스
|
|
CREATE INDEX idx_documents_fts ON documents
|
|
USING GIN (to_tsvector('simple', coalesce(title, '') || ' ' || coalesce(extracted_text, '')));
|
|
|
|
-- 트리그램 인덱스 (한국어 부분 매칭)
|
|
CREATE INDEX idx_documents_trgm ON documents
|
|
USING GIN ((coalesce(title, '') || ' ' || coalesce(extracted_text, '')) gin_trgm_ops);
|
|
|
|
-- 벡터 유사도 검색 인덱스
|
|
CREATE INDEX idx_documents_embedding ON documents
|
|
USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
|
|
|
|
-- 해시 기반 중복 검색
|
|
CREATE INDEX idx_documents_hash ON documents (file_hash);
|
|
|
|
-- 재가공 대상 필터링
|
|
CREATE INDEX idx_documents_ai_version ON documents (ai_model_version);
|
|
CREATE INDEX idx_documents_extractor_version ON documents (extractor_version);
|
|
CREATE INDEX idx_documents_embed_version ON documents (embed_model_version);
|
|
```
|
|
|
|
### tasks 테이블 (CalDAV 캐시)
|
|
|
|
```sql
|
|
-- Synology Calendar가 원본, 이 테이블은 웹 UI 표시용 캐시
|
|
CREATE TABLE tasks (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
caldav_uid TEXT UNIQUE, -- CalDAV VTODO UID
|
|
title TEXT NOT NULL,
|
|
description TEXT,
|
|
due_date TIMESTAMPTZ,
|
|
priority SMALLINT DEFAULT 0, -- 0=없음, 1=높음, 5=중간, 9=낮음
|
|
completed BOOLEAN DEFAULT FALSE,
|
|
completed_at TIMESTAMPTZ,
|
|
document_id BIGINT REFERENCES documents(id), -- 관련 문서 연결
|
|
source VARCHAR(50), -- 'law_change', 'inbox_overflow', 'manual'
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
```
|
|
|
|
### processing_queue 테이블 (비동기 가공 큐)
|
|
|
|
```sql
|
|
CREATE TYPE process_stage AS ENUM ('extract', 'classify', 'embed');
|
|
CREATE TYPE process_status AS ENUM ('pending', 'processing', 'completed', 'failed');
|
|
|
|
CREATE TABLE processing_queue (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
document_id BIGINT REFERENCES documents(id) NOT NULL,
|
|
stage process_stage NOT NULL,
|
|
status process_status DEFAULT 'pending',
|
|
attempts SMALLINT DEFAULT 0,
|
|
max_attempts SMALLINT DEFAULT 3,
|
|
error_message TEXT,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
started_at TIMESTAMPTZ,
|
|
completed_at TIMESTAMPTZ,
|
|
|
|
UNIQUE (document_id, stage, status) -- 같은 단계 중복 방지
|
|
);
|
|
|
|
CREATE INDEX idx_queue_pending ON processing_queue (stage, status)
|
|
WHERE status = 'pending';
|
|
```
|
|
|
|
---
|
|
|
|
## 7. 문서 가공 파이프라인
|
|
|
|
### 전체 흐름
|
|
|
|
```
|
|
파일 유입
|
|
│ (이메일 첨부 / 웹 클립 / 법령 모니터 / 직접 업로드 / Drive 동기화)
|
|
│
|
|
▼
|
|
① NAS에 원본 저장 + DB에 메타데이터 등록
|
|
│ file_path, file_hash, file_format, source_channel
|
|
│ file_hash로 중복 검사 → 이미 있으면 가공 스킵
|
|
│
|
|
▼
|
|
② kordoc-service로 텍스트 추출 (비동기)
|
|
│ HWP/HWPX/PDF → Markdown
|
|
│ Synology Office 포맷 → Synology API로 텍스트 추출
|
|
│ → DB.extracted_text 저장
|
|
│ → DB.extractor_version 기록
|
|
│
|
|
▼
|
|
③ MLX로 AI 분류/태그/요약 (비동기)
|
|
│ extracted_text → Qwen3.5-35B-A3B
|
|
│ → DB.ai_summary, ai_tags, ai_domain, ai_sub_group 저장
|
|
│ → DB.ai_model_version 기록
|
|
│ → 결과에 따라 NAS 폴더 이동 (Inbox → Knowledge/...)
|
|
│
|
|
▼
|
|
④ GPU 서버로 벡터 임베딩 (비동기)
|
|
│ extracted_text → nomic-embed-text
|
|
│ → DB.embedding 저장
|
|
│ → DB.embed_model_version 기록
|
|
│
|
|
▼
|
|
⑤ 관련 태스크 생성 (조건부)
|
|
법령 변경 → CalDAV VTODO 생성 + MailPlus 알림
|
|
Inbox 적체(>3건) → 태스크 생성
|
|
```
|
|
|
|
### 재가공 전략
|
|
|
|
모델 업그레이드 시 전체를 다시 돌리지 않는다:
|
|
|
|
```sql
|
|
-- kordoc 업그레이드 시: 이전 버전으로 추출된 문서만 재처리
|
|
INSERT INTO processing_queue (document_id, stage)
|
|
SELECT id, 'extract' FROM documents
|
|
WHERE extractor_version != 'kordoc@2.0.0'
|
|
AND extracted_text IS NOT NULL;
|
|
|
|
-- AI 모델 변경 시: 텍스트 재추출 없이 분류만 재실행
|
|
INSERT INTO processing_queue (document_id, stage)
|
|
SELECT id, 'classify' FROM documents
|
|
WHERE ai_model_version != 'qwen3.5-35b-a3b-v2'
|
|
AND extracted_text IS NOT NULL;
|
|
|
|
-- 임베딩 모델 변경 시: 임베딩만 재생성
|
|
INSERT INTO processing_queue (document_id, stage)
|
|
SELECT id, 'embed' FROM documents
|
|
WHERE embed_model_version != 'nomic-embed-text-v2'
|
|
AND extracted_text IS NOT NULL;
|
|
```
|
|
|
|
### 파일 변경 감지
|
|
|
|
편집 가능 문서(editable)는 Synology Office에서 수정될 수 있다.
|
|
FastAPI의 백그라운드 워커가 주기적으로(또는 Synology Drive webhook으로) 파일 해시를 비교:
|
|
|
|
```
|
|
file_hash 변경 감지
|
|
→ extracted_text 무효화
|
|
→ processing_queue에 extract → classify → embed 순서로 등록
|
|
→ 파이프라인 재실행
|
|
```
|
|
|
|
---
|
|
|
|
## 8. kordoc 통합
|
|
|
|
### 마이크로서비스 구성
|
|
|
|
kordoc은 Node.js 기반이므로, FastAPI(Python)와 분리하여 Docker 컨테이너로 운영:
|
|
|
|
```yaml
|
|
# docker-compose.yml (발췌)
|
|
kordoc-service:
|
|
build:
|
|
context: ./services/kordoc
|
|
dockerfile: Dockerfile
|
|
ports:
|
|
- "3100:3100"
|
|
volumes:
|
|
- /volume4/Document_Server:/documents:ro # NAS 마운트 (읽기 전용)
|
|
environment:
|
|
- MAX_FILE_SIZE=500MB
|
|
- TIMEOUT=30000
|
|
```
|
|
|
|
### API 엔드포인트 (kordoc-service 내부)
|
|
|
|
```
|
|
POST /parse
|
|
body: { filePath: string } 또는 multipart/form-data
|
|
response: { markdown: string, metadata: object, format: string }
|
|
|
|
POST /compare
|
|
body: { filePathA: string, filePathB: string }
|
|
response: { diffs: BlockDiff[] }
|
|
|
|
POST /to-hwpx
|
|
body: { markdown: string }
|
|
response: Buffer (HWPX 파일)
|
|
```
|
|
|
|
### 지원 포맷별 처리
|
|
|
|
| 포맷 | 엔진 | 처리 방식 |
|
|
|------|------|-----------|
|
|
| HWP 5.x | kordoc (OLE2 + CFB) | 21종 제어문자 처리, zlib 압축해제 |
|
|
| HWPX | kordoc (ZIP + XML) | 매니페스트 파싱, 병합 셀, 중첩 테이블 |
|
|
| PDF | kordoc (pdfjs-dist) | 라인 그룹핑, 테이블 감지, 머리글/바닥글 필터링 |
|
|
| .odoc/.osheet | Synology Office API | 텍스트 추출 후 Markdown 변환 |
|
|
| Markdown | 직접 읽기 | 추가 변환 불필요 |
|
|
| 이미지/도면 | GPU 서버 (Qwen2.5-VL-7B) | OCR → 텍스트 추출 |
|
|
|
|
---
|
|
|
|
## 9. UI 구성
|
|
|
|
### 3-패널 레이아웃
|
|
|
|
```
|
|
┌─────────────────┬───────────────────────────────────────┐
|
|
│ │ │
|
|
│ 사이드바 (280px) │ 문서 뷰어 (가변) │
|
|
│ │ │
|
|
│ ┌─────────────┐│ 포맷별 렌더링: │
|
|
│ │ 🔍 검색 ││ PDF → pdf.js 내장 뷰어 │
|
|
│ └─────────────┘│ 검색어 하이라이팅, 페이지 점프 │
|
|
│ │ DOCX등 → Synology Office iframe │
|
|
│ ┌─────────────┐│ 편집 가능한 실시간 미리보기 │
|
|
│ │ 📥 Inbox(3) ││ HWP → kordoc Markdown 렌더링 │
|
|
│ │ 📁 폴더 탐색 ││ 테이블/이미지 포함 │
|
|
│ │ 🏷️ 태그 트리 ││ MD → 렌더링 뷰 + 편집 토글 │
|
|
│ │ 🕐 최근 문서 ││ 코드 하이라이팅, KaTeX, Mermaid│
|
|
│ │ ⭐ 즐겨찾기 ││ 이미지 → 직접 렌더링 │
|
|
│ └─────────────┘│ │
|
|
│ ├───────────────────────────────────────┤
|
|
│ │ 메타데이터 패널 │
|
|
│ ┌─────────────┐│ │
|
|
│ │ 📋 오늘 할일 ││ 태그 | 분류 | AI 요약 │
|
|
│ │ 📊 통계 ││ 관련 문서 5건 (pgvector 유사도) │
|
|
│ └─────────────┘│ 유입 경로 | 원본 형식 | 가공 이력 │
|
|
│ │ [Synology에서 열기] [내보내기] [편집] │
|
|
└─────────────────┴───────────────────────────────────────┘
|
|
```
|
|
|
|
### DEVONthink 대비 개선 포인트
|
|
|
|
| 기능 | DEVONthink | PKM 웹앱 |
|
|
|------|-----------|----------|
|
|
| HWP 미리보기 | Quick Look (불안정) | kordoc → Markdown 렌더링 (테이블/이미지 포함) |
|
|
| Office 편집 | 외부 앱 실행 필요 | Synology Office iframe 내 편집 |
|
|
| AI 요약 | 없음 | 문서 열 때 자동 표시 |
|
|
| 관련 문서 | See Also (불투명한 알고리즘) | pgvector 코사인 유사도 (투명, 조정 가능) |
|
|
| 검색 하이라이팅 | 제한적 | pdf.js / Markdown 렌더러에서 정확한 위치 점프 |
|
|
| 법령 비교 | 없음 | kordoc compare → 조항별 diff 표시 |
|
|
| 접근성 | macOS만 | 웹 브라우저 어디서든 |
|
|
| 모바일 | DEVONthink To Go (유료) | 반응형 웹 UI |
|
|
|
|
### 대시보드 위젯
|
|
|
|
로그인 시 첫 화면:
|
|
|
|
- **오늘의 태스크**: Synology Calendar에서 CalDAV로 가져온 VTODO 목록
|
|
- **Inbox 미분류**: 분류 대기 중인 문서 수 + 빠른 분류 UI
|
|
- **법령 변경 알림**: 최근 감지된 법령 변경사항
|
|
- **최근 문서**: 최근 추가/수정된 문서 5건
|
|
- **시스템 상태**: 각 서비스 가동 상태 (FastAPI, kordoc, MLX, GPU, NAS)
|
|
|
|
---
|
|
|
|
## 10. 인증 및 외부 접근
|
|
|
|
### 구조
|
|
|
|
```
|
|
인터넷 → Caddy (Mac mini, 자동 HTTPS)
|
|
│
|
|
├── pkm.도메인.net → FastAPI (JWT 인증 필요)
|
|
├── office.도메인.net → Synology Office (프록시)
|
|
└── cal.도메인.net → Synology Calendar (프록시)
|
|
```
|
|
|
|
### 인증 방식: JWT + TOTP 2FA
|
|
|
|
1인 사용 시스템이므로 경량하게 구현:
|
|
|
|
- 로그인: 아이디/비밀번호 → TOTP 2단계 인증 → JWT 발급
|
|
- JWT: Access Token (15분) + Refresh Token (7일)
|
|
- 비밀번호: bcrypt 해싱, DB 저장
|
|
- TOTP: Vaultwarden에 시드 저장, 앱(Google Authenticator 등)으로 생성
|
|
- Rate Limiting: 로그인 시도 5회 실패 시 15분 잠금
|
|
|
|
### Caddy 설정 예시
|
|
|
|
```
|
|
document.hyungi.net {
|
|
reverse_proxy localhost:8000 # FastAPI
|
|
}
|
|
|
|
# Synology Office는 NAS로 프록시
|
|
office.hyungi.net {
|
|
reverse_proxy https://ds1525.hyungi.net:5001 {
|
|
header_up Host {upstream_hostport}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 11. AI Gateway 및 모델 전략
|
|
|
|
### 설계 원칙: Qwen3.5 35B 우선, 외부 API는 최후 수단
|
|
|
|
모든 자동화 파이프라인은 **로컬 모델(Qwen3.5-35B-A3B)을 기본**으로 사용한다.
|
|
Claude API(종량제)는 로컬 모델로 처리 불가능한 복잡한 분석에만 제한적으로 사용한다.
|
|
이를 통해 운영 비용을 0원에 가깝게 유지하면서, 필요시에만 외부 API 비용이 발생하는 구조를 만든다.
|
|
|
|
> **참고: Claude 구독 vs API 인증**
|
|
> Anthropic은 2026년 1월부터 구독(Pro/Max) OAuth 토큰의 서드파티 사용을 약관상 금지했다.
|
|
> 자동화 파이프라인에서 Claude를 사용하려면 반드시 **API 키(종량제)**를 사용해야 한다.
|
|
> 개인 실험 목적의 NanoClaw 등은 현재 작동하지만 회색 지대이므로,
|
|
> PKM 핵심 파이프라인은 이에 의존하지 않는다.
|
|
|
|
### AI Gateway (GPU 서버)
|
|
|
|
GPU 서버에 AI 중계 서버(Gateway)를 두어, 모든 AI 요청을 단일 진입점으로 관리한다.
|
|
FastAPI에서 직접 각 모델 엔드포인트를 호출하는 대신, Gateway가 라우팅/폴백/로깅을 담당한다.
|
|
|
|
```
|
|
FastAPI (Mac mini)
|
|
│
|
|
▼
|
|
AI Gateway (GPU 서버, OpenAI 호환 API)
|
|
│
|
|
├── /v1/chat/completions (텍스트 생성)
|
|
│ ├── 1순위: Mac mini MLX → Qwen3.5-35B-A3B (상시, 무료)
|
|
│ ├── 2순위: GPU Ollama → Qwen3.5 또는 동급 모델 (MLX 장애 시 폴백)
|
|
│ └── 3순위: Claude API → claude-sonnet (복잡한 분석 전용, 종량제)
|
|
│
|
|
├── /v1/embeddings (벡터 임베딩)
|
|
│ └── GPU Ollama → nomic-embed-text (로컬, 무료)
|
|
│
|
|
├── /v1/vision (OCR/비전)
|
|
│ └── GPU Ollama → Qwen2.5-VL-7B (로컬, 무료)
|
|
│
|
|
└── /v1/rerank (리랭킹)
|
|
└── GPU Ollama → bge-reranker-v2-m3 (로컬, 무료)
|
|
```
|
|
|
|
### 모델 라우팅 규칙
|
|
|
|
| 작업 | 모델 | 위치 | 비용 | 비고 |
|
|
|------|------|------|------|------|
|
|
| 문서 분류/태그 | Qwen3.5-35B-A3B | Mac mini MLX | 무료 | **기본값**, 전체 처리량의 95%+ |
|
|
| 문서 요약 (4,000자 이내) | Qwen3.5-35B-A3B | Mac mini MLX | 무료 | 대부분의 문서 커버 |
|
|
| 문서 요약 (장문/다국어) | Claude Sonnet | API | 종량제 | 로컬 모델 한계 초과 시만 |
|
|
| 법령 비교/분석 | Qwen3.5-35B-A3B | Mac mini MLX | 무료 | 우선 시도, 품질 미달 시 Claude |
|
|
| 벡터 임베딩 | nomic-embed-text | GPU 서버 | 무료 | GPU 전용 |
|
|
| 이미지/도면 OCR | Qwen2.5-VL-7B | GPU 서버 | 무료 | GPU 전용 |
|
|
| RAG 리랭킹 | bge-reranker-v2-m3 | GPU 서버 | 무료 | GPU 전용 |
|
|
| 복잡한 분석 (장문, 전문가 수준) | Claude Sonnet/Opus | API | 종량제 | 수동 트리거 또는 명시적 요청 |
|
|
|
|
### 폴백 체인 및 비용 제어
|
|
|
|
```python
|
|
# config.yaml
|
|
ai:
|
|
gateway:
|
|
endpoint: "http://gpu-server:8080" # AI Gateway 단일 진입점
|
|
|
|
models:
|
|
primary: # 기본 모델 (자동화 전체)
|
|
endpoint: "http://localhost:8800/v1/chat/completions" # Mac mini MLX
|
|
model: "mlx-community/Qwen3.5-35B-A3B-4bit"
|
|
max_tokens: 4096
|
|
timeout: 60
|
|
|
|
fallback: # MLX 장애 시 폴백
|
|
endpoint: "http://gpu-server:11434/v1/chat/completions" # GPU Ollama
|
|
model: "qwen3.5:35b-a3b"
|
|
max_tokens: 4096
|
|
timeout: 120
|
|
|
|
premium: # 복잡한 분석 전용 (수동 트리거)
|
|
endpoint: "https://api.anthropic.com/v1/messages"
|
|
model: "claude-sonnet-4-20250514"
|
|
api_key: "${CLAUDE_API_KEY}"
|
|
max_tokens: 8192
|
|
# 비용 제어
|
|
daily_budget_usd: 5.00 # 일일 한도
|
|
require_explicit_trigger: true # 자동 파이프라인에서 자동 호출 금지
|
|
|
|
embedding:
|
|
endpoint: "http://gpu-server:11434/api/embeddings"
|
|
model: "nomic-embed-text"
|
|
|
|
vision:
|
|
endpoint: "http://gpu-server:11434/api/generate"
|
|
model: "Qwen2.5-VL-7B"
|
|
|
|
rerank:
|
|
endpoint: "http://gpu-server:11434/api/rerank"
|
|
model: "bge-reranker-v2-m3"
|
|
```
|
|
|
|
### AI Gateway 구현
|
|
|
|
GPU 서버에 경량 프록시 서비스를 Docker로 운영:
|
|
|
|
```yaml
|
|
# GPU 서버 docker-compose.yml
|
|
ai-gateway:
|
|
build: ./services/ai-gateway
|
|
ports:
|
|
- "8080:8080"
|
|
environment:
|
|
- PRIMARY_ENDPOINT=http://mac-mini:8800/v1/chat/completions
|
|
- FALLBACK_ENDPOINT=http://localhost:11434/v1/chat/completions
|
|
- CLAUDE_API_KEY=${CLAUDE_API_KEY}
|
|
- DAILY_BUDGET_USD=5.00
|
|
restart: unless-stopped
|
|
```
|
|
|
|
Gateway의 핵심 기능:
|
|
|
|
- **자동 폴백**: Primary(MLX) 실패/타임아웃 시 Fallback(GPU Ollama)으로 전환
|
|
- **비용 제어**: Claude API 호출 시 일일 예산 확인, 초과 시 거부
|
|
- **요청 로깅**: 모든 AI 요청의 모델/토큰수/응답시간/비용 기록
|
|
- **헬스체크**: 각 모델 엔드포인트의 가용성 주기적 확인
|
|
- **라우팅 태그**: 요청에 `x-model-tier: primary|fallback|premium` 태그로 명시적 라우팅 가능
|
|
|
|
### AI 추상화 레이어 (FastAPI 측)
|
|
|
|
```python
|
|
# ai/client.py — 통합 클라이언트
|
|
class AIClient:
|
|
"""AI Gateway를 통한 통합 클라이언트. 기본값은 항상 Qwen3.5."""
|
|
|
|
def __init__(self, config: dict):
|
|
self.gateway = config["gateway"]["endpoint"]
|
|
self.models = config["models"]
|
|
|
|
async def classify(self, text: str) -> dict:
|
|
"""문서 분류 — 항상 primary(Qwen3.5) 사용"""
|
|
return await self._call(self.models["primary"], prompt=CLASSIFY_PROMPT.format(text=text))
|
|
|
|
async def summarize(self, text: str, force_premium: bool = False) -> str:
|
|
"""문서 요약 — 기본 Qwen3.5, 장문이거나 명시적 요청 시만 Claude"""
|
|
if force_premium or len(text) > 15000:
|
|
return await self._call(self.models["premium"], prompt=SUMMARIZE_PROMPT.format(text=text))
|
|
return await self._call(self.models["primary"], prompt=SUMMARIZE_PROMPT.format(text=text))
|
|
|
|
async def embed(self, text: str) -> list[float]:
|
|
"""벡터 임베딩 — GPU 서버 전용"""
|
|
return await self._call_embed(self.models["embedding"], text=text)
|
|
|
|
async def ocr(self, image_bytes: bytes) -> str:
|
|
"""이미지 OCR — GPU 서버 전용"""
|
|
return await self._call_vision(self.models["vision"], image=image_bytes)
|
|
|
|
async def _call(self, model_config: dict, prompt: str) -> str:
|
|
"""OpenAI 호환 API 호출 + 자동 폴백"""
|
|
try:
|
|
return await self._request(model_config, prompt)
|
|
except (TimeoutError, ConnectionError):
|
|
# Primary 실패 시 Fallback 시도
|
|
if model_config == self.models["primary"]:
|
|
return await self._request(self.models["fallback"], prompt)
|
|
raise
|
|
```
|
|
|
|
### NanoClaw 통합 (선택적 확장)
|
|
|
|
NanoClaw는 PKM 핵심 파이프라인이 아닌, **대화형 인터페이스**로서 선택적으로 통합:
|
|
|
|
```
|
|
NanoClaw (GPU 서버, Docker)
|
|
│
|
|
├── Synology Chat / Telegram 연동
|
|
│ → "지난주 법령 변경사항 알려줘" → PKM API 호출 → 답변
|
|
│ → "이 문서 분류해줘" (파일 전송) → 가공 파이프라인 트리거
|
|
│
|
|
├── 인증: Claude API 키 (종량제) — 구독 OAuth 의존 안 함
|
|
│
|
|
└── 스케줄링: 일일 다이제스트를 채팅으로 전송 (MailPlus 보완)
|
|
```
|
|
|
|
NanoClaw의 `ANTHROPIC_BASE_URL`을 AI Gateway로 설정하면,
|
|
NanoClaw도 동일한 라우팅 규칙(Qwen3.5 우선)을 따르게 된다:
|
|
|
|
```env
|
|
# NanoClaw .env
|
|
ANTHROPIC_BASE_URL=http://localhost:8080/v1 # AI Gateway
|
|
ANTHROPIC_AUTH_TOKEN=${CLAUDE_API_KEY} # API 키 (구독 OAuth 아님)
|
|
```
|
|
|
|
### 장기 전환 시 변경 사항
|
|
|
|
| 전환 | config.yaml 변경 | 코드 수정 |
|
|
|------|------------------|-----------|
|
|
| MLX → GPU vLLM (Mac mini 퇴역) | primary.endpoint URL 변경 | 없음 |
|
|
| Qwen3.5 → 더 좋은 모델 | primary.model 변경 | 없음 |
|
|
| GPU 확장 (RTX Pro 6000) | 더 큰 모델 추가, 라우팅 규칙 조정 | 없음 |
|
|
| Claude API 불필요해질 때 | premium 섹션 제거 | 없음 |
|
|
| AI Gateway를 NAS로 이전 | gateway.endpoint URL 변경 | 없음 |
|
|
|
|
---
|
|
|
|
## 12. Synology 서비스 연동
|
|
|
|
### Synology Calendar (CalDAV)
|
|
|
|
OmniFocus를 대체하는 태스크 관리:
|
|
|
|
```python
|
|
# caldav 라이브러리로 연동
|
|
import caldav
|
|
|
|
client = caldav.DAVClient(
|
|
url="https://ds1525.hyungi.net/caldav/",
|
|
username="hyungi",
|
|
password=CALDAV_PASSWORD
|
|
)
|
|
|
|
# 태스크 생성 (법령 변경 감지 시)
|
|
calendar = client.calendar(name="PKM")
|
|
calendar.save_todo(
|
|
summary="[법령검토] 산업안전보건법 시행규칙 개정안 확인",
|
|
description="2026-04-01 관보 게재. 변경 내용 검토 필요.",
|
|
priority=1, # 높음
|
|
due=datetime.now() + timedelta(days=3)
|
|
)
|
|
```
|
|
|
|
### MailPlus (IMAP 수집 + SMTP 알림)
|
|
|
|
기존 mailplus_archive.py에서 AppleScript 의존성만 제거:
|
|
|
|
```python
|
|
# 수집: 기존 IMAP 로직 유지 (이미 AppleScript 미사용)
|
|
# 알림: SMTP 발송 추가
|
|
import smtplib
|
|
from email.mime.text import MIMEText
|
|
|
|
def send_notification(subject: str, body: str):
|
|
msg = MIMEText(body, 'html')
|
|
msg['Subject'] = subject
|
|
msg['From'] = 'pkm@hyungi.net'
|
|
msg['To'] = 'hyungi@hyungi.net'
|
|
|
|
with smtplib.SMTP_SSL(MAILPLUS_HOST, 465) as server:
|
|
server.login(MAILPLUS_USER, MAILPLUS_PASS)
|
|
server.send_message(msg)
|
|
```
|
|
|
|
### Synology Office (문서 뷰어)
|
|
|
|
웹앱에서 문서 클릭 시 파일 유형에 따라 분기:
|
|
|
|
```typescript
|
|
// SvelteKit 프론트엔드
|
|
function openDocument(doc: Document) {
|
|
switch (doc.file_format) {
|
|
case 'odoc':
|
|
case 'osheet':
|
|
case 'odp':
|
|
// Synology Office iframe으로 열기
|
|
viewerUrl = `https://office.hyungi.net/oo/r/${doc.synology_file_id}`;
|
|
break;
|
|
case 'pdf':
|
|
// pdf.js 내장 뷰어
|
|
viewerUrl = `/viewer/pdf?path=${doc.file_path}`;
|
|
break;
|
|
case 'hwp':
|
|
case 'hwpx':
|
|
// kordoc 변환된 Markdown 렌더링
|
|
viewerContent = doc.extracted_text;
|
|
viewerMode = 'markdown';
|
|
break;
|
|
case 'md':
|
|
// Markdown 렌더링 + 편집 토글
|
|
viewerContent = doc.extracted_text;
|
|
viewerMode = 'markdown-editable';
|
|
break;
|
|
default:
|
|
// 이미지 직접 렌더링 또는 다운로드
|
|
viewerUrl = `/api/files/${doc.id}/raw`;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 13. v1 코드 재활용 계획
|
|
|
|
### 재활용 가능 (70%+)
|
|
|
|
| v1 파일 | v2 용도 | 변경 사항 |
|
|
|---------|---------|-----------|
|
|
| `scripts/pkm_utils.py` | `app/core/utils.py` | `run_applescript()` 제거, 나머지 유지 |
|
|
| `scripts/law_monitor.py` | `app/workers/law_monitor.py` | AppleScript → FastAPI DB 직접 저장 |
|
|
| `scripts/mailplus_archive.py` | `app/workers/mailplus_archive.py` | AppleScript → DB 저장 + SMTP 알림 추가 |
|
|
| `scripts/pkm_daily_digest.py` | `app/workers/daily_digest.py` | OmniFocus 쿼리 → CalDAV 쿼리, AppleScript → DB 쿼리 |
|
|
| `scripts/prompts/classify_document.txt` | `app/prompts/classify.txt` | 그대로 사용 |
|
|
| `scripts/embed_to_chroma.py` | `app/workers/embed_worker.py` | ChromaDB → pgvector |
|
|
|
|
### 폐기
|
|
|
|
| v1 파일 | 이유 |
|
|
|---------|------|
|
|
| `applescript/auto_classify.scpt` | AppleScript 전체 폐기 |
|
|
| `applescript/omnifocus_sync.scpt` | OmniFocus → CalDAV로 대체 |
|
|
| `launchd/*.plist` | Docker 내부 스케줄러(APScheduler)로 대체 |
|
|
|
|
---
|
|
|
|
## 14. 프로젝트 구조 (v2)
|
|
|
|
```
|
|
pkm-web/
|
|
├── docker-compose.yml ← 전체 서비스 정의
|
|
├── config.yaml ← AI 엔드포인트, NAS 경로 등
|
|
├── credentials.env ← 인증 정보 (.gitignore)
|
|
│
|
|
├── app/ ← FastAPI 백엔드
|
|
│ ├── main.py ← FastAPI 앱 엔트리포인트
|
|
│ ├── core/
|
|
│ │ ├── config.py ← 설정 로딩
|
|
│ │ ├── database.py ← PostgreSQL 연결
|
|
│ │ ├── auth.py ← JWT + TOTP 인증
|
|
│ │ └── utils.py ← 공통 유틸리티
|
|
│ ├── models/
|
|
│ │ ├── document.py ← documents 테이블 ORM
|
|
│ │ ├── task.py ← tasks 테이블 ORM
|
|
│ │ └── queue.py ← processing_queue ORM
|
|
│ ├── api/
|
|
│ │ ├── documents.py ← 문서 CRUD API
|
|
│ │ ├── search.py ← 전문검색 + 벡터검색 API
|
|
│ │ ├── tasks.py ← CalDAV 연동 태스크 API
|
|
│ │ ├── dashboard.py ← 대시보드 데이터 API
|
|
│ │ └── export.py ← 내보내기 (Markdown → DOCX/HWPX)
|
|
│ ├── workers/
|
|
│ │ ├── file_watcher.py ← NAS 파일 변경 감지
|
|
│ │ ├── extract_worker.py ← kordoc 호출, 텍스트 추출
|
|
│ │ ├── classify_worker.py ← AI 분류/태그/요약
|
|
│ │ ├── embed_worker.py ← 벡터 임베딩
|
|
│ │ ├── law_monitor.py ← 법령 변경 모니터링
|
|
│ │ ├── mailplus_archive.py ← 이메일 수집
|
|
│ │ └── daily_digest.py ← 일일 다이제스트
|
|
│ ├── prompts/
|
|
│ │ └── classify.txt ← AI 분류 프롬프트
|
|
│ └── ai/
|
|
│ └── client.py ← AI 추상화 레이어
|
|
│
|
|
├── services/
|
|
│ └── kordoc/
|
|
│ ├── Dockerfile ← kordoc Node.js 컨테이너
|
|
│ ├── package.json
|
|
│ └── server.js ← HTTP API 래퍼
|
|
│
|
|
├── frontend/ ← SvelteKit 프론트엔드
|
|
│ ├── src/
|
|
│ │ ├── routes/
|
|
│ │ │ ├── +page.svelte ← 대시보드 (메인)
|
|
│ │ │ ├── documents/ ← 문서 탐색/검색/뷰어
|
|
│ │ │ ├── inbox/ ← Inbox 분류 UI
|
|
│ │ │ └── settings/ ← 설정
|
|
│ │ ├── lib/
|
|
│ │ │ ├── components/ ← 공통 컴포넌트
|
|
│ │ │ │ ├── DocumentViewer.svelte
|
|
│ │ │ │ ├── MetadataPanel.svelte
|
|
│ │ │ │ ├── Sidebar.svelte
|
|
│ │ │ │ └── TaskWidget.svelte
|
|
│ │ │ └── stores/ ← 상태 관리
|
|
│ │ └── app.html
|
|
│ └── package.json
|
|
│
|
|
├── migrations/ ← PostgreSQL 마이그레이션
|
|
│ └── 001_initial_schema.sql
|
|
│
|
|
├── scripts/
|
|
│ └── migrate_from_devonthink.py ← v1 → v2 마이그레이션 스크립트
|
|
│
|
|
├── docs/
|
|
│ ├── architecture.md ← 이 문서
|
|
│ └── deploy.md ← 배포 가이드
|
|
│
|
|
└── tests/
|
|
├── test_api.py
|
|
├── test_classify.py
|
|
└── test_pipeline.py
|
|
```
|
|
|
|
---
|
|
|
|
## 15. 마이그레이션 전략 (v1 → v2)
|
|
|
|
### 단계별 전환
|
|
|
|
기존 DEVONthink 시스템을 운영하면서 점진적으로 전환:
|
|
|
|
**Phase 0: 기반 구축 (1~2주)**
|
|
- Docker Compose 구성 (FastAPI + PostgreSQL + kordoc + Caddy)
|
|
- DB 스키마 생성
|
|
- 인증 시스템 구현
|
|
- NAS SMB 마운트 확인
|
|
|
|
**Phase 1: 데이터 마이그레이션 (1~2주)**
|
|
- DEVONthink 13개 DB에서 파일 목록 + 메타데이터 추출
|
|
- NAS의 `Document_Server/PKM/` 폴더 구조 생성
|
|
- 파일 복사 + DB 등록 스크립트 (`migrate_from_devonthink.py`)
|
|
- kordoc으로 전문 텍스트 추출 (배치)
|
|
- AI 분류 재실행 (배치)
|
|
- 벡터 임베딩 생성 (배치)
|
|
|
|
**Phase 2: 핵심 기능 구현 (2~3주)**
|
|
- 문서 검색 API + UI
|
|
- 문서 뷰어 (pdf.js, Markdown, Synology Office)
|
|
- Inbox 자동 분류 파이프라인
|
|
- 파일 변경 감지 + 재가공
|
|
|
|
**Phase 3: 자동화 이전 (1~2주)**
|
|
- law_monitor → FastAPI 워커로 이전
|
|
- mailplus_archive → FastAPI 워커로 이전
|
|
- daily_digest → FastAPI 워커로 이전 + MailPlus 알림
|
|
- CalDAV 태스크 연동
|
|
|
|
**Phase 4: UI 완성 + 최적화 (2~3주)**
|
|
- 대시보드 위젯
|
|
- 태그/폴더 탐색 UI
|
|
- 메타데이터 패널 (AI 요약, 관련 문서)
|
|
- 반응형 모바일 대응
|
|
- 성능 최적화 (캐싱, 인덱스 튜닝)
|
|
|
|
**Phase 5: DEVONthink 퇴역**
|
|
- 2주간 병행 운영 후 DEVONthink 완전 종료
|
|
- NAS의 `Document_Server/DEVONThink/` 폴더 아카이브 후 정리
|
|
|
|
### 예상 소요: 약 8~12주
|
|
|
|
---
|
|
|
|
## 16. 장기 로드맵
|
|
|
|
### 서버 전환 계획
|
|
|
|
```
|
|
현재 (2026):
|
|
Mac mini → Docker (FastAPI, PostgreSQL, kordoc, Caddy) + MLX + Roon Core
|
|
GPU 서버 → 임베딩, OCR, 리랭킹 + Plex, Komga
|
|
Synology → 파일 저장, Office, Calendar, MailPlus, Drive
|
|
|
|
중기 (GPU 서버 확장 시):
|
|
Mac mini → Roon Core 전용
|
|
GPU 서버 → Docker 전체 이전 + AI 추론 통합 (vLLM/Ollama CUDA)
|
|
config.yaml의 endpoint URL만 변경
|
|
Synology → 변동 없음
|
|
|
|
장기 (Mac mini 퇴역 시):
|
|
GPU 서버 → 모든 컴퓨팅 (또는 Roon도 Linux 지원 시 통합)
|
|
Synology → 변동 없음 (교체 시에도 Synology 유지)
|
|
|
|
궁극 (Synology GPU 지원 시):
|
|
Synology 단일 서버 가능성 (파일 + 컴퓨팅 + AI 통합)
|
|
```
|
|
|
|
### 전환 시 변경 사항
|
|
|
|
| 전환 | 변경 | 코드 수정 |
|
|
|------|------|-----------|
|
|
| Mac mini → GPU 서버 | docker-compose.yml 이동, config.yaml 수정 | 없음 |
|
|
| MLX → vLLM/Ollama | config.yaml의 endpoint URL 변경 | 없음 |
|
|
| GPU 확장 (RTX Pro 6000) | 더 큰 모델 사용 가능, config.yaml만 변경 | 없음 |
|
|
| Synology 교체 | NAS IP/도메인 변경, credentials.env 수정 | 없음 |
|
|
|
|
---
|
|
|
|
## 17. 자동화 스케줄
|
|
|
|
Docker 내부에서 APScheduler로 관리 (launchd 대체):
|
|
|
|
| 시간 | 작업 | 주기 |
|
|
|------|------|------|
|
|
| 07:00 | law_monitor | 매일 |
|
|
| 07:00, 18:00 | mailplus_archive | 매일 2회 |
|
|
| 20:00 | daily_digest | 매일 |
|
|
| */5분 | file_watcher | 상시 (NAS 변경 감지) |
|
|
| */10분 | processing_queue consumer | 상시 (가공 큐 처리) |
|
|
|
|
---
|
|
|
|
## 부록 A: credentials.env 템플릿
|
|
|
|
```env
|
|
# PostgreSQL
|
|
POSTGRES_HOST=localhost
|
|
POSTGRES_PORT=5432
|
|
POSTGRES_DB=pkm
|
|
POSTGRES_USER=pkm
|
|
POSTGRES_PASSWORD=
|
|
|
|
# AI
|
|
MLX_ENDPOINT=http://localhost:8800/v1/chat/completions
|
|
MLX_MODEL=mlx-community/Qwen3.5-35B-A3B-4bit
|
|
GPU_SERVER_IP=
|
|
GPU_EMBED_PORT=11434
|
|
CLAUDE_API_KEY=
|
|
|
|
# Synology NAS
|
|
NAS_SMB_PATH=/Volumes/Document_Server # macOS SMB 마운트 경로
|
|
NAS_DOMAIN=ds1525.hyungi.net
|
|
NAS_TAILSCALE_IP=100.101.79.37
|
|
NAS_PORT=15001
|
|
|
|
# Synology MailPlus
|
|
MAILPLUS_HOST=mailplus.hyungi.net
|
|
MAILPLUS_PORT=993
|
|
MAILPLUS_SMTP_PORT=465
|
|
MAILPLUS_USER=hyungi
|
|
MAILPLUS_PASS=
|
|
|
|
# Synology Calendar (CalDAV)
|
|
CALDAV_URL=https://ds1525.hyungi.net/caldav/
|
|
CALDAV_USER=hyungi
|
|
CALDAV_PASS=
|
|
|
|
# kordoc
|
|
KORDOC_ENDPOINT=http://kordoc-service:3100
|
|
|
|
# 인증
|
|
JWT_SECRET=
|
|
TOTP_SECRET=
|
|
|
|
# 법령 API
|
|
LAW_OC=
|
|
```
|
|
|
|
## 부록 B: docker-compose.yml 골격
|
|
|
|
```yaml
|
|
version: '3.8'
|
|
|
|
services:
|
|
postgres:
|
|
image: pgvector/pgvector:pg16
|
|
volumes:
|
|
- pgdata:/var/lib/postgresql/data
|
|
- ./migrations:/docker-entrypoint-initdb.d
|
|
environment:
|
|
POSTGRES_DB: pkm
|
|
POSTGRES_USER: pkm
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
|
ports:
|
|
- "5432:5432"
|
|
restart: unless-stopped
|
|
|
|
kordoc-service:
|
|
build: ./services/kordoc
|
|
ports:
|
|
- "3100:3100"
|
|
volumes:
|
|
- ${NAS_SMB_PATH}:/documents:ro
|
|
restart: unless-stopped
|
|
|
|
fastapi:
|
|
build: ./app
|
|
ports:
|
|
- "8000:8000"
|
|
volumes:
|
|
- ${NAS_SMB_PATH}:/documents
|
|
depends_on:
|
|
- postgres
|
|
- kordoc-service
|
|
env_file:
|
|
- credentials.env
|
|
restart: unless-stopped
|
|
|
|
frontend:
|
|
build: ./frontend
|
|
ports:
|
|
- "3000:3000"
|
|
depends_on:
|
|
- fastapi
|
|
restart: unless-stopped
|
|
|
|
caddy:
|
|
image: caddy:2
|
|
ports:
|
|
- "80:80"
|
|
- "443:443"
|
|
volumes:
|
|
- ./Caddyfile:/etc/caddy/Caddyfile
|
|
- caddy_data:/data
|
|
depends_on:
|
|
- fastapi
|
|
- frontend
|
|
restart: unless-stopped
|
|
|
|
volumes:
|
|
pgdata:
|
|
caddy_data:
|
|
```
|