전북특별자치도 조직에서 문서를 자동으로 적절한 담당자에게 배부하는 POC 웹 서비스입니다.
전북특별자치도 조직에서 외부/내부로부터 들어오는 문서를 자동으로 적절한 담당자에게 배부하는 POC 웹 서비스 구축
- OCR 기반 문서 파싱: DeepSeek-OCR을 활용한 정확한 텍스트 추출
- LLM 기반 텍스트 보정: OCR 오류 자동 수정 및 가독성 향상
- 지능형 필터링: 수신자 및 부서 정보 기반 후보자 범위 자동 좁힘
- 3단계 우선순위 파이프라인: 과거 문서 기반 고속/정확 배부
- 하이브리드 랭킹: RAG 벡터 검색 + LLM 평가를 결합한 담당자 추천
- 자동 배정: 최적 담당자 자동 배정 및 배부 이력 관리
- PDF, DOCX, TXT 파일 업로드 지원
- 드래그 앤 드롭 인터페이스
- DeepSeek-OCR을 통한 이미지/스캔 문서 텍스트 추출
- LLM 기반 OCR 오류 자동 보정
- RAG 기반 벡터 검색으로 유사 직원 후보 탐색
- 수신자 필터링: 문서 수신자 정보 기반 후보 범위 좁힘
- 부서 필터링: 문서 내용과 관련된 부서 선정
- 3단계 우선순위 파이프라인: 과거 문서 담당자 기반 고속 배부
- 하이브리드 랭킹: RAG 점수 + LLM 평가 결합
- 문서 배부 이력 조회 및 통계
- 자동/수동 배부 구분 관리
- 배부 현황 대시보드
- 반응형 웹 디자인
- 실시간 처리 진행 상황 표시
- 직원 검색 기능
- 배부 이력 테이블
- 프레임워크: FastAPI (Python 3.11+)
- 웹 서버: uvicorn
- 벡터 DB: Qdrant (dept_knowledge 컬렉션)
- 임베딩 모델: multilingual-e5-large-instruct
- LLM: 외부 LLM API (OpenAI 형식 호환)
- OCR: DeepSeek-OCR
- 데이터베이스: SQLite + SQLAlchemy (aiosqlite)
- 문서 처리: PyPDF2, python-docx
- 프레임워크: React 18
- 언어: TypeScript
- 빌드 도구: Vite
- 스타일링: Tailwind CSS
- 상태 관리: React Query
- 라우팅: React Router
- HTTP 클라이언트: Axios
- 컨테이너화: Docker, Docker Compose
- 웹 서버: Nginx (프론트엔드 서빙)
- 포트:
- Frontend: 7000
- Backend API: 7001 (내부 7000)
- Qdrant: 6333
- DeepSeek-OCR: 30100
doc-agent/
├── backend/ # FastAPI 백엔드
│ ├── app/
│ │ ├── main.py # FastAPI 애플리케이션 엔트리포인트
│ │ ├── config.py # 환경 변수 설정 관리
│ │ ├── auth.py # 인증 관련 유틸리티
│ │ ├── exceptions.py # 커스텀 예외 클래스
│ │ │
│ │ ├── database/
│ │ │ ├── db.py # SQLAlchemy 모델 및 DB 초기화
│ │ │ └── __init__.py
│ │ │
│ │ ├── models/
│ │ │ ├── schemas.py # Pydantic 스키마 (API 요청/응답)
│ │ │ ├── ocr_schemas.py # OCR 관련 스키마
│ │ │ └── __init__.py
│ │ │
│ │ ├── routers/
│ │ │ ├── documents.py # 문서 관련 API 엔드포인트
│ │ │ ├── auth.py # 인증 관련 API 엔드포인트
│ │ │ └── __init__.py
│ │ │
│ │ └── services/
│ │ ├── ocr.py # DeepSeek-OCR 통합
│ │ ├── text_correction.py # LLM 기반 OCR 텍스트 보정
│ │ ├── recipient_filter.py # 수신자 필터링 로직
│ │ ├── department_filter.py # 부서 필터링 로직
│ │ ├── historical_search.py # 3단계 우선순위 파이프라인 (핵심)
│ │ ├── document_summarizer.py # 문서 요약 및 하이브리드 랭킹
│ │ ├── target_department.py # 타겟 부서 결정
│ │ ├── rank_mapper.py # 직급 매핑 및 필터링
│ │ ├── rag.py # Qdrant 벡터 검색
│ │ ├── llm.py # LLM API 호출
│ │ ├── document.py # 문서 처리 로직
│ │ └── __init__.py
│ │
│ ├── data/
│ │ └── documents.db # SQLite 데이터베이스 파일
│ ├── uploads/ # 업로드된 파일 저장 디렉터리
│ ├── requirements.txt # Python 의존성
│ ├── Dockerfile # Backend Docker 이미지
│ └── test_three_stage_pipeline.py # 파이프라인 테스트 스크립트
│
├── frontend/ # React 프론트엔드
│ ├── src/
│ │ ├── main.tsx # React 앱 엔트리포인트
│ │ ├── App.tsx # 메인 앱 컴포넌트
│ │ ├── index.css # 글로벌 스타일
│ │ │
│ │ ├── pages/
│ │ │ ├── Dashboard.tsx # 대시보드 페이지
│ │ │ ├── DocumentList.tsx # 문서 목록 페이지
│ │ │ ├── DocumentDetail.tsx # 문서 상세 페이지
│ │ │ └── Login.tsx # 로그인 페이지
│ │ │
│ │ ├── components/
│ │ │ ├── DocumentUpload.tsx # 문서 업로드 컴포넌트
│ │ │ ├── StatsBanner.tsx # 통계 배너
│ │ │ ├── AssignmentHistoryTable.tsx # 배부 이력 테이블
│ │ │ ├── RecommendationProgress.tsx # 추천 진행 상황 표시
│ │ │ ├── EmployeeSearch.tsx # 직원 검색
│ │ │ ├── AssigneeCard.tsx # 담당자 카드
│ │ │ ├── DocumentCard.tsx # 문서 카드
│ │ │ └── ProtectedRoute.tsx # 인증 라우트
│ │ │
│ │ ├── api/
│ │ │ └── client.ts # API 클라이언트 (Axios)
│ │ │
│ │ ├── contexts/
│ │ │ └── AuthContext.tsx # 인증 컨텍스트
│ │ │
│ │ ├── types/
│ │ │ └── index.ts # TypeScript 타입 정의
│ │ │
│ │ └── utils/
│ │ └── dateUtils.ts # 날짜 유틸리티
│ │
│ ├── package.json # npm 의존성
│ ├── vite.config.ts # Vite 설정
│ ├── tailwind.config.js # Tailwind CSS 설정
│ ├── tsconfig.json # TypeScript 설정
│ ├── Dockerfile # Frontend Docker 이미지
│ └── nginx.conf # Nginx 설정
│
├── docker-compose.yml # Docker Compose 설정
├── .env # 환경 변수 파일
├── .env.example # 환경 변수 예시
└── README.md # 이 파일
┌─────────────────────────────────────────────────────────────────┐
│ 프론트엔드 (React + Nginx) │
│ http://localhost:7000 │
└─────────────────────────────────────────────────────────────────┘
↓ API 호출
┌─────────────────────────────────────────────────────────────────┐
│ 백엔드 (FastAPI + uvicorn) │
│ http://localhost:7001 │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 7단계 파이프라인 │ │
│ │ │ │
│ │ 1. OCR 파싱 (DeepSeek-OCR) │ │
│ │ 2. LLM 텍스트 보정 │ │
│ │ 3. 수신자 필터링 (조건부) │ │
│ │ 4. 부서 필터링 │ │
│ │ 5. 3단계 우선순위 파이프라인 (핵심) │ │
│ │ - Stage 1: Skeleton Matching (1-2초) │ │
│ │ - Stage 2: Hybrid Search (3-5초) │ │
│ │ - Stage 3: LLM Verification (8-12초) │ │
│ │ 6. 문서 요약 + 최종 랭킹 │ │
│ │ 7. 자동 배정 │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
↓ 벡터 검색 ↓ LLM 호출 ↓ OCR 파싱
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Qdrant │ │ LLM API Server │ │ DeepSeek-OCR │
│ (벡터 DB) │ │ (MiniMax-M2) │ │ │
│ :6333 │ │ :30010 │ │ :30100 │
└──────────────────┘ └──────────────────┘ └──────────────────┘
사용자 → [문서 업로드] → Backend API
↓
[OCR 텍스트 추출]
↓
[LLM 텍스트 보정]
↓
[수신자/부서 필터링]
↓
[3단계 우선순위 검색]
├─ Skeleton Match → 즉시 배부 (1-2초)
├─ Hybrid Search → 즉시 배부 (3-5초)
└─ LLM Verify → 배부 또는 전체 파이프라인
↓
[문서 요약 + 하이브리드 랭킹]
(RAG 40% + LLM 60%)
↓
[자동 배정 및 DB 저장]
↓
[결과 반환] → 사용자
시스템의 핵심인 7단계 문서 배부 파이프라인에 대한 상세 설명입니다.
문서 업로드
↓
Phase 1: OCR 파싱 (1-3초)
↓
Phase 2: LLM 텍스트 보정 (1-2초)
↓
Phase 3: 수신자 필터링 (0-2초, 조건부)
↓
Phase 4: 부서 필터링 (2-3초)
↓
Phase 5: 3단계 우선순위 파이프라인 (1-12초) ← 핵심
├─ Stage 1: Skeleton Matching (1-2초, 40% 케이스)
├─ Stage 2: Hybrid Search (3-5초, 25% 케이스)
└─ Stage 3: LLM Verification (8-12초, 15% 케이스)
↓
Phase 6: 문서 요약 + 최종 랭킹 (5-8초, 20% 케이스)
↓
Phase 7: 자동 배정 (1초 미만)
↓
결과 (배부 완료)
평균 처리 시간: 6-8초 (기존 15-20초 대비 60% 단축)
목적: 업로드된 문서에서 텍스트 추출
처리 과정:
-
DeepSeek-OCR API 호출
- 이미지 기반 PDF: OCR로 텍스트 추출
- 텍스트 기반 PDF: 직접 텍스트 추출
- DOCX/TXT: python-docx/텍스트 리더로 추출
-
OCR 결과 저장
ocr_raw_text: 원본 OCR 텍스트ocr_confidence: OCR 신뢰도 (0.0-1.0)
-
실패 시 Fallback
- OCR 실패 시 파일에서 직접 텍스트 추출
- 모두 실패 시 상태를 "파싱 실패"로 변경
구현 파일: backend/app/services/ocr.py, backend/app/routers/documents.py:108-148
처리 시간: 1-3초
목적: OCR 오류 수정 및 가독성 향상
처리 과정:
-
OCR 신뢰도 확인
confidence < 0.8: LLM으로 보정confidence >= 0.8: 원본 사용
-
LLM 텍스트 보정
- 오타 수정
- 띄어쓰기 보정
- 문맥에 맞는 단어 교정
-
결과 저장
corrected_text: 보정된 텍스트content: 최종 사용할 텍스트
프롬프트 구조:
당신은 OCR 텍스트 보정 전문가입니다. 다음 OCR 결과를 분석하여:
1. 맞춤법 오류 수정
2. 띄어쓰기 교정
3. 문맥상 이상한 부분 수정
4. 원본 의미 최대한 보존
[OCR 원본 텍스트]
{raw_ocr_text}
JSON 응답:
{
"corrected_text": "보정된 전체 텍스트",
"corrections": [...]
}
구현 파일: backend/app/services/text_correction.py, backend/app/routers/documents.py:150-172
처리 시간: 1-2초
목적: 문서에 특정 수신자가 명시된 경우 후보자 범위 좁히기
처리 과정:
-
수신자 키워드 탐지
- 키워드: '수신', '수신자', '수신처', '받는사람'
- 키워드가 없으면 포괄적 배부로 처리
-
수신자 정보 추출
- 키워드 주변 문맥 추출 (전후 150자)
-
LLM 분류
- 특정 대상: "정책기획과장", "인사팀" 등 구체적 부서명/직책명
- 포괄적 대상: "전 부서", "관계 부서" 등
-
후보자 검색 (특정 수신자인 경우만)
- RAG로 수신자 텍스트와 유사한 직원 TOP 3 검색
분기 로직:
수신자 키워드 있음?
├─ NO → Phase 4로 진행
└─ YES → 특정 수신자?
├─ NO (포괄적) → Phase 4로 진행
└─ YES → 수신자 기반 후보 검색 → Phase 5로
구현 파일: backend/app/services/recipient_filter.py, backend/app/routers/documents.py:179-205
처리 시간: 0-2초 (조건부)
목적: 문서 내용과 관련된 부서 선정
처리 과정:
-
전체 부서 목록 조회
- Qdrant에서 모든 직원 정보 조회 (top_k=1000)
- dept1, dept2, dept3 기준으로 중복 제거 및 정렬
-
LLM 부서 선택
- 문서 제목과 내용(최대 2000자) 분석
- 관련 부서 1-3개 선택
-
점수 부여 기준 (0-100)
- 90-100: 직접적으로 관련된 핵심 부서
- 70-89: 관련성 높은 부서
- 50-69: 부분적으로 관련된 부서
- 50 미만: 제외
-
후보자 필터링
- 선택된 부서에 속한 직원만 필터링
- 필터링 후 후보가 3명 미만이면 원본 유지 (과도한 필터링 방지)
프롬프트 구조:
문서 내용을 분석하여 업무 관련 부서를 특정하는 전문가입니다.
[문서 내용]
{document_text}
[가용 부서 목록]
- 정책담당관 > 정책기획과 > 업무분석팀
- 인사팀 > 채용파트
- 재무과 > 예산팀
...
JSON 응답:
{
"selected_departments": [
{"dept1": "정책담당관", "dept2": "정책기획과", "dept3": "업무분석팀", "relevance_score": 95},
...
],
"reasoning": "선택 이유"
}
구현 파일: backend/app/services/department_filter.py, backend/app/routers/documents.py:207-235
처리 시간: 2-3초
목적: tasks 컬렉션의 과거 문서를 활용한 고속/정확 배부
이 파이프라인은 시스템의 핵심으로, 과거 문서 담당자 정보를 활용하여 처리 속도와 정확도를 대폭 향상시킵니다.
문서 제목 입력
↓
┌──────────────────────────────────────────┐
│ Stage 1: Skeleton Matching (Fast-Track) │
│ 정규화된 제목이 100% 일치? │
└──────────────────────────────────────────┘
├─ YES → 즉시 배부 (1-2초, 40% 케이스)
└─ NO → Stage 2로
↓
┌──────────────────────────────────────────┐
│ Stage 2: Hybrid Search (Deep-Check) │
│ 임베딩 + 키워드 + 행위어 + 시간 가중치 │
└──────────────────────────────────────────┘
├─ 유사도 ≥ 0.95 → 즉시 배부 (3-5초, 25% 케이스)
├─ 유사도 0.85~0.95 → Stage 3로
└─ 유사도 < 0.85 → Phase 6로 (전체 파이프라인)
↓
┌──────────────────────────────────────────┐
│ Stage 3: LLM Verification (Safety Net) │
│ 연속성/대체불가/Action일치 체크 │
└──────────────────────────────────────────┘
├─ 모두 통과 → 배부 (8-12초, 15% 케이스)
└─ 실패 → Phase 6로 (점수 30% 감점, 20% 케이스)
목적: 정규화된 제목이 100% 일치하는 과거 문서 찾기
정규화 규칙 (2025-11-28 개선):
def _normalize_to_skeleton(title):
# 1. 괄호/대괄호 제거
title = re.sub(r'\(.*?\)', '', title) # (전 직원 공람) 제거
title = re.sub(r'\[.*?\]', '', title) # [안내] 제거
# 2. 연도 표기 제거
title = re.sub(r'\d{4}년도?', '', title) # 2024년, 2024년도
title = re.sub(r'\d{2}년도?', '', title) # 25년, 25년도
# 3. 시기 표기 제거
title = re.sub(r'[상하]반기', '', title)
title = re.sub(r'\d+월', '', title)
# 4. 순서 표기 제거
title = re.sub(r'제\d+차', '', title) # 제1차, 제11차
# 5. 행정 접미사 제거
# 중요: '계획', '결과', '현황', '보고'는 제거하지 않음 (업무 구분 키워드)
admin_suffixes = [
'안내', '송부', '요청', '협조', '공유', '알림',
'개최', '시행', '실시', '제출', '통보',
'명단', '목록', '신청', '대상', '접수', '승인', '검토'
]
for suffix in admin_suffixes:
title = title.replace(suffix, '')
# 6. 한글만 남기기
title = re.sub(r'[^가-힣]', ' ', title)
# 7. 중복 단어 제거
words = []
seen = set()
for word in title.split():
if word and word not in seen:
words.append(word)
seen.add(word)
return ''.join(words)예시:
입력: "2025년 예산 집행 현황 보고"
skeleton: "예산집행현황보고"
tasks 검색:
- "2024년 예산 집행 현황 보고" (보고자: 김철수)
skeleton: "예산집행현황보고"
→ 100% 일치! 즉시 배부
반례 (이제 구분됨):
"2025년 예산 집행 계획" → skeleton: "예산집행계획"
"2025년 예산 집행 현황" → skeleton: "예산집행현황"
→ 다른 문서로 구분됨 (계획 ≠ 현황)
처리 시간: 1-2초 (가장 빠름)
구현 파일: backend/app/services/historical_search.py:104-192
목적: 임베딩 + 키워드 + 행위어 + 시간을 종합한 유사도 계산
핵심 개선사항:
- 문제: 주제(예: 적극행정)는 같지만 행위(조사 vs 경진대회)가 다르면 다른 업무
- 해결: 제목의 마지막 실질 명사(행위어)를 추출하여 가중치 부여
행위어 추출 예시:
"적극행정 우수사례 경진대회 개최 안내" → 행위어: "경진대회"
"적극행정 인식도 조사 계획" → 행위어: "조사"
"국민 참여 예산 공모사업 실시 안내" → 행위어: "공모사업"
점수 계산 공식:
최종 점수 = 임베딩 점수 × 키워드 가중치 × 행위어 가중치 × 시간 가중치
가중치 상세:
| 요소 | 범위 | 설명 |
|---|---|---|
| 임베딩 점수 | 0.0 ~ 1.0 | 기본 벡터 유사도 |
| 키워드 가중치 | 0.5 ~ 1.5 | 핵심 키워드 매칭률 - 0% 일치: 0.5배 - 100% 일치: 1.5배 |
| 행위어 가중치 | 0.3 ~ 1.0 | 완전 일치: 1.0배 부분 일치: 0.65배 불일치: 0.3배 (큰 페널티) |
| 시간 가중치 | 0.3 ~ 1.3 | 작년 문서: 1.3배 올해: 1.1배 2년 이상: 감소 |
예시:
케이스 1: 같은 업무 (높은 점수)
입력: "2025년 예산 집행 현황 보고"
과거: "2024년 예산 집행 현황 보고"
- 임베딩: 0.92
- 키워드: 1.5배 (예산, 집행, 현황 모두 일치)
- 행위어: 1.0배 ("현황" == "현황")
- 시간: 1.3배 (작년)
최종 = 0.92 × 1.5 × 1.0 × 1.3 = 1.79 (정규화 → 0.98)
→ 0.95 이상! 즉시 배부
케이스 2: 다른 업무 (페널티)
입력: "2025년 적극행정 인식도 조사"
과거: "2024년 적극행정 우수사례 경진대회"
- 임베딩: 0.85 (주제 "적극행정" 같음)
- 키워드: 1.2배 (적극행정 일치)
- 행위어: 0.3배 ("조사" ≠ "경진대회", 큰 페널티!)
- 시간: 1.3배
최종 = 0.85 × 1.2 × 0.3 × 1.3 = 0.40
→ 0.85 미만, Phase 6으로 진행
분기:
- 유사도 ≥ 0.95 → 즉시 배부 (3-5초)
- 유사도 0.85~0.95 → Stage 3 검증
- 유사도 < 0.85 → Phase 6으로
처리 시간: 3-5초
구현 파일: backend/app/services/historical_search.py:518-696
목적: 애매한 유사도 구간(0.85~0.95)에서만 LLM이 최종 검증
LLM 호출 조건:
✓ 유사도 0.85~0.95: LLM 판단 실행 (Safety Net)
✗ 유사도 ≥ 0.95: LLM 생략 (고신뢰)
✗ 유사도 < 0.85: LLM 불필요 (저유사도)
3가지 필수 체크 (모두 충족 필요):
-
연속성 (Continuity)
- 작년의 그 업무가 올해 이 업무로 이어진 것인가?
- 예: "2024년 조사" → "2025년 조사" (정기 업무) ✓
- 예: "2024년 조사" → "2025년 계획 수립" (다른 단계) ✗
-
대체 불가능성 (Exclusivity)
- 과거 문서를 처리한 사람이 현재 문서도 처리했을 확률 90% 이상인가?
- 예: "월간 예산 보고" → 동일 담당자 ✓
- 예: "적극행정 조사"와 "적극행정 마일리지 계획" → 다른 담당자 가능 ✗
-
Action 일치 (Action-Level Match)
- 업무의 Action 레벨이 같은가?
- Action 유형: 조사/설문 vs 계획 수립 vs 행사 운영 vs 평가/심사
- 예: "인식도 조사"와 "우수사례 경진대회" → 주제(적극행정)만 같고 Action 다름 ✗
판단 예시:
사례 1: 연속성 확인 ✓
현재: "2025년 1분기 예산 집행 현황 보고"
과거: "2024년 4분기 예산 집행 현황 보고"
판단: ✓ 연속성 ✓ 대체불가 ✓ Action일치 → 즉시 배부
사례 2: 연속성 부정 ✗
현재: "2025년 적극행정 우수사례 경진대회 개최"
과거: "2024년 적극행정 인식도 조사 계획"
판단: ✓ 연속성 (주제 같음) ✗ 대체불가 (담당자 다를 수 있음) ✗ Action불일치
→ 점수 30% 감점 → 전체 파이프라인 진행
LLM 응답 형식:
{
"is_continuous": true/false,
"continuity_check": true/false,
"exclusivity_check": true/false,
"action_match_check": true/false,
"reasoning": "판단 이유...",
"confidence": 0.0-1.0
}판단 결과:
- 3가지 모두 통과 → 배부 (8-12초)
- 하나라도 실패 → 점수 30% 감점, Phase 6으로
처리 시간: 8-12초 (LLM 호출 포함)
구현 파일: backend/app/services/historical_search.py:826-930
Stage 1~3에서 매칭된 과거 문서의 보고자 → 현재 직원 검색
보고자 정확 일치 우선:
for reporter in set(reporters): # 과거 문서의 보고자들
# RAG로 현재 직원 검색 (상위 3명)
candidates = search_similar_employees(reporter, top_k=3)
for candidate in candidates:
# 정확 일치 vs 부분 일치 구분
is_exact_match = (candidate.name == reporter)
if is_exact_match:
# 정확 일치: tasks의 보고자와 정확히 같은 이름
# → 높은 점수 보장 (최소 0.95)
candidate.score = max(0.95, avg_historical_score)
elif reporter in candidate.name:
# 부분 일치: 이름이 포함 (예: "김철수" in "김철수A")
# → 점수 70% 감소
candidate.score = avg_historical_score * 0.7효과:
tasks: "2024년 예산 현황" (보고자: 김철수)
현재 직원 DB: 김철수, 김철수A, 김철수B
기존: 모두 같은 점수
개선:
- 김철수: 0.95+ (1순위) ✓
- 김철수A: 0.66 (2순위)
- 김철수B: 0.66 (3순위)
→ 정확한 김철수가 1순위!
구현 파일: backend/app/services/historical_search.py:408-444
목적: LLM으로 문서 분석 후 하이브리드 랭킹으로 최종 후보 선정
처리 과정:
-
문서 요약 (LLM)
- 키워드: 5-10개
- 요약: 2-3문장
- 필요 역량: 전문성/역량
-
후보자 결정 우선순위
- 1순위: 과거 문서 기반 후보 (Phase 5 결과)
- 2순위: 수신자 필터링 결과 (Phase 3)
- 3순위: RAG 전체 검색 (문서 내용 기반, top_k=20)
-
하이브리드 랭킹
A. RAG 점수 (40% 가중치)
- 문서 키워드 + 요약으로 재검색
- 벡터 유사도 기반 점수 (0-1)
B. LLM 점수 (60% 가중치)
- 각 후보자를 0-100점으로 평가
- 평가 기준:
- 업무 적합성 (40점): 담당 업무와 문서 내용의 관련성
- 전문성 (30점): 필요 역량 보유 여부
- 부서 관련성 (30점): 소속 부서와 문서 주제 관련성
최종 점수 계산:
final_score = (rag_score × 100 × 0.4) + (llm_score × 0.6) -
정렬 및 반환
- 최종 점수 기준 내림차순 정렬
- TOP 5 후보자 반환
점수 기준:
90-100: 최적의 담당자 (직접 관련 업무)
70-89: 적합한 담당자 (관련 경험)
50-69: 가능한 담당자 (부분 관련성)
50 미만: 부적합
구현 파일: backend/app/services/document_summarizer.py, backend/app/routers/documents.py:351-452
처리 시간: 5-8초
목적: 최고 점수 후보자를 자동 배정하고 DB 저장
처리 과정:
-
1순위 후보자 선택
ranked_candidates = sorted(candidates, key=lambda x: x.final_score, reverse=True) top_candidate = ranked_candidates[0]
-
배정 정보 저장
assigned_to: 담당자 이름assigned_dept: 담당자 부서assigned_at: 배정 시각is_auto_assigned: Truestatus: "배부 완료"
-
복수 배부 대상 저장 (JSON)
[ { "name": "김철수", "rank": "7급", "dept1": "재무과", "final_score": 95.5, ... }, ... ] -
추천 이유 저장 (JSON)
{ "pipeline_stage": "Stage 1: Skeleton Match", "title_similarity": 1.0, "candidates": [...], "reasoning": "과거 동일 업무 담당자" }
DB 스키마:
class DocumentModel:
id: Integer
title: String
filename: String
status: String # "배부 완료"
# 배부 정보
assigned_to: String
assigned_dept: String
assigned_at: DateTime
is_auto_assigned: Boolean
# 상세 정보 (JSON)
recommendation_json: Text
assigned_candidates: Text
filtered_departments: Text
# OCR 관련
ocr_raw_text: Text
ocr_confidence: Float
corrected_text: Text구현 파일: backend/app/routers/documents.py:454-535
처리 시간: 1초 미만
| Stage | 조건 | 처리 시간 | 정확도 | 비율 (예상) |
|---|---|---|---|---|
| Stage 1 | Skeleton 100% 일치 | 1-2초 | 99%+ | 40% |
| Stage 2 | 유사도 ≥ 0.95 | 3-5초 | 95%+ | 25% |
| Stage 3 | 유사도 0.85~0.95 + LLM | 8-12초 | 90%+ | 15% |
| Full Pipeline | 유사도 < 0.85 또는 실패 | 15-25초 | 95%+ | 20% |
Phase 1: OCR 파싱 1-3초
Phase 2: 텍스트 보정 1-2초
Phase 3: 수신자 필터링 0-2초 (조건부)
Phase 4: 부서 필터링 2-3초
Phase 5: 우선순위 파이프라인
├─ Stage 1 (40%) 1-2초
├─ Stage 2 (25%) 3-5초
├─ Stage 3 (15%) 8-12초
└─ Skip (20%) 0초
Phase 6: 문서 요약 + 랭킹 5-8초 (Full Pipeline만)
Phase 7: 자동 배정 <1초
평균: 6-8초 (기존 15-20초 대비 60% 단축)
- Stage 1 (Skeleton Match): 정기 보고서 처리 시간 90% 단축 (20초 → 2초)
- Stage 2 (Action-based): 행위어 페널티로 오분류 70% 감소
- Stage 3 (LLM Safety Net): 애매한 케이스만 LLM 호출하여 비용 60% 절감
- 보고자 정확 일치: tasks의 정확한 담당자 95%+ 1순위 배정
- Docker 및 Docker Compose 설치
- Qdrant 서버 접근 가능 (로컬 또는 원격)
- LLM API 서버 접근 가능
- DeepSeek-OCR 서버 접근 가능
프로젝트 루트 디렉터리에 .env 파일을 생성하거나 수정합니다.
# .env 파일 확인
cat .env주요 환경 변수는 환경 변수 설정 섹션을 참조하세요.
# 프로젝트 루트 디렉터리에서
docker-compose up --build또는 백그라운드로 실행:
docker-compose up -d --build- 웹 서비스:
http://localhost:7000 - API 문서:
http://localhost:7001/docs - Qdrant 대시보드:
http://localhost:6333/dashboard(별도 Qdrant 사용 시)
# 중지
docker-compose down
# 로그 확인
docker-compose logs -f
# 특정 서비스 로그 확인
docker-compose logs -f backend
docker-compose logs -f frontend
# 서비스 재시작
docker-compose restart
# 컨테이너 상태 확인
docker-compose pscd backend
# 가상환경 생성 (선택)
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# 의존성 설치
pip install -r requirements.txt
# 서버 실행
uvicorn app.main:app --host 0.0.0.0 --port 7000 --reloadcd frontend
# 의존성 설치
npm install
# 개발 서버 실행
npm run dev개발 서버는 http://localhost:5173에서 실행되며, API는 http://localhost:7000으로 프록시됩니다.
# Frontend 빌드
cd frontend
npm run build
# Backend는 uvicorn으로 실행
cd backend
uvicorn app.main:app --host 0.0.0.0 --port 7000# Qdrant 서버 URL
QDRANT_URL=http://localhost:6333
# Qdrant API 키 (선택적)
QDRANT_API_KEY=
# 벡터 컬렉션명
QDRANT_COLLECTION=dept_knowledgeDocker Compose 사용 시:
QDRANT_URL은 자동으로http://host.docker.internal:6333으로 설정됩니다.- 별도 설정 불필요
# LLM API 엔드포인트
LLM_API_URL=http://192.168.0.201:30010/v1/chat/completions
# LLM API 키 (선택적)
LLM_API_KEY=
# LLM 모델명
LLM_MODEL=/mnt/ssd16tb/MiniMaxAI--MiniMax-M2주의사항:
- OpenAI 형식 API를 사용합니다.
/v1/chat/completions엔드포인트가 필요합니다.
# DeepSeek-OCR API URL
DEEPSEEK_OCR_URL=http://220.124.155.35:30100
# DeepSeek-OCR API 키 (선택적)
DEEPSEEK_OCR_API_KEY=
# OCR 처리 타임아웃 (초)
DEEPSEEK_OCR_TIMEOUT=60# 임베딩 모델명
EMBEDDING_MODEL=intfloat/multilingual-e5-large-instruct처음 실행 시 자동으로 다운로드됩니다.
# 데이터베이스 연결 URL
DATABASE_URL=sqlite+aiosqlite:///./documents.dbSQLite를 사용합니다. 다른 DB를 사용하려면 SQLAlchemy 형식 URL을 입력하세요.
# 업로드된 파일 저장 디렉터리
UPLOAD_DIR=./uploads# 수신자 필터링 활성화
RECIPIENT_FILTER_ENABLED=true
# 부서 필터링 활성화
DEPARTMENT_FILTER_ENABLED=true
# 최소 신뢰도 점수
MIN_CONFIDENCE_SCORE=0.5POST /api/documents/upload
Content-Type: multipart/form-data
파라미터:
- file: 업로드할 파일 (PDF/DOCX/TXT)
응답:
{
"id": 1,
"title": "문서 제목",
"filename": "example.pdf",
"status": "OCR 처리 중",
"uploaded_at": "2025-11-28T12:00:00",
...
}
GET /api/documents/
응답:
[
{
"id": 1,
"title": "문서 제목",
"status": "배부 완료",
"assigned_to": "김철수",
"uploaded_at": "2025-11-28T12:00:00",
...
},
...
]
GET /api/documents/{id}
응답:
{
"id": 1,
"title": "문서 제목",
"content": "문서 내용...",
"status": "배부 완료",
"assigned_to": "김철수",
"assigned_dept": "재무과",
"recommendation_json": {...},
...
}
GET /api/documents/history
응답:
[
{
"id": 1,
"title": "문서 제목",
"assigned_to": "김철수",
"assigned_at": "2025-11-28T13:00:00",
"is_auto_assigned": true,
...
},
...
]
POST /api/documents/{id}/recommend
응답:
{
"candidates": [
{
"name": "김철수",
"rank": "7급",
"dept1": "재무과",
"final_score": 95.5,
"reasoning": "과거 동일 업무 담당자",
...
},
...
],
"pipeline_stage": "Stage 1: Skeleton Match",
"processing_time": 2.3
}
POST /api/documents/{id}/assign
요청 본문:
{
"assigned_to": "김철수",
"assigned_dept": "재무과",
"is_auto": true
}
응답:
{
"id": 1,
"assigned_to": "김철수",
"assigned_dept": "재무과",
"assigned_at": "2025-11-28T13:00:00",
"is_auto_assigned": true,
"status": "배부 완료"
}
GET /api/documents/employees/search?query=김철수
응답:
[
{
"name": "김철수",
"rank": "7급",
"dept1": "재무과",
"dept2": "예산팀",
"score": 0.95,
...
},
...
]
GET /api/documents/stats/today
응답:
{
"total_uploaded": 15,
"total_assigned": 12,
"auto_assigned": 10,
"manual_assigned": 2
}
GET /api/documents/health/qdrant
응답:
{
"status": "healthy",
"collections": ["dept_knowledge", "tasks"],
"version": "1.7.0"
}
GET /health
응답:
{
"status": "ok"
}
- Swagger UI:
http://localhost:7001/docs - ReDoc:
http://localhost:7001/redoc
증상: VectorSearchError: Qdrant 연결 실패
해결 방법:
-
Qdrant 연결 상태 확인
curl http://localhost:7001/api/documents/health/qdrant
-
Qdrant 서버 실행 확인
curl http://localhost:6333/collections
-
환경 변수 확인
# .env 파일에서 QDRANT_URL=http://localhost:6333 QDRANT_COLLECTION=dept_knowledge -
컬렉션 존재 확인
- Qdrant 대시보드:
http://localhost:6333/dashboard dept_knowledge컬렉션이 있는지 확인
- Qdrant 대시보드:
증상: LLMServiceError: LLM API 호출 실패
해결 방법:
-
LLM_API_URL이 올바른지 확인curl -X POST $LLM_API_URL \ -H "Content-Type: application/json" \ -d '{"messages": [{"role": "user", "content": "test"}], "model": "..."}'
-
LLM 서버가 실행 중인지 확인
-
네트워크 연결 확인 (방화벽, VPN 등)
-
로그 확인
docker-compose logs -f backend | grep LLM
증상: OCR 처리 실패
해결 방법:
-
DeepSeek-OCR 서버 실행 확인
curl http://220.124.155.35:30100/health
-
환경 변수 확인
DEEPSEEK_OCR_URL=http://220.124.155.35:30100
-
타임아웃 설정 확인
DEEPSEEK_OCR_TIMEOUT=60 # 초 단위
증상: 애플리케이션 시작 시 환경 변수 오류
해결 방법:
-
.env파일이 프로젝트 루트에 있는지 확인ls -la .env
-
환경 변수 형식이 올바른지 확인 (공백 없이
KEY=value) -
필수 환경 변수가 모두 설정되었는지 확인
python -c "from backend.app.config import settings; settings.validate_settings()"
증상: Docker 컨테이너 실행 실패
해결 방법:
-
컨테이너 상태 확인
docker-compose ps
-
로그 확인
docker-compose logs backend docker-compose logs frontend
-
컨테이너 재시작
docker-compose restart
-
완전 재빌드
docker-compose down docker-compose up --build
-
볼륨 및 네트워크 정리
docker-compose down -v docker network prune docker volume prune
증상: 파일 업로드 시 오류 발생
해결 방법:
-
파일 형식 확인 (PDF, DOCX, TXT만 지원)
-
파일 크기 확인 (너무 큰 파일은 업로드 실패 가능)
-
업로드 디렉터리 권한 확인
ls -ld backend/uploads chmod 755 backend/uploads
-
로그 확인
docker-compose logs -f backend | grep upload
증상: 문서 처리가 너무 느림
해결 방법:
-
파이프라인 단계별 처리 시간 확인
docker-compose logs backend | grep "Processing time"
-
Qdrant 연결 속도 확인
curl -w "@curl-format.txt" http://localhost:6333/collections -
LLM API 응답 속도 확인
-
OCR 처리 속도 확인
-
필요시 타임아웃 설정 증가
DEEPSEEK_OCR_TIMEOUT=120
-
문서 업로드
- 메인 화면에서 파일을 드래그 앤 드롭하거나 "파일 선택" 버튼 클릭
- PDF/DOCX/TXT 파일 업로드
-
자동 처리
- 시스템이 7단계 파이프라인을 통해 자동으로 담당자 추천
- 실시간 진행 상황 표시
-
결과 확인
- 추천된 담당자 정보 및 점수 확인
- 추천 이유 및 파이프라인 단계 확인
-
담당자 확정
- 추천된 담당자로 자동 배정
- 또는 다른 후보 선택하거나 직접 검색하여 배정
-
배부 현황 확인
- 대시보드에서 오늘 처리한 문서 수, 확정 건수 등 확인
- 최근 배부 이력 테이블에서 상세 정보 확인
이 프로젝트는 전북특별자치도의 내부 POC 프로젝트입니다.
프로젝트 루트에는 아래와 같은 추가 문서들이 있습니다:
PIPELINE.md- 7단계 파이프라인의 상세한 기술 문서NEW_ARCHITECTURE_PLAN.md- 아키텍처 설계 문서SETUP.md- 환경 설정 가이드CHECKLIST.md- 시스템 점검 및 개선 사항CHANGELOG.md- 변경 이력QDRANT_CONNECTION.md- Qdrant 연결 가이드
최종 업데이트: 2025-11-28