본문 바로가기
AI API 활용

AI로 PDF 요약 프로그램 만들기 - 100페이지도 3분이면 끝

by 소개왕 탑백귀 2026. 4. 6.

AI로 PDF 요약 프로그램 만들기 - 100페이지도 3분이면 끝

2026년 4월 기준 | AI API 활용

요약: 업무 중 쏟아지는 PDF 보고서, 논문, 계약서를 일일이 읽을 시간이 없습니다. PyPDF2로 텍스트를 뽑고 Claude API로 요약하는 프로그램을 직접 만들어봤더니, 100페이지짜리 문서도 3분 안에 핵심만 뽑아줍니다. 전체 코드와 실측 데이터를 공유합니다.

왜 AI PDF 요약이 필요한가 - 실무 사례

회사에서 매주 월요일 아침마다 쌓여있는 PDF가 있었습니다. 시장 분석 보고서 3건, 기술 검토 문서 2건, 거래처에서 보낸 제안서 1건. 평균 50~80페이지짜리인데, 오전 회의 전까지 핵심을 파악해야 합니다.

솔직히 말하면 처음엔 ChatGPT 웹에 PDF를 올려서 요약했습니다. 그런데 문제가 있었습니다:

  • 파일 크기 제한: 대용량 PDF는 업로드가 안 되거나 중간에 잘림
  • 일괄 처리 불가: 파일을 하나씩 올려야 해서 5개 문서 처리에 30분 이상 걸림
  • 출력 포맷이 제각각: 매번 요약 형식이 달라서 비교가 어려움
  • 보안 이슈: 사내 문서를 외부 웹 서비스에 올리는 것 자체가 부담

그래서 직접 만들기로 했습니다. Python으로 PDF 텍스트를 추출하고, API로 요약하는 스크립트를 짜면 위 문제가 전부 해결됩니다. 로컬에서 돌리니 보안 걱정도 없고, 스크립트 한 번 실행이면 폴더 안의 PDF 전부를 알아서 요약해줍니다.

이번 글에서는 제가 실제로 쓰고 있는 코드를 기반으로, PDF 요약 프로그램을 처음부터 끝까지 만드는 과정을 정리합니다.

환경 설정 - PyPDF2 + Claude API 설치

필요한 패키지는 딱 3개입니다. PyPDF2로 PDF에서 텍스트를 뽑고, anthropic 라이브러리로 Claude API를 호출합니다. python-dotenv는 API 키를 환경 변수로 관리하기 위해 씁니다.

# 패키지 설치
pip install PyPDF2 anthropic python-dotenv

프로젝트 폴더를 하나 만들고, .env 파일에 API 키를 저장합니다.

# .env 파일
ANTHROPIC_API_KEY=sk-ant-api03-여기에_본인_키_입력
주의: .env 파일은 절대 Git에 올리지 마세요. .gitignore에 반드시 추가하세요. API 키가 유출되면 요금 폭탄을 맞을 수 있습니다.

Python 버전은 3.9 이상이면 됩니다. 저는 3.11 환경에서 개발했고, 3.10에서도 문제없이 동작하는 걸 확인했습니다.

그리고 폴더 구조는 이렇게 잡았습니다:

pdf_summarizer/
├── main.py              # 메인 실행 파일
├── pdf_reader.py        # PDF 텍스트 추출 모듈
├── summarizer.py        # AI 요약 모듈
├── .env                 # API 키
├── input/               # 요약할 PDF 파일
└── output/              # 요약 결과 저장

기본 PDF 텍스트 추출 코드

PDF에서 텍스트를 뽑는 건 PyPDF2로 간단합니다. 다만 실전에서 겪은 몇 가지 함정이 있어서, 처음부터 방어 코드를 넣어두는 게 좋습니다.

import PyPDF2
from pathlib import Path


def extract_text_from_pdf(file_path: str) -> dict:
    """PDF 파일에서 텍스트를 추출한다.

    Returns:
        {
            'filename': 파일명,
            'total_pages': 총 페이지 수,
            'pages': [{'page_num': 1, 'text': '...'}, ...],
            'full_text': 전체 텍스트
        }
    """
    path = Path(file_path)
    if not path.exists():
        raise FileNotFoundError(f"파일을 찾을 수 없습니다: {file_path}")

    pages = []
    full_text_parts = []

    with open(file_path, "rb") as f:
        reader = PyPDF2.PdfReader(f)
        total_pages = len(reader.pages)

        for i, page in enumerate(reader.pages):
            text = page.extract_text()
            if text and text.strip():
                pages.append({
                    "page_num": i + 1,
                    "text": text.strip()
                })
                full_text_parts.append(text.strip())

    return {
        "filename": path.name,
        "total_pages": total_pages,
        "pages": pages,
        "full_text": "\n\n".join(full_text_parts)
    }

