카테고리 없음

매일 뉴스 자동수집 및 활동지 변환 파이프라인

shinypeace 2026. 4. 7. 13:11

입시 뉴스 → 수업 활동지 자동화 파이프라인 구축기

개요

베리타스알파 입시 뉴스를 자동 수집·분류하고, AI가 교사용 요약을 생성한 뒤,
HTML + DOCX 형식의 수업 활동지로 변환하는 파이프라인을 구축했다.
이 글은 기술 스택, 구현 과정, 트러블슈팅을 기록한다.


1. 파이프라인 전체 흐름

[베리타스알파 크롤링]

[링크 필터링 + 제목 추출]

[Gemini API → 교사용 요약 생성]

[결과 .txt 저장]

[HTML 활동지 생성] + [DOCX 활동지 생성]


2. 크롤링 구현

requests + BeautifulSoup 조합으로 구현했다.
핵심 포인트는 링크 필터다. find_all('a')로 전부 긁으면
네비게이션, 광고 링크까지 포함된다.
사이트별로 실제 기사 URL 패턴을 지정해서 필터링했다.

target_sites = [
    {
        "name": "베리타스알파(입시뉴스)",
        "url": "http://www.veritas-a.com/news/articleList.html?sc_section_code=S1N1",
        "link_filter": "/news/articleView"   # 기사 URL만 통과
    },
    {
        "name": "교육부 블로그(정책/정보)",
        "url": "https://if-blog.tistory.com/",
        "link_filter": "/entry/"             # 포스트 URL만 통과
    }
]

링크 보정은 urllib.parse.urljoin을 사용한다.
site['url'].rstrip('/') + link 방식은 쿼리스트링 포함 베이스 URL에서
오작동하기 때문이다.

from urllib.parse import urlparse, urljoin

def get_base_url(url):
    parsed = urlparse(url)
    return f"{parsed.scheme}://{parsed.netloc}"

link = urljoin(get_base_url(site['url']), raw_link)

3. Gemini API 연동 및 Rate Limit 처리

문제 상황

gemini-2.0-flash free tier 사용 시 첫 번째 요청에서 즉시 429 발생.
429 RESOURCE_EXHAUSTED
Quota exceeded: GenerateRequestsPerDayPerProjectPerModel-FreeTier

해결 전략: 모델 폴백 + 자동 재시도

MODELS = ['gemini-1.5-flash', 'gemini-1.5-flash-8b', 'gemini-1.0-pro']

def call_gemini_with_retry(prompt, max_retries=3):
    for model in MODELS:
        for attempt in range(max_retries):
            try:
                res = client.models.generate_content(
                    model=model, contents=prompt
                )
                return res.text.strip()
            except Exception as e:
                if '429' in str(e):
                    # 에러 메시지에서 retryDelay 파싱
                    delay_match = re.search(r'retryDelay.*?(\d+)s', str(e))
                    wait = int(delay_match.group(1)) + 2 if delay_match else (attempt + 1) * 10
                    print(f"[{model}] {wait}초 대기 후 재시도 ({attempt+1}/{max_retries})")
                    time.sleep(wait)
                else:
                    break  # 429가 아닌 에러는 다음 모델로
        time.sleep(5)
    return None

프롬프트 설계

단일 프롬프트로 교사용 분류, 요약, 현장 적용 메모를 한 번에 생성했다.
항목을 고정 포맷으로 지정해 후처리 없이 바로 파일에 기록할 수 있도록 했다.

prompt = f"""
다음 입시 뉴스 제목을 고등학교 진학지도 교사용으로 분석해줘.
제목: {title}

아래 항목을 순서대로 작성해:
분류: [대입제도/논술/전형변화 중 택1] / [세부분류] / 활용도 [높음/중/낮음]
교사용 제목: (25자 이내)
핵심 내용 1줄 요약: (40자 이내)
진학지도 포인트: (현장 적용 관점에서)
2027·2028 대입 연결성: (직접/간접/약함)
학교 현장 적용 메모: (수업·상담 활용법)
"""

4. HTML 활동지 생성

순수 HTML + CSS + vanilla JS로 구현했다.
외부 라이브러리 없이 인쇄 및 PDF 저장이 가능하도록 설계했다.

핵심 기능

주제 탭 전환

function selectTopic(btn, id) {
    document.querySelectorAll('.topic-btn')
            .forEach(b => b.classList.remove('active'));
    document.querySelectorAll('.topic-content')
            .forEach(c => c.classList.remove('active'));
    btn.classList.add('active');
    document.getElementById('content-' + id).classList.add('active');
}

contenteditable 기반 작성 영역

<div class="write-box"
     contenteditable="true"
     data-placeholder="내용을 여기에 작성하세요…">
</div>
.write-box:empty::before {
    content: attr(data-placeholder);
    color: #BBB;
    pointer-events: none;
}

인쇄 최적화

@media print {
    .topic-selector, .action-bar { display: none !important; }
    .section { break-inside: avoid; }
    .topic-content.active { display: block !important; }
}

5. DOCX 활동지 생성

docx npm 패키지를 사용했다. (npm install -g docx)

주요 트러블슈팅

① 테이블 렌더링 깨짐

columnWidths와 각 셀의 width를 모두 DXA로 명시해야 한다.
WidthType.PERCENTAGE는 Google Docs에서 깨진다.

new Table({
    width: { size: 9026, type: WidthType.DXA },
    columnWidths: [4513, 4513],  // 반드시 합계 = 전체 width
    rows: [new TableRow({ children: [
        new TableCell({
            width: { size: 4513, type: WidthType.DXA },  // 셀에도 명시 필수
            borders,
            margins: { top: 80, bottom: 80, left: 120, right: 120 },
            children: [new Paragraph({ children: [new TextRun("내용")] })]
        })
    ]})]
})

② 테이블 배경색 검게 나오는 문제

ShadingType.SOLID가 아닌 ShadingType.CLEAR를 써야 한다.

// ❌ 검은 배경 나옴
shading: { fill: "E8F5EE", type: ShadingType.SOLID }

// ✅ 정상
shading: { fill: "E8F5EE", type: ShadingType.CLEAR }

③ 줄바꿈

\n 문자는 docx에서 무시된다. 별도 Paragraph로 분리해야 한다.

// ❌
new TextRun("첫 줄\n두 번째 줄")

// ✅
new Paragraph({ children: [new TextRun("첫 줄")] }),
new Paragraph({ children: [new TextRun("두 번째 줄")] })

6. 최종 출력물

파일 용도
교사용_입시데이터_통합본.txt AI 분석 원본 저장
입시뉴스_활동지.html 브라우저 열람 / PDF 저장 / 인쇄
입시뉴스_활동지.docx Word 편집 / Google Docs 연동 / Classroom 배포

7. 향후 개선 방향

  • 스케줄러 연동: cron 또는 GitHub Actions로 주 1회 자동 실행
  • Google Drive API: 생성된 DOCX를 Drive에 자동 업로드 후 공유 링크 반환
  • 학교별 커스터마이징: 교사가 관심 주제를 선택하면 해당 섹션만 생성
  • Gemini 유료 전환: free tier 한계 해결, 일 500건 이상 처리 가능