# 평일 09:00 Slack 헤드라인 전달: 네이버 RSS + Playwright 구현 문서

## 작성일: 2025-09-05
## 상태: 배포 완료(운영 확인 2025-09-06)

## 목표/배경
- 평일 09:00에 컴퍼니엑스 Slack 채널/DM로 당일 스타트업 헤드라인(제목+링크) 자동 전송.
- 소스: 네이버 블로그 ‘깡프로 스타트업 트렌드 연구소’ 당일 포스트.
- 문제: RSS description이 중간에서 잘려(약 8개) 본문 전체를 담지 못함 → 원문(PostView iframe) 파싱 필요.

## 아키텍처(23/24 서버 맥락)
- 23: 메인(Nginx, Gitea, frontend, auth). 24: 로빙/스킬(rb8001, skill-*).
- skill-news(24, 8505): 헤드라인 생성 API 제공(HTTP). 저장/전송은 상위에서 담당.
- rb8001(24, 8001): 스케줄러(테스트 단발, 향후 09:00 등록) + Slack 전송.

## 최종 엔드포인트(구현됨)
- `POST /api/news/naver/fetch-headlines`
  - 요청: `{ "format": "json|slack" }` (기본 json)
  - 응답(json): `{ success, count, items:[{title,url}], source_post_url }`
  - 응답(slack): `{ success, count, text, items, source_post_url }`
  - 동작: RSS→최신 포스트 링크→PostView iframe 렌더링→본문 컨테이너에서 제목/URL 추출→Slack 텍스트 생성

## 파싱 로직(단순·안정화)
- 렌더링: Playwright(Chromium, headless)로 PostView 본문 컨테이너 텍스트/HTML 확보.
- 추출 규칙(HTML 우선):
  - 본문 전체의 모든 `a[href]`를 순회.
  - URL 검증(urlparse netloc 존재, 내부 뷰어/블로그 제외: `postview.naver`, `blog.naver.com`).
  - 제목은 해당 앵커의 가장 가까운 블록(`p/li/div/span`) 텍스트에서 앵커 텍스트·URL·선행번호 제거 후 생성.
  - 비어 있으면 앵커 텍스트 사용(텍스트가 URL이면 제외). 80자 초과 시 말줄임.
- 보조(텍스트 기반): HTML 추출 실패 시 텍스트 패턴에서 URL 인접 텍스트로 보조 매칭.
- 번호: 1..N은 포맷 단계에서 부여(연속번호 제약 제거).

## Slack 포맷(멘트/가독성)
- 헤더(오프너): 기본 “안녕하세요, 로빙입니다. 오늘 스타트업 헤드라인만 모았어요.”
- 본문: `NN. <URL|제목>` 행으로 정렬(번호는 01부터)
- 푸터(클로저): 기본 “가볍게 시작해 힘차게 달려봐요. 로빙이 함께합니다.”
- 줄 간격: 기본 이중 줄바꿈(ENV로 단일 전환 가능)
- 출처 표기: `출처: <https://rss.blog.naver.com/startupventure.xml|네이버 깡뉴스>`

## 환경변수(스킬)
- 필수/기본
  - `NAVER_RSS_URL`(기본: 깡뉴스 RSS), `NAVER_MAX_ITEMS`(표시 개수, 무제한은 크게 설정)
  - `SCRAPE_TIMEOUT`(기본 30초)
- 포맷팅
  - `HEADLINES_OPENER`, `HEADLINES_CLOSER`, `HEADLINES_SOURCE_URL`
  - `HEADLINES_DOUBLE_SPACING`(true/false)

## 환경변수(로빙 rb8001) - 운영 설정 확인됨
- `HEADLINES_TEST_CHANNEL_ID`=C0920L68267 (실제 채널)
- `HEADLINES_TEST_RUN_AT`=09:00 (KST)
- `HEADLINES_SCHEDULE_DAYS`=mon-fri (평일만)
- `HEADLINES_MIN_COUNT`=1 (모든 뉴스 전송)
- `SLACK_BOT_TOKEN`=xoxb-... (설정됨)

## 스케줄링/흐름 (APScheduler 적용 완료)
1) rb8001 시작 시 APScheduler가 cron job 자동 등록(평일 09:00, 서버 재시작 불필요).
2) 환경변수: `HEADLINES_SCHEDULE_DAYS`(mon-fri), `HEADLINES_TEST_NOW`(즉시 테스트).
3) 관리 API: `/api/schedule/list`(목록), `/api/schedule/test`(즉시 실행).

## 테스트 결과(요약)
- 배포/헬스: skill-news 8505 정상, rb8001 재기동 후 스케줄 등록 로그 확인.
- 22:10 실전 전송: 채널로 전송 성공(초기엔 잡음/가독성 이슈 있었음).
- 개선 과정:
  - 초기: RSS 절단 → 본문 전체 파싱으로 잡음 포함.
  - 정규식 기반 번호/URL 패턴 → 0건 회귀 케이스 발생.
  - HTML 앵커 우선 + 근접 텍스트 제목 생성 → 제목 정상화, 유효 URL만 유지.
- 현재: 제목이 URL로 표시되던 문제 해결, 표시 개수는 `NAVER_MAX_ITEMS`로 조정.

## 운영 가이드
- 전송 정책: `count < 임계치`면 운영자 경고 메시지로 대체.
- 로그: “수집 N건/필터 제외 M건/최종 K건” 요약을 INFO로 기록.
- 헬스체크: 외부 핑은 TTL 캐시(예: 60초) 또는 `/healthz` 경량 엔드포인트 병행 권장.
- 실패 대응: 1→2→4초 백오프 3회 재시도, 최종 실패는 운영 채널 알림.

## 리스크/제약
- 네이버 마크업 변경 시 셀렉터/타이밍 재조정 필요 (자동 모니터링 없음).
- 내부 링크 정책: `blog.naver.com`, `PostView.naver`는 제외, `naver.me`/언론 도메인은 허용.

## 검증 시나리오
- Slack 텍스트:
  - `curl -X POST http://localhost:8505/api/news/naver/fetch-headlines -H 'Content-Type: application/json' -d '{"format":"slack"}'`
- JSON:
  - `curl -X POST http://localhost:8505/api/news/naver/fetch-headlines -H 'Content-Type: application/json' -d '{"format":"json"}' | python3 -m json.tool`

## 롤백/튜닝
- 롤백: skill-news 최근 커밋 `git revert <hash> && git push` → Actions로 자동 재배포.
- 튜닝 포인트: `NAVER_MAX_ITEMS`, 줄간격, 오프너/클로저, URL 필터(내부 도메인 목록) 등 ENV로 조정.

## 결론
- 목표(제목+링크 헤드라인 자동 전송) 달성을 위한 최소·안정 경로를 구현 완료.
- 파싱은 HTML 앵커 우선, 포맷은 Slack 멘트/번호/간격으로 가독성 확보.
- 스케줄은 rb8001에서 관리하며, 기능은 HTTP API로만 결합(함수형·무하드코딩 원칙 준수).