여기서 포인트가 몇 가지 있습니다:

  • 빈 페이지 필터링: 스캔본 PDF나 이미지 기반 PDF는 텍스트가 안 뽑힙니다. text.strip()으로 빈 페이지를 걸러야 나중에 API 비용이 낭비되지 않습니다.
  • 페이지별 저장: 전체 텍스트만 저장하면 나중에 "몇 페이지에 있는 내용인지" 추적이 안 됩니다. 페이지 정보를 함께 들고 다녀야 합니다.
  • 파일 경로 검증: 실전에서 경로 오타 때문에 에러 나는 경우가 의외로 많습니다. 미리 체크하면 디버깅 시간이 줄어듭니다.
팁: 스캔본 PDF라서 텍스트가 안 뽑히면 OCR이 필요합니다. pytesseract + pdf2image 조합을 쓸 수 있는데, 이건 별도 글에서 다루겠습니다. 이번 글은 텍스트 기반 PDF에 집중합니다.

AI 요약 기능 추가 - Claude API 연동

텍스트를 뽑았으면 이제 요약할 차례입니다. Claude API를 호출해서 요약하는 함수를 만듭니다.

import anthropic
from dotenv import load_dotenv
import os

load_dotenv()
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))


def summarize_text(text: str, detail_level: str = "medium") -> str:
    """텍스트를 AI로 요약한다.

    Args:
        text: 요약할 텍스트
        detail_level: 'brief'(3줄), 'medium'(10줄), 'detailed'(섹션별)
    """
    prompts = {
        "brief": "다음 문서를 3줄 이내로 핵심만 요약해주세요.",
        "medium": "다음 문서를 10줄 이내로 요약해주세요. 핵심 주장, 주요 데이터, 결론을 포함해주세요.",
        "detailed": "다음 문서를 섹션별로 나누어 상세하게 요약해주세요. 각 섹션의 핵심 내용과 중요 수치를 포함해주세요.",
    }

    prompt = prompts.get(detail_level, prompts["medium"])

    message = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=2048,
        messages=[
            {
                "role": "user",
                "content": f"{prompt}\n\n---\n\n{text}"
            }
        ]
    )

    return message.content[0].text

모델은 claude-sonnet-4-20250514를 씁니다. 요약 작업에는 Sonnet이면 충분합니다. Opus를 쓰면 품질이 약간 올라가긴 하는데, 비용이 5배 이상 차이나서 요약 용도로는 과합니다. 제가 직접 비교해본 결과, 일반 보고서 요약에서는 Sonnet과 Opus의 품질 차이가 거의 체감되지 않았습니다.

detail_level 파라미터를 둔 이유가 있습니다. 아침 회의 전에 빠르게 훑을 때는 brief, 본격적으로 검토할 문서는 detailed로 돌립니다. 프롬프트 하나 바꾸는 것만으로 용도가 확 달라집니다.

긴 PDF 처리 - 청크 분할 요약 전략

여기가 핵심입니다. 10페이지짜리 PDF는 텍스트 전체를 한 번에 API에 넣으면 됩니다. 그런데 100페이지가 넘어가면 얘기가 다릅니다.

Claude Sonnet의 입력 컨텍스트가 200K 토큰이라 텍스트 양 자체는 들어갑니다. 하지만 문제는 다른 데 있습니다:

  • 비용: 입력 토큰이 많아지면 비용이 비례해서 올라감
  • 품질: 너무 긴 텍스트를 한번에 넣으면 중간 부분의 내용이 누락되는 경향이 있음
  • 속도: 입력이 길수록 응답 생성 시간도 길어짐

그래서 "분할 요약 후 통합" 전략을 씁니다. 큰 문서를 적당한 크기로 쪼개서 각각 요약하고, 마지막에 요약들을 다시 한번 통합 요약하는 방식입니다.

