PDF 인보이스를 AI로 자동 분석하고, 구독 비용을 한눈에 관리하는 시스템
SaaS 구독이 늘어나면서 매달 쌓이는 인보이스 PDF를 일일이 열어보는 게 번거로웠습니다. "PDF만 넣으면 알아서 정리해주는 시스템"을 만들고 싶었고, 그 결과물입니다.
전체 인보이스 현황과 주요 지표를 한눈에 확인할 수 있습니다.
파싱된 모든 인보이스를 조회하고 검색할 수 있습니다.
개별 인보이스의 상세 정보와 항목별 내역을 확인합니다.
월별 지출 추이와 벤더별 비용 분석 차트를 제공합니다.
디렉토리 단위로 인보이스 PDF를 일괄 처리하고 진행 상황을 확인합니다.
Swagger UI를 통해 REST API를 테스트하고 문서를 확인할 수 있습니다.
- PDF 업로드 → 자동으로 인보이스 정보 추출
- 인보이스 번호, 발행사, 금액, 항목별 내역까지 구조화
- 구독 서비스의 경우 청구 주기(월간/연간) 자동 감지
- 디렉토리 단위 일괄 처리 지원
- 중복 요청 방지 및 실시간 진행률 확인
- 실패한 파일만 따로 재처리 가능
- 월별 지출 추이 차트
- 벤더별 비용 분석
- 갱신 예정 구독 알림
왜 Zerox + GPT-4o-mini인가?
처음엔 Tesseract OCR을 고려했지만, 인보이스마다 레이아웃이 달라서 정규식 기반 파싱이 한계가 있었습니다. LLM 기반 추출로 방향을 틀었고, 몇 가지 옵션 중 Zerox를 선택했습니다:
- 비용: GPT-4o-mini는 GPT-4 대비 약 1/10 비용
- 정확도: JSON Schema를 지정하면 구조화된 응답을 안정적으로 받음
- 개발 편의성: Zerox가 PDF → 이미지 변환, 멀티페이지 처리를 추상화
// apps/batch/src/parser/parser.service.ts
const result = await zerox({
filePath,
model: 'gpt-4o-mini',
extractOnly: true,
schema: invoiceSchema, // JSON Schema로 출력 형식 고정
concurrency: 5,
maxRetries: 2,
});날짜 정규화 문제
해외 SaaS 인보이스는 25.12.2024 (유럽식), 12/25/2024 (미국식) 등 포맷이 제각각입니다. 모든 날짜를 ISO 형식(YYYY-MM-DD)으로 통일하는 정규화 레이어를 추가했습니다.
왜 Bull Queue인가?
단순히 for문으로 처리할 수도 있지만, 다음 이유로 메시지 큐를 도입했습니다:
- 장애 격리: API 서버와 배치 워커 분리 → 한쪽이 죽어도 다른 쪽 영향 없음
- 재시도 정책: 지수 백오프로 일시적 실패 자동 복구
- 수평 확장: 워커만 늘리면 처리량 증가
┌─────────────┐ ┌─────────┐ ┌─────────────┐
│ API App │────▶│ Redis │────▶│ Batch Worker│
│ (트리거) │ │ (Queue) │ │ (처리) │
└─────────────┘ └─────────┘ └─────────────┘
3계층 중복 방지
같은 인보이스가 여러 번 등록되는 걸 막기 위해 3단계 검증을 구현했습니다:
| 계층 | 체크 포인트 | 구현 |
|---|---|---|
| 1. 작업 레벨 | 동일 디렉토리/파일에 대해 이미 진행 중인 작업이 있는가? | BatchJobRepository.findActiveJob() |
| 2. 큐 레벨 | Bull Queue에 고유한 jobId 지정 | parse-dir-${directory}-${id} |
| 3. 비즈니스 레벨 | 이미 등록된 인보이스 번호인가? | InvoiceRepository.existsByInvoiceNumber() |
재시도 정책
네트워크 오류나 OpenAI API 일시 장애에 대응하기 위해 지수 백오프를 적용했습니다:
{
attempts: 3,
backoff: {
type: 'exponential',
delay: 1000, // 1초 → 2초 → 4초
},
}flowchart TB
subgraph Client
Browser[웹 브라우저]
end
subgraph API["API Server :3000"]
REST[REST API]
SSR[SSR Views]
Swagger[Swagger Docs]
end
subgraph Queue["Message Queue"]
Redis[(Redis)]
Bull[Bull Queue]
end
subgraph Worker["Batch Worker :3001"]
Processor[Job Processor]
Parser[PDF Parser]
Zerox[Zerox + GPT-4o-mini]
end
subgraph Storage
PG[(PostgreSQL)]
Files[/Invoice PDFs/]
end
Browser --> REST
Browser --> SSR
REST --> Bull
Bull --> Redis
Redis --> Processor
Processor --> Parser
Parser --> Zerox
Parser --> PG
REST --> PG
SSR --> PG
Processor --> Files
| 분류 | 기술 |
|---|---|
| Runtime | Node.js, NestJS 11 |
| Database | PostgreSQL, TypeORM |
| Queue | Bull, Redis |
| AI/OCR | Zerox, OpenAI GPT-4o-mini |
| Frontend | Handlebars (SSR), Tailwind CSS, Chart.js |
| DevOps | Docker, GitHub Actions |
| Documentation | Swagger/OpenAPI |
# 1. 저장소 클론
git clone https://github.com/YOUR_USERNAME/invoice.git
cd invoice
# 2. 환경 변수 설정
cp .env.example .env
# .env 파일을 열어 OPENAI_API_KEY 설정
# 3. 실행
docker-compose up -d
# 4. 접속
# - 웹 UI: http://localhost:3000
# - API 문서: http://localhost:3000/api/docs# 사전 요구사항: Node.js 18+, PostgreSQL, Redis
# 1. 의존성 설치
npm install
# 2. 환경 변수 설정
cp .env.example .env
# DATABASE_*, REDIS_*, OPENAI_API_KEY 설정
# 3. DB 마이그레이션
npm run migration:run
# 4. 개발 서버 실행 (터미널 2개)
npm run start:api:dev # API 서버
npm run start:batch:dev # Batch 워커
# 5. 접속
# - 웹 UI: http://localhost:3000
# - API 문서: http://localhost:3000/api/docs| 변수명 | 설명 | 기본값 |
|---|---|---|
PORT |
API 서버 포트 | 3000 |
BATCH_PORT |
Batch 워커 포트 | 3001 |
DATABASE_HOST |
PostgreSQL 호스트 | localhost |
DATABASE_PORT |
PostgreSQL 포트 | 5432 |
DATABASE_USER |
PostgreSQL 사용자 | postgres |
DATABASE_PASSWORD |
PostgreSQL 비밀번호 | - |
DATABASE_NAME |
데이터베이스 이름 | invoice |
REDIS_HOST |
Redis 호스트 | localhost |
REDIS_PORT |
Redis 포트 | 6379 |
OPENAI_API_KEY |
OpenAI API 키 (필수) | - |
INVOICES_DIR |
인보이스 PDF 디렉토리 | ./invoices |
# 단위 테스트
npm run test
# 특정 파일만 테스트
npm run test -- --testPathPattern="invoices"
# 테스트 커버리지
npm run test:cov
# E2E 테스트
npm run test:e2e| Method | Endpoint | 설명 |
|---|---|---|
| GET | /api/invoices |
인보이스 목록 조회 |
| GET | /api/invoices/:id |
인보이스 상세 조회 |
| DELETE | /api/invoices/:id |
인보이스 삭제 |
| Method | Endpoint | 설명 |
|---|---|---|
| GET | /api/statistics/summary |
전체 요약 통계 |
| GET | /api/statistics/monthly |
월별 통계 |
| GET | /api/statistics/by-vendor |
벤더별 통계 |
| GET | /api/statistics/subscriptions |
활성 구독 목록 |
| Method | Endpoint | 설명 |
|---|---|---|
| POST | /api/batch/trigger |
디렉토리 동기화 트리거 |
| POST | /api/batch/trigger/file |
단일 파일 파싱 |
| GET | /api/batch/job/:jobId |
작업 상태 조회 |
전체 API 문서는
/api/docs에서 Swagger UI로 확인할 수 있습니다.
invoice/
├── apps/
│ ├── api/ # REST API + SSR 서버
│ │ └── src/
│ │ ├── invoices/ # 인보이스 CRUD
│ │ ├── statistics/ # 통계 분석
│ │ ├── batch/ # 배치 트리거
│ │ └── views/ # Handlebars 템플릿
│ │
│ └── batch/ # 배치 처리 워커
│ └── src/
│ ├── parser/ # PDF 파싱 (Zerox)
│ ├── batch/ # 큐 프로세서
│ └── commands/ # CLI 명령어
│
├── libs/
│ └── common/ # 공유 라이브러리
│ └── src/
│ ├── database/ # TypeORM 엔티티, 레포지토리
│ ├── dto/ # 공통 DTO
│ ├── interfaces/ # 인터페이스 정의
│ └── filters/ # 글로벌 예외 필터
│
└── test/ # E2E 테스트
현재 버전에서 아쉬운 부분과 개선 방향입니다:
- 인증/인가: 현재 인증 없음 → JWT 기반 인증 추가 예정
- 알림 기능: 구독 갱신 임박 시 이메일/슬랙 알림
- 다중 통화 지원: 현재 USD 기준 → 환율 적용 통합 대시보드
- PDF 미리보기: 원본 PDF를 웹에서 바로 확인
- 벤더 자동 분류: 인보이스 패턴 학습으로 카테고리 자동 태깅
이 프로젝트는 MIT 라이선스를 따릅니다.





