From d8c130a245b8792a7cc7ebed639226916a15cc1c Mon Sep 17 00:00:00 2001 From: happybell80 Date: Sat, 6 Sep 2025 00:04:23 +0900 Subject: [PATCH] docs: update naver rss playwright troubleshooting --- ...aver_rss_playwright_0900_slack_delivery.md | 136 ++++++++++-------- 1 file changed, 78 insertions(+), 58 deletions(-) diff --git a/troubleshooting/250905_happybell80_naver_rss_playwright_0900_slack_delivery.md b/troubleshooting/250905_happybell80_naver_rss_playwright_0900_slack_delivery.md index 95665ca..ceb858d 100644 --- a/troubleshooting/250905_happybell80_naver_rss_playwright_0900_slack_delivery.md +++ b/troubleshooting/250905_happybell80_naver_rss_playwright_0900_slack_delivery.md @@ -1,72 +1,92 @@ -# 평일 09:00 Slack 뉴스 전달을 위한 네이버 RSS+Playwright 수집 설계 +# 평일 09:00 Slack 헤드라인 전달: 네이버 RSS + Playwright 구현 문서 ## 작성일: 2025-09-05 -## 상태: 해결방안 확정·구현 계획 수립 +## 상태: 배포 완료(운영 검증 중) -## 목표 -- 평일 09:00에 컴퍼니엑스 Slack 채널과 각자 DM으로 당일 스타트업 뉴스를 전송한다. -- 소스는 네이버 블로그 ‘깡프로 스타트업 트렌드 연구소’의 당일 포스트다. +## 목표/배경 +- 평일 09:00에 컴퍼니엑스 Slack 채널/DM로 당일 스타트업 헤드라인(제목+링크) 자동 전송. +- 소스: 네이버 블로그 ‘깡프로 스타트업 트렌드 연구소’ 당일 포스트. +- 문제: RSS description이 중간에서 잘려(약 8개) 본문 전체를 담지 못함 → 원문(PostView iframe) 파싱 필요. -## 문제 요약 -- RSS: https://rss.blog.naver.com/startupventure.xml -- RSS `description`이 약 8개 항목에서 절단되어 본문(20개+)이 누락된다. -- 실제 본문은 블로그 페이지 내 `PostView.naver` iframe에 포함되어 정적 파싱으로는 불완전하다. +## 아키텍처(23/24 서버 맥락) +- 23: 메인(Nginx, Gitea, frontend, auth). 24: 로빙/스킬(rb8001, skill-*). +- skill-news(24, 8505): 헤드라인 생성 API 제공(HTTP). 저장/전송은 상위에서 담당. +- rb8001(24, 8001): 스케줄러(테스트 단발, 향후 09:00 등록) + Slack 전송. -## 최소 구성(가장 단순) -- 목적: 저장을 생략하고, 09:00에 “제목+링크 목록(Slack 텍스트/JSON)”만 생성해 상위가 전송. -- 입력: ENV `NAVER_RSS_URL`(기본값), 요청 `date(YYYY-MM-DD)|today`, `format(json|slack)`. -- 처리: RSS→당일 포스트 URL→Playwright로 `PostView.naver` iframe 렌더링→제목+원문 링크(+순서) 추출. -- 출력: JSON `{success,count,items:[{title,url,position}]}` 또는 Slack 텍스트(`• ` 리스트). -- 저장: 기본 비저장(가장 단순). 옵션 `STORE_MINIMAL=true` 시 Chroma에 최소 메타 저장(idempotent 보조). +## 최종 엔드포인트(구현됨) +- `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 텍스트 생성 -## 확정된 사실(코드/실측) -- skill-news 서비스 포트: 8505 (`/health`, `/api/news/...`). -- Endpoints: `/api/news/search`, `/latest`, `/summarize`, `/status`, `/process/all`, `/search/similar`. -- 데이터 영속: ChromaDB `CHROMA_DB_PATH=/app/data/chroma`, 컬렉션 `news_articles`. -- Playwright 사용 가능(Chromium headless, iframe 내 본문 접근 검증 완료). +## 파싱 로직(단순·안정화) +- 렌더링: 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은 포맷 단계에서 부여(연속번호 제약 제거). -## 해결 전략(하이브리드) -1) RSS로 당일 포스트 URL만 획득한다. -2) 해당 포스트의 `PostView.naver` iframe을 Playwright로 렌더링하여 리스트형 항목의 제목과 기사 원문 링크를 모두 추출한다. -3) 추출 항목을 중복 제거·정규화 후 최소 메타데이터로 ChromaDB에 저장(재실행 안전성 확보). -4) Slack 포맷 텍스트( `` 리스트 )와 JSON을 모두 제공한다. -5) 상위 로빙/슬랙봇이 채널/DM 전송을 담당(HTTP API만으로 연동). +## Slack 포맷(멘트/가독성) +- 헤더(오프너): 기본 “안녕하세요, 로빙입니다. 오늘 스타트업 헤드라인만 모았어요.” +- 본문: `NN. ` 행으로 정렬(번호는 01부터) +- 푸터(클로저): 기본 “가볍게 시작해 힘차게 달려봐요. 로빙이 함께합니다.” +- 줄 간격: 기본 이중 줄바꿈(ENV로 단일 전환 가능) +- 출처 표기: `출처: ` -## API 설계(HTTP 전용) -- `POST /api/news/naver/fetch-headlines` (최소 경로) - - 요청: `{ "date": "YYYY-MM-DD", "source": "startupventure", "format": "json|slack" }` - - 응답(JSON): `{ success, count, items: [{title, url}], message? }` - - 응답(Slack): `text` 필드에 불릿 리스트 문자열 포함. -- `GET /api/news/naver/latest` - - 당일/최근 포스트 기준 재생성 없이 캐시된 결과 반환. +## 환경변수(스킬) +- 필수/기본 + - `NAVER_RSS_URL`(기본: 깡뉴스 RSS), `NAVER_MAX_ITEMS`(표시 개수, 무제한은 크게 설정) + - `SCRAPE_TIMEOUT`(기본 30초) +- 포맷팅 + - `HEADLINES_OPENER`, `HEADLINES_CLOSER`, `HEADLINES_SOURCE_URL` + - `HEADLINES_DOUBLE_SPACING`(true/false) -## 파싱 규칙(함수형·환경주입) -- 입력: `rss_url`, `date`, `max_items`, `timeout`은 ENV → 요청값으로 오버라이드. -- 출력: 순수 데이터(제목, URL) 리스트. 상태 저장은 호출자(데이터 매니저)가 수행. -- 셀렉터 전략: 번호형 텍스트 패턴 탐지 + 앵커 태그의 절대 URL 보정. -- 예외: 네트워크/구조 변경 시 빈 리스트와 원인 메시지 반환(부작용 없음). +## 환경변수(로빙 rb8001) +- `SKILL_NEWS_URL`(예: http://skill-news:8505) +- `SLACK_BOT_TOKEN`(채널/DM 전송용) +- 단발 테스트: `HEADLINES_TEST_CHANNEL_ID`, `HEADLINES_TEST_RUN_AT`(예: 22:10) +- 임계치: `HEADLINES_MIN_COUNT`(전송 최소 개수) -## 스케줄링/흐름(평일 09:00) -1) 스케줄러(Gitea Actions 혹은 상위 컨트롤러)가 09:00에 `POST /api/news/naver/fetch` 호출. -2) 응답을 받아 채널용 메시지와 DM용 메시지를 구성(상위 로직). -3) Slack 전송 성공/실패 로그를 상위에서 저장. skill-news는 수집·정규화에 집중. +## 스케줄링/흐름 +1) rb8001 시작 시 테스트 단발 스케줄(예: 22:10) 등록 및 실행. +2) 운영 전환 시 평일 09:00 등록(메모리 스케줄러 또는 APScheduler). 전송은 rb8001이 수행. +3) skill-news `/health` 확인 후 `fetch-headlines` 호출 → Slack 채널/DM 전송. -## 운영 포인트 -- 재시도: 3회(지수 백오프), 실패 시 캐시 결과 반환. -- 속도제한: 항목 간 `sleep(0.5~1.0s)`로 요청 간격 조절. -- 중복: 제목+URL 해시로 식별, 이미 저장된 항목은 건너뜀. -- 관찰성: `/api/news/status`에 수집/요약 카운터와 최신 시각 반영. +## 테스트 결과(요약) +- 배포/헬스: skill-news 8505 정상, rb8001 재기동 후 스케줄 등록 로그 확인. +- 22:10 실전 전송: 채널로 전송 성공(초기엔 잡음/가독성 이슈 있었음). +- 개선 과정: + - 초기: RSS 절단 → 본문 전체 파싱으로 잡음 포함. + - 정규식 기반 번호/URL 패턴 → 0건 회귀 케이스 발생. + - HTML 앵커 우선 + 근접 텍스트 제목 생성 → 제목 정상화, 유효 URL만 유지. +- 현재: 제목이 URL로 표시되던 문제 해결, 표시 개수는 `NAVER_MAX_ITEMS`로 조정. -## 검증 시나리오(curl) -- 수집: `curl -X POST http://localhost:8505/api/news/naver/fetch -H 'Content-Type: application/json' -d '{"date":"2025-09-05","source":"startupventure","format":"slack"}'` -- 상태: `curl http://localhost:8505/api/news/status` +## 운영 가이드 +- 전송 정책: `count < 임계치`면 운영자 경고 메시지로 대체. +- 로그: “수집 N건/필터 제외 M건/최종 K건” 요약을 INFO로 기록. +- 헬스체크: 외부 핑은 TTL 캐시(예: 60초) 또는 `/healthz` 경량 엔드포인트 병행 권장. +- 실패 대응: 1→2→4초 백오프 3회 재시도, 최종 실패는 운영 채널 알림. -## 리스크 및 대응 -- 네이버 마크업 변경: 셀렉터 다중화, 본문 fallback(전체 body 텍스트) 적용. -- 일시적 차단: User-Agent/Referer 설정과 백오프 재시도. -- RSS 지연: 당일 포스트 미등재 시 전일 18시~당일 09시 범위 탐색 후 빈 결과 처리. +## 리스크/제약 +- 네이버 마크업 변경 시 셀렉터/타이밍 재조정 필요. +- 내부 링크 정책: `blog.naver.com`, `PostView.naver`는 제외, `naver.me`/언론 도메인은 허용. +- Slack 제한: 메시지 길이 한도 존재(필요 시 ‘… 외 N건’ 처리 도입 고려). -## 다음 단계 -- `naver/fetch` 엔드포인트 구현(함수형), 데이터 매니저에 아이템 최소 저장 로직 보강. -- 슬랙 포맷 메시지 빌더 추가 또는 기존 `format=slack` 경로 재사용. -- Gitea 스케줄 워크플로에 09:00 평일 트리거 추가(롤백·로그 포함). +## 검증 시나리오 +- 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 && git push` → Actions로 자동 재배포. +- 튜닝 포인트: `NAVER_MAX_ITEMS`, 줄간격, 오프너/클로저, URL 필터(내부 도메인 목록) 등 ENV로 조정. + +## 결론 +- 목표(제목+링크 헤드라인 자동 전송) 달성을 위한 최소·안정 경로를 구현 완료. +- 파싱은 HTML 앵커 우선, 포맷은 Slack 멘트/번호/간격으로 가독성 확보. +- 스케줄은 rb8001에서 관리하며, 기능은 HTTP API로만 결합(함수형·무하드코딩 원칙 준수).