def split_into_chunks(text: str, max_chars: int = 12000) -> list:
    """텍스트를 적절한 크기의 청크로 분할한다.

    문단 단위로 끊어서 문맥이 깨지지 않도록 한다.
    """
    paragraphs = text.split("\n\n")
    chunks = []
    current_chunk = []
    current_length = 0

    for para in paragraphs:
        para_length = len(para)

        if current_length + para_length > max_chars and current_chunk:
            chunks.append("\n\n".join(current_chunk))
            current_chunk = [para]
            current_length = para_length
        else:
            current_chunk.append(para)
            current_length += para_length

    if current_chunk:
        chunks.append("\n\n".join(current_chunk))

    return chunks


def summarize_long_document(text: str) -> str:
    """긴 문서를 분할 요약 후 통합하는 전략"""

    # 짧은 문서는 바로 요약
    if len(text) < 15000:
        return summarize_text(text, "detailed")

    # 긴 문서: 청크 분할
    chunks = split_into_chunks(text, max_chars=12000)
    print(f"문서를 {len(chunks)}개 청크로 분할합니다.")

    # 각 청크 요약
    chunk_summaries = []
    for i, chunk in enumerate(chunks):
        print(f"  청크 {i+1}/{len(chunks)} 요약 중...")
        summary = summarize_text(chunk, "medium")
        chunk_summaries.append(f"[파트 {i+1}]\n{summary}")

    # 청크 요약들을 모아서 최종 통합 요약
    combined = "\n\n".join(chunk_summaries)
    print("  최종 통합 요약 생성 중...")

    final_summary = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=3000,
        messages=[
            {
                "role": "user",
                "content": ("다음은 긴 문서를 나누어 요약한 내용입니다. "
                           "이 요약들을 통합하여 전체 문서의 핵심을 "
                           "하나의 체계적인 요약으로 정리해주세요.\n\n"
                           f"{combined}")
            }
        ]
    )

    return final_summary.content[0].text

청크 크기를 12,000자로 잡은 이유가 있습니다. 한글 기준으로 토큰 변환율이 영문보다 높아서, 12,000자면 대략 6,000~8,000 토큰 정도 됩니다. 이 정도면 Sonnet이 중간 내용을 놓치지 않고 잘 요약합니다.

실험해본 결과, 청크가 너무 작으면 (5,000자 이하) 문맥이 끊겨서 요약 품질이 떨어지고, 너무 크면 (20,000자 이상) 중간 내용 누락이 생겼습니다. 12,000자가 제가 찾은 최적값입니다.

팁: 문단 단위로 끊는 게 중요합니다. 글자 수로 기계적으로 자르면 문장 중간에서 잘려서 요약 품질이 크게 떨어집니다. 위 코드에서 \n\n으로 분할하는 이유가 이것입니다.

실전 코드 - 전체 통합 프로그램

위의 모듈들을 합쳐서 실제로 쓸 수 있는 완성 프로그램입니다. 폴더 안의 PDF를 자동으로 찾아서 요약하고, 결과를 텍스트 파일로 저장합니다.

#!/usr/bin/env python3
"""PDF 자동 요약 프로그램 - Claude API 기반"""

import os
import sys
import time
from pathlib import Path
from datetime import datetime

import PyPDF2
import anthropic
from dotenv import load_dotenv

load_dotenv()

# Claude API 클라이언트 초기화
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))

# 설정값
CHUNK_SIZE = 12000          # 청크 최대 글자 수
SHORT_DOC_THRESHOLD = 15000 # 이 이하면 분할 안 함
MODEL_NAME = "claude-sonnet-4-20250514"


def extract_text(pdf_path: str) -> dict:
    """PDF에서 텍스트를 추출한다."""
    with open(pdf_path, "rb") as f:
        reader = PyPDF2.PdfReader(f)
        pages = []
        for i, page in enumerate(reader.pages):
            text = page.extract_text()
            if text and text.strip():
                pages.append({"page": i + 1, "text": text.strip()})

    full_text = "\n\n".join(p["text"] for p in pages)
    return {
        "filename": Path(pdf_path).name,
        "total_pages": len(reader.pages),
        "extracted_pages": len(pages),
        "full_text": full_text,
        "char_count": len(full_text),
    }


def split_chunks(text: str) -> list:
    """긴 텍스트를 문단 단위로 청크 분할한다."""
    paragraphs = text.split("\n\n")
    chunks, current, length = [], [], 0

    for para in paragraphs:
        if length + len(para) > CHUNK_SIZE and current:
            chunks.append("\n\n".join(current))
            current, length = [para], len(para)
        else:
            current.append(para)
            length += len(para)

    if current:
        chunks.append("\n\n".join(current))
    return chunks


def call_api(prompt: str, max_tokens: int = 2048) -> str:
    """Claude API를 호출한다."""
    response = client.messages.create(
        model=MODEL_NAME,
        max_tokens=max_tokens,
        messages=[{"role": "user", "content": prompt}]
    )
    return response.content[0].text


def summarize_document(text: str, detail: str = "medium") -> str:
    """문서를 요약한다. 길이에 따라 전략을 자동 선택."""

    system_prompts = {
        "brief": "다음 문서를 3줄 이내로 핵심만 요약하세요.",
        "medium": "다음 문서를 10줄 이내로 요약하세요. 핵심 주장, 주요 데이터, 결론을 포함하세요.",
        "detailed": "다음 문서를 섹션별로 상세하게 요약하세요. 핵심 내용과 중요 수치를 포함하세요.",
    }
    base_prompt = system_prompts.get(detail, system_prompts["medium"])

    # 짧은 문서: 직접 요약
    if len(text) < SHORT_DOC_THRESHOLD:
        return call_api(f"{base_prompt}\n\n---\n\n{text}")

    # 긴 문서: 분할 요약 후 통합
    chunks = split_chunks(text)
    print(f"    → {len(chunks)}개 청크로 분할")

    summaries = []
    for i, chunk in enumerate(chunks):
        print(f"    → 청크 {i+1}/{len(chunks)} 요약 중...")
        s = call_api(f"{base_prompt}\n\n---\n\n{chunk}")
        summaries.append(f"[파트 {i+1}]\n{s}")

    # 통합 요약
    print("    → 최종 통합 요약 생성 중...")
    combined = "\n\n".join(summaries)
    final = call_api(
        "다음은 긴 문서를 나누어 요약한 내용입니다. "
        "전체 문서의 핵심을 하나의 체계적인 요약으로 "
        f"정리해주세요.\n\n{combined}",
        max_tokens=3000
    )
    return final


def process_folder(input_dir: str, output_dir: str, detail: str = "medium"):
    """폴더 안의 모든 PDF를 처리한다."""
    input_path = Path(input_dir)
    output_path = Path(output_dir)
    output_path.mkdir(parents=True, exist_ok=True)

    pdf_files = sorted(input_path.glob("*.pdf"))
    if not pdf_files:
        print("PDF 파일이 없습니다.")
        return

    print(f"총 {len(pdf_files)}개 PDF 파일 발견\n")
    total_start = time.time()

    for idx, pdf_file in enumerate(pdf_files, 1):
        print(f"[{idx}/{len(pdf_files)}] {pdf_file.name}")
        start = time.time()

        # 1. 텍스트 추출
        try:
            doc = extract_text(str(pdf_file))
        except Exception as e:
            print(f"    ✗ 텍스트 추출 실패: {e}")
            continue

        if not doc["full_text"].strip():
            print("    ✗ 추출된 텍스트가 없습니다 (스캔본 PDF일 수 있음)")
            continue

        print(f"    {doc['total_pages']}페이지, {doc['char_count']:,}자 추출")

        # 2. 요약
        summary = summarize_document(doc["full_text"], detail)

        # 3. 결과 저장
        elapsed = time.time() - start
        result_name = pdf_file.stem + "_summary.txt"
        result_path = output_path / result_name

        with open(result_path, "w", encoding="utf-8") as f:
            f.write(f"파일: {doc['filename']}\n")
            f.write(f"페이지: {doc['total_pages']}p\n")
            f.write(f"처리 시간: {elapsed:.1f}초\n")
            f.write(f"생성 일시: {datetime.now().strftime('%Y-%m-%d %H:%M')}\n")
            f.write(f"{'='*50}\n\n")
            f.write(summary)

        print(f"    ✓ 완료 ({elapsed:.1f}초) → {result_name}")

    total_elapsed = time.time() - total_start
    print(f"\n전체 처리 완료: {total_elapsed:.1f}초")


if __name__ == "__main__":
    process_folder(
        input_dir="./input",
        output_dir="./output",
        detail="medium"   # brief / medium / detailed
    )

이 코드를 main.py로 저장하고, input 폴더에 PDF를 넣은 뒤 python main.py를 실행하면 됩니다. 실행하면 이런 식으로 진행 상황이 출력됩니다:

총 3개 PDF 파일 발견

[1/3] 시장분석_2026Q1.pdf
    45페이지, 38,210자 추출
    → 4개 청크로 분할
    → 청크 1/4 요약 중...
    → 청크 2/4 요약 중...
    → 청크 3/4 요약 중...
    → 청크 4/4 요약 중...
    → 최종 통합 요약 생성 중...
    ✓ 완료 (47.2초) → 시장분석_2026Q1_summary.txt

[2/3] 기술검토_보고서.pdf
    12페이지, 9,840자 추출
    ✓ 완료 (8.3초) → 기술검토_보고서_summary.txt

[3/3] 제안서_A사.pdf
    28페이지, 22,150자 추출
    → 2개 청크로 분할
    → 청크 1/2 요약 중...
    → 청크 2/2 요약 중...
    → 최종 통합 요약 생성 중...
    ✓ 완료 (25.6초) → 제안서_A사_summary.txt

전체 처리 완료: 81.1초

실측 결과 - 페이지별 처리 시간, 비용 비교표

실제로 다양한 길이의 PDF를 돌려보고 시간과 비용을 측정했습니다. 비용은 Anthropic API 대시보드에서 확인한 실제 수치입니다.

문서 유형 페이지 추출 글자수 청크 수 처리 시간 비용
기술 문서 10p 8,200자 1 (분할 없음) 6초 $0.008
사업 제안서 30p 24,000자 2 22초 $0.025
시장 분석 보고서 50p 41,500자 4 52초 $0.048
학술 논문 (영문) 80p 72,000자 6 1분 38초 $0.082
법률 계약서 120p 105,000자 9 2분 45초 $0.12

100페이지 전후 문서가 3분 이내에 처리됩니다. 비용도 건당 $0.1 수준이라 부담이 없습니다. 하루에 10건 돌려도 $1이 안 됩니다.

비교를 위해 다른 방법과도 비교해봤습니다:

방법 50p 문서 처리 시간 비용 일괄 처리 포맷 통일
직접 읽기 30~60분 무료 불가 해당 없음
ChatGPT 웹 5~10분 월 $20 구독 불가 불안정
유료 PDF 요약 서비스 1~3분 월 $15~30 일부 지원 서비스마다 다름
이 프로그램 52초 건당 $0.05 지원 완전 통일

시간, 비용, 편의성 모든 면에서 직접 만든 프로그램이 압도적입니다. 물론 초기 개발 시간이 들지만, 이 글의 코드를 그대로 쓰면 30분이면 셋업이 끝납니다.

활용 팁과 주의사항

몇 달간 실전에서 쓰면서 알게 된 점들을 정리합니다.

비용 더 줄이기

요약 품질에 민감하지 않은 일상 문서라면 모델을 claude-haiku-4-5-20251001로 바꾸세요. Sonnet 대비 비용이 1/10 수준이고, 일반 보고서 요약 품질은 충분합니다. 코드에서 MODEL_NAME만 바꾸면 됩니다.

# 비용 절약 모드
MODEL_NAME = "claude-haiku-4-5-20251001"   # 건당 $0.005 수준

# 품질 우선 모드 (중요 문서용)
MODEL_NAME = "claude-sonnet-4-20250514"    # 건당 $0.05 수준

에러 처리 강화

API 호출은 네트워크 상황에 따라 가끔 실패합니다. 실전에서는 반드시 재시도 로직을 넣어야 합니다.

import time


def call_api_with_retry(prompt: str, max_retries: int = 3) -> str:
    """재시도 로직이 포함된 API 호출"""
    for attempt in range(max_retries):
        try:
            response = client.messages.create(
                model=MODEL_NAME,
                max_tokens=2048,
                messages=[{"role": "user", "content": prompt}]
            )
            return response.content[0].text
        except anthropic.RateLimitError:
            wait = 2 ** attempt * 5   # 5초, 10초, 20초
            print(f"    Rate limit 도달. {wait}초 후 재시도...")
            time.sleep(wait)
        except anthropic.APIError as e:
            print(f"    API 오류: {e}")
            if attempt < max_retries - 1:
                time.sleep(3)
            else:
                raise
    raise Exception("최대 재시도 횟수 초과")

스캔본 PDF 대응

PyPDF2로 텍스트가 안 뽑히는 PDF는 이미지 기반 스캔본입니다. 이 경우 OCR을 적용해야 하는데, 간단하게 체크하는 로직을 추가해두면 좋습니다.

def check_pdf_type(pdf_path: str) -> str:
    """PDF가 텍스트 기반인지 이미지 기반인지 판별한다."""
    with open(pdf_path, "rb") as f:
        reader = PyPDF2.PdfReader(f)
        # 처음 3페이지에서 텍스트 추출 시도
        sample_pages = min(3, len(reader.pages))
        text_found = 0
        for i in range(sample_pages):
            text = reader.pages[i].extract_text()
            if text and len(text.strip()) > 50:
                text_found += 1

    if text_found == 0:
        return "image"    # OCR 필요
    elif text_found < sample_pages:
        return "mixed"    # 일부 이미지 포함
    else:
        return "text"     # 정상
주의사항 정리:
  • 보안: 사내 기밀 문서를 API로 보내기 전에 회사 보안 정책을 확인하세요. Anthropic API는 입력 데이터를 학습에 사용하지 않지만, 전송 자체가 금지된 경우도 있습니다.
  • 정확도: AI 요약은 100% 정확하지 않습니다. 계약서나 법률 문서는 반드시 원문을 확인하세요. 요약은 "어디를 봐야 하는지 가이드"로 쓰는 게 맞습니다.
  • 표와 그래프: PyPDF2는 표나 그래프의 데이터를 제대로 못 뽑는 경우가 많습니다. 표가 중요한 문서라면 pdfplumber 라이브러리를 대신 써보세요.
  • 한글 깨짐: 일부 오래된 PDF에서 한글이 깨져서 추출되는 경우가 있습니다. 이런 경우 pdfplumber가 PyPDF2보다 나은 결과를 보일 때가 많습니다.

추가로 해볼 만한 것들

이 프로그램을 기반으로 확장할 수 있는 아이디어입니다:

  • Slack 연동: 요약 결과를 Slack 채널에 자동 전송. 매일 아침 보고서 요약을 팀 채널에 올려주면 다들 좋아합니다.
  • 키워드 기반 필터링: 특정 키워드가 포함된 부분만 집중 요약. 계약서에서 "위약금", "해지" 관련 조항만 뽑아달라고 프롬프트를 바꾸면 됩니다.
  • 요약 캐싱: 같은 PDF를 다시 돌릴 때 파일 해시를 비교해서, 이미 요약한 문서는 캐시에서 가져오면 비용을 아낄 수 있습니다.
  • 비동기 처리: asyncio + anthropic.AsyncAnthropic을 쓰면 여러 청크를 동시에 요약할 수 있어서, 긴 문서의 처리 시간이 절반 이하로 줄어듭니다.

특히 비동기 처리는 효과가 큽니다. 9개 청크를 순차적으로 처리하면 2분 45초 걸리던 게, 동시에 3개씩 처리하면 1분 내로 줄어듭니다. 다만 Rate Limit에 걸릴 수 있어서, 동시 요청 수를 3~5개로 제한하는 게 좋습니다.

import asyncio
from anthropic import AsyncAnthropic

async_client = AsyncAnthropic()
semaphore = asyncio.Semaphore(3)  # 동시 요청 3개로 제한


async def summarize_chunk_async(chunk: str, idx: int) -> str:
    """비동기로 청크를 요약한다."""
    async with semaphore:
        print(f"    → 청크 {idx} 요약 시작")
        response = await async_client.messages.create(
            model=MODEL_NAME,
            max_tokens=2048,
            messages=[{
                "role": "user",
                "content": f"다음을 10줄 이내로 요약하세요.\n\n{chunk}"
            }]
        )
        return response.content[0].text


async def summarize_all_chunks(chunks: list) -> list:
    """모든 청크를 동시에 요약한다."""
    tasks = [
        summarize_chunk_async(chunk, i + 1)
        for i, chunk in enumerate(chunks)
    ]
    return await asyncio.gather(*tasks)

저는 현재 이 프로그램에 Slack 연동까지 붙여서 쓰고 있습니다. 매일 아침 7시에 크론잡으로 전날 들어온 PDF를 자동 요약하고, 팀 Slack에 올려줍니다. 월요일 아침 회의 준비 시간이 한 시간에서 10분으로 줄었습니다.

PDF 요약은 AI API를 활용하는 가장 실용적인 프로젝트 중 하나입니다. 코드 자체가 복잡하지 않으면서도 체감되는 생산성 향상이 큽니다. 위 코드를 그대로 복사해서 한번 돌려보세요. 첫 실행에서 바로 "이거 진작 만들 걸" 하는 생각이 들 겁니다.