DMF_Crawler/docs/research/02-benchmark-github-projects.md
Yun Chan 56a6e2da93 chore: 저장소 구조 정리 및 문서화, 첫 커밋
- src/dist 산출물 분리 원칙 정리(.gitignore, .gitattributes)
- 루트 및 주요 폴더(config/scripts/prompts/tests/src, 런타임 폴더 5종)에
  안내용 README.md 추가
- CHANGELOG.md, LICENSE, docs/ops/05-release-and-versioning.md 추가
- docs/README.md 문서 지도 갱신
2026-09-04 09:25:44 +09:00

3003 lines
205 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 유사 프로젝트 벤치마킹과 채택 구조
> **이 문서의 역할**: DMF_Crawler 를 스크래치에서 만들기 전에, 실제로 존재가 확인된 GitHub 저장소 37개 이상을 범주별로 해부하여 "무엇을 베끼고 무엇을 버릴지"를 확정하고, 그 근거 위에서 최종 디렉터리 구조와 모듈 경계(fetch / parse / diff / store / report / notify / orchestrate)를 SSOT 로 못박는 문서다.
---
## 0. 한눈에 보기
이 문서에서 내린 결론(상세 근거는 각 섹션에):
- **한국 식약처(MFDS) DMF 전용 오픈소스는 존재하지 않는다.** 30회 이상의 검색과 68회의 실제 페이지 fetch 로도 `nedrug`/`MFDS`/`DMF` 를 대상으로 한 크롤러 저장소를 하나도 찾지 못했다. 가장 근접한 것이 `Q00/data.go.kr-crawling`(★4, DUR 품목정보 API → xlsx)뿐이다. **즉 fetch/parse 계층은 우리가 직접 쓴다. 베낄 것은 "구조"이지 "코드"가 아니다.**
- **1차 데이터 소스는 스크래핑이 아니라 공공 API 로 확정한다.** `식품의약품안전처_원료의약품등록(DMF)현황` OpenAPI 엔드포인트 `https://apis.data.go.kr/1471000/MdcDmfInfoService01/getMdcDmfList01``DMF_PERMIT_NO`, `INGR_KOR_NAME`, `ENTP_NAME`, `MNFCTR_NAME`, `MNFCTR_PLACE`, `MANUF_COUNTRY_CODE_NM`, `DMF_PERMIT_DATE` 를 JSON/XML 로 준다. 공고 게시판(`nedrug.mfds.go.kr/bbs/117`, 총 710건)은 API 가 놓치는 "변경/취하" 서사를 보완하는 2차 소스다.
- **아키텍처의 정본 레퍼런스는 `mrueda/nomenclator-delta`다.** 스페인 보건부 의약품 목록의 월간 델타를 추적하는 프로젝트로, `수집(collection) → 정규화(normalization) → 디핑(diffing) → 검증(validation)` 이라는 4단계 모듈 분리와 `data/`(스냅샷 + 변경 이력) 레이아웃이 우리 요구사항과 1:1 로 대응한다. **모듈 경계는 이걸 그대로 채택한다.**
- **범용 변경감지 도구(changedetection.io ★33.5k, urlwatch ★3.1k, huginn ★49.9k)는 "도입"하지 않고 "설계 개념만" 가져온다.** 이들은 텍스트 블록 diff 도구지 레코드 키 기반 diff 도구가 아니다. 우리에게 필요한 것은 `DMF_PERMIT_NO` 를 키로 한 added/removed/changed 판정이고, 그 형태는 `larsyencken/csvdiff`(★131, `_index`/`added`/`removed`/`changed` JSON 구조)가 정답이다. 다만 csvdiff 는 2021-02-18 아카이브되었으므로 **JSON 스키마만 채택하고 구현은 자체 작성**한다.
- **운영 방식은 "Windows Task Scheduler(schtasks) + 상주 워치독" 2단 구조로 간다.** `michalzobec/autorunsalerts` 가 검증한 패턴 — SYSTEM 컨텍스트 스캔 태스크와 사용자 컨텍스트 토스트 태스크를 분리 — 를 그대로 채택한다. 토스트는 SYSTEM/서비스 세션에서 뜨지 않기 때문이다(BurntToast 문서가 명시).
- **서비스화는 WinSW(★14.3k, `v2.12.0` 안정)를 1순위로 한다.** `<onfailure action="restart" delay="10 sec"/>` + `<resetfailure>1 hour</resetfailure>` + `<startmode>Automatic</startmode>` + `<log mode="roll">` 이 XML 한 장에 선언적으로 들어가고 .NET 외 런타임 의존이 없다. NSSM(★1.2k, v2.24 / 2014-08-31)은 대안, pywin32 서비스는 기각(복구 액션이 트리거되지 않는 알려진 결함 — pywin32 issue #1563).
- **AI CLI headless 호출 규약은 `claude -p` / `gemini -p` / `codex exec` 3종의 공통 패턴을 `agy -p` 로 사상한다**: (1) 프롬프트를 파일로 빼서 `-p "$(cat prompt.txt)"`, (2) `--output-format json` + JSON Schema 로 구조화 출력 강제, (3) stdout 을 파일로 리다이렉트 후 파싱, (4) 종료코드로 분기. `agy` 자체의 플래그는 raw dump 에서 확인되지 않았으므로 **⚠️ 미검증**이며 부트스트랩 단계에서 `agy --help` 로 실측해야 한다.
- **리포트는 xlsxwriter 로 만들고, 탭 간 연동은 `write_url(row, col, 'internal:Sheet2!A1')` 로 구현한다.** openpyxl 대신 xlsxwriter 를 쓰는 이유는 `add_table()` / `conditional_format()` / 내부 하이퍼링크가 한 API 로 깔끔하게 나오기 때문이다(`Bwhiz/Auto-Excel-Reports` 는 openpyxl 예시이나 서식 품질 요구가 우리보다 낮다).
- **알림은 Apprise(★17.2k) 한 겹으로 추상화한다.** `windows://`(pywin32 필요, 250자 제한, 같은 PC 한정), `tgram://bottoken/ChatID`, `slack://`, `mailto://` 를 URL 문자열 하나로 갈아끼울 수 있다. 다만 리치 토스트(버튼/이미지)는 `win11toast`(★333) 로 직접 호출하는 이중 경로를 둔다.
- **최종 디렉터리 구조는 `src/dmf_crawler/{fetch,parse,diff,store,report,notify,orchestrate}` + `data/{raw,snapshots,history}` + `ops/{winsw,tasks,watchdog}` + `config/sources.yaml` 이다.** 각 경로의 출처는 §10 에 저장소별로 명기했다.
---
## 1. 목차
- [0. 한눈에 보기](#0-한눈에-보기)
- [1. 목차](#1-목차)
- [2. 조사 방법과 신뢰도 표기 규칙](#2-조사-방법과-신뢰도-표기-규칙)
- [3. 전체 저장소 인덱스 (실존 확인 37건)](#3-전체-저장소-인덱스-실존-확인-37건)
- [4. (a) 한국 식약처 / 공공데이터 크롤러](#4-a-한국-식약처--공공데이터-크롤러)
- [5. (b) FDA / openFDA / 규제 데이터](#5-b-fda--openfda--규제-데이터)
- [6. (c) 규제 변경 감지 · 인텔리전스](#6-c-규제-변경-감지--인텔리전스)
- [7. (d) 범용 변경 감지 도구](#7-d-범용-변경-감지-도구)
- [8. (e) 크롤링 → 엑셀/시트 리포트 파이프라인](#8-e-크롤링--엑셀시트-리포트-파이프라인)
- [9. (f) AI CLI headless 자동화](#9-f-ai-cli-headless-자동화)
- [10. (g) Windows 서비스화 · 워치독 · 토스트](#10-g-windows-서비스화--워치독--토스트)
- [11. (h) awesome 리스트 및 기타 참고](#11-h-awesome-리스트-및-기타-참고)
- [12. 채택 결정 표 (채택 / 부분채택 / 기각)](#12-채택-결정-표-채택--부분채택--기각)
- [13. 최종 디렉터리 구조 제안](#13-최종-디렉터리-구조-제안)
- [14. 모듈 경계 제안과 입출력 계약](#14-모듈-경계-제안과-입출력-계약)
- [15. 데이터 소스 실측 정보 (DMF API / 공고 게시판)](#15-데이터-소스-실측-정보-dmf-api--공고-게시판)
- [부록 A. 출처 목록](#부록-a-출처-목록)
- [부록 B. 미해결 질문 / 실측 필요 항목](#부록-b-미해결-질문--실측-필요-항목)
---
## 2. 조사 방법과 신뢰도 표기 규칙
원본 리서치는 WebSearch 33회 + WebFetch 68회로 수행되었다(WebSearch 예산 200/200 소진으로 #31, #32, #33 검색은 미수행). 본 문서는 그 raw dump 를 손실 없이 정제한 것이다.
| 표기 | 의미 |
|---|---|
| **[F#n]** | raw dump 의 `[FETCH #n]` — 해당 페이지를 실제로 열어 확인함 (verified_by_fetch=true) |
| **[S#n]** | raw dump 의 `[SEARCH #n]` — 검색 결과 링크로만 등장. 페이지를 직접 열지 않음 |
| **⚠️ 미검증** | 존재/수치를 직접 확인하지 못함. 삭제하지 않고 남기되 구현 전 실측 필요 |
스타 수·커밋 수는 **2026-09-02 조사 시점 기준**이며, GitHub 페이지에 "last commit date" 가 텍스트로 노출되지 않은 경우 커밋 총수로 대체 기록했다(원 dump 가 그렇게 기록했으므로 그대로 보존).
---
## 3. 전체 저장소 인덱스 (실존 확인 37건)
아래는 **WebFetch 로 저장소 페이지를 실제로 열어 확인한** 항목이다. 범주 기호는 원 조사 항목 (a)~(h) 를 따른다.
| # | 저장소 | URL | ★ | Fork | 언어 | 최근 활동 | 무엇을 하는가 | **이 프로젝트에서 정확히 무엇을 베낄 것인가** |
|---|---|---|---|---|---|---|---|---|
| 1 | `Q00/data.go.kr-crawling` | https://github.com/Q00/data.go.kr-crawling | 4 | 0 | Python | development 브랜치 55 commits | 건강정보·의약품 크롤링, gevent 멀티스레딩 표방 | `config.py.example` 로 API 키를 코드 밖으로 빼는 패턴, `column.py` 로 응답 필드명↔한글명 매핑을 **생성해서 파일로 떨구는** 아이디어, `page = int(totalCount/100) + 1` 페이지네이션 공식 |
| 2 | `jjscan/data.go.kr-1` | https://github.com/jjscan/data.go.kr-1 | 0 | 0 | R | 미표기 | data.go.kr MFDS `DURPrdlstInfoService`/`getUsjntTabooInfoList` 수집 | **실패 사례 카탈로그**: 연결 실패→폴링 재시도, `totalCount == 0` 로 빈 응답 판정, 351,010건 단일스레드 17시간 병목. 우리 재시도/공백판정 로직의 근거 |
| 3 | `WooilJeong/PublicDataReader` | https://github.com/WooilJeong/PublicDataReader | 597 | 113 | Python | 168 commits | 공공데이터포털/KOSIS/ECOS 등 조회 파이썬 라이브러리 | 공공 API 래퍼의 **패키지 레이아웃과 provider 별 모듈 분리**. 단 식약처/의약품 커버리지는 **없음**(문서 명시) → 의존하지 않고 구조만 참고 |
| 4 | `NomaDamas/k-skill` | https://github.com/NomaDamas/k-skill | 7.4k | — | — | — | 한국 특화 스킬 모음. `docs/features/mfds-food-safety.md` + `scripts/mfds_food_safety.py` | **API 키를 사용자 머신이 아니라 프록시 서버 환경변수(`DATA_GO_KR_API_KEY`, `FOODSAFETYKOREA_API_KEY`)에 두는 분리 원칙**. 우리는 로컬 단독이므로 `.env` 로 대체하되 "키를 코드/리포지토리에 절대 넣지 않는다"는 규범만 채택 |
| 5 | `FDA/openfda` | https://github.com/FDA/openfda | 705 | 166 | Python | — | FDA 공식. Luigi 파이프라인으로 공개 데이터셋 → JSON → Elasticsearch | **`openfda/` 패키지 + `schemas/` + `config/` + `scripts/` 4분할 레이아웃**. 데이터셋별 파이프라인 파일 분리(NSDE, CAERS, Substance, Device Clearance, Device PMA, Device Event) |
| 6 | `jbremz/FDA-Analysis` | https://github.com/jbremz/FDA-Analysis | 5 | 2 | Python | — | (은퇴한) Drugs@FDA 사이트를 Scrapy 로 22,000+ 제품 스크래핑 후 pandas 분석 | **"스파이더 디렉터리 / 원시 CSV / 분석 스크립트 / 노트북" 4분할**. 우리 `data/raw``notebooks/` 분리의 근거 |
| 7 | `logiover/fda-data-scraper` | https://github.com/logiover/fda-data-scraper | 0 | 0 | 미표기 | 1 commit | openFDA 9개 데이터셋 → JSON/CSV/XLSX/JSONL/XML/HTML | **출력 포맷을 하나의 CLI 인자로 스위칭하는 설계**. 우리도 `--format xlsx|csv|json` 을 단일 report 모듈에서 처리 |
| 8 | `coderxio/OpenFDA` | https://github.com/coderxio/OpenFDA | 3 | 3 | Python | — | openFDA drug NDC 데이터셋을 DB 에 적재, CherryPy 로 서빙 | `docker-compose.yml` + `docker-compose.override.yml`**최초적재/운영 구성 분리**. 우리는 Docker 미사용이나 "초기 백필 실행"과 "일일 증분 실행"을 다른 엔트리포인트로 분리하는 개념을 채택 |
| 9 | `Tanguy9862/AI-Powered-FDA-Drug-Scraper` | https://github.com/Tanguy9862/AI-Powered-FDA-Drug-Scraper | 3 | 0 | Python | — | Drugs.com 신약 승인 페이지 스크래핑 → LangChain+GPT-4o-mini 분류 (1,770건) | **`scraper.py` / `classification.py` / `utils.py` 3파일 분리** — 수집과 LLM 후처리를 절대 한 파일에 섞지 않는다. 회사명 표기 정규화(약 1000→700종)의 필요성 근거 |
| 10 | `anton-semerenko/pharma-radar` | https://github.com/anton-semerenko/pharma-radar | 0 | 0 | Python | 2 commits | Claude(Opus급) 에이전트가 매일 06:00 규제/경쟁 인텔리전스 브리핑 생성 → Telegraph + Telegram | **가장 유사한 프로젝트.**`prompts/system_prompt.md` 로 에이전트 방법론을 파일로 분리 ② `config/sources.yaml` 로 소스 계층·루브릭 정의 ③ `src/deliver.py` 는 표준 라이브러리만 사용 ④ **"≥2개 독립 출처 또는 1개 공식 규제 1차 출처"라는 검증 정책** ⑤ 매일 06:00 스케줄 — 우리 요구사항과 동일 |
| 11 | `mrueda/nomenclator-delta` | https://github.com/mrueda/nomenclator-delta | 1 | 0 | Python | Updated Aug 9, 2026 | 스페인 보건부 Nomenclátor 의약품 목록의 **월간 델타**(추가/삭제/변경) 비교 | **아키텍처 정본.** `src/nomenclator_delta/`(collection·normalization·diffing·validation), `data/`(스냅샷+변경이력), `site/`(정적 앱), `docs-site/`, `tests/`. CLI 는 `python3 -m nomenclator_delta validate data` / `python3 -m nomenclator_delta dist` 형태의 서브커맨드 |
| 12 | `Mzands2622/Zanalytix` | https://github.com/Mzands2622/Zanalytix | 0 | 0 | Python | Updated Mar 9, 2026 / 1 commit | 60+ 제약사 파이프라인 페이지 스크래핑 → GPT-4o 로 old vs new 비교, 우선순위 1~5 부여 | **① 소스별 파서 모듈을 1파일 1소스로 쪼개는 규칙(`{company}_pipeline.py`, `fetch_{company}_html()` / `process_{company}_html()` 시그니처 통일) ② 날짜 스냅샷을 JSON 으로 보관 ③ 변경건에 우선순위 점수를 매겨 알림 대상 선별** |
| 13 | `suriyadeepan/WebScraping-for-Healthcare` | https://github.com/suriyadeepan/WebScraping-for-Healthcare | 8 | 1 | Python | 44 commits | DrugBank·ClinicalTrials.gov·EMC(SMPC/PIL)·HPRA·MHRA 등 규제/의약 데이터 수집 | `phscrape` 패키지 안에서 소스별 모듈이 **동일한 `fetch()` / `crawl_k()` 인터페이스**를 노출하는 규약 |
| 14 | `dgtlmoon/changedetection.io` | https://github.com/dgtlmoon/changedetection.io | 33.5k | 2.0k | Python | 2,448 commits / release `0.55.8` (13 Jul 09:26) | 웹페이지 변경 감지·알림 SaaS/셀프호스트 | **개념만**: CSS/XPath/JSONPath/jq 로 감시 범위를 좁히는 필터 체인, word/line/character 3단계 diff 시각화, 타임존 인식 스케줄(요일·시간 제한), Apprise 알림, `{{diff}}`/`{{diff_added}}`/`{{diff_removed}}` 템플릿 토큰 |
| 15 | `thp/urlwatch` | https://github.com/thp/urlwatch | 3.1k | 354 | Python | 974 commits / **릴리스 없음** | URL·셸 명령 출력의 변경을 감시하고 unified diff 로 통지 | **`urls.yaml` 잡 정의 스키마**(url/name/method/data/headers/cookies/encoding/filter/ignore_connection_errors), `job_defaults` 로 공통 설정 상속, 28종 내장 필터 체인 개념, `diff_filter` 로 diff 결과 자체를 후처리하는 발상 |
| 16 | `huginn/huginn` | https://github.com/huginn/huginn | 49.9k | 4.3k | Ruby | 4,134 commits | 에이전트가 웹을 읽고 이벤트를 만들어 유향 그래프로 전파 | `WebsiteAgent`**`mode: all / on_change / merge`** 3분류 — 우리 diff 모듈의 출력 모드와 정확히 대응. 기본 스케줄 `every_12h`. `extract` 설정이 선언적 추출 스펙을 데이터로 표현 |
| 17 | `larsyencken/csvdiff` | https://github.com/larsyencken/csvdiff | 131 | 31 | Python | **2021-02-18 아카이브** | 두 CSV 를 키 기준으로 비교해 added/removed/changed 산출 | **JSON 출력 스키마를 그대로 채택**: `_index`(키 컬럼 배열), `added`, `removed`, `changed`(키별 field-level from/to). CLI 옵션 `--style=summary\|pretty`, `--output`, `--ignore-columns`, `--significance` 도 우리 CLI 에 이식 |
| 18 | `ecprice/newsdiffs` | https://github.com/ecprice/newsdiffs | 506 | 136 | Python (Django) | — | 뉴스 기사 변경 이력 추적 프레임워크 | `parsers/` 아래 `BaseParser` 상속 사이트별 서브클래스 구조, 진행 로그와 에러 로그를 **분리** 기록(`/tmp/newsdiffs_logging` per-run vs `/tmp/newsdiffs/logging_errs` cumulative) |
| 19 | `simonw/git-scraper-template` | https://github.com/simonw/git-scraper-template | 132 | 10 | — | — | GitHub Actions 로 URL 을 주기적으로 받아 변경 시 커밋(Git scraping) | **"스냅샷을 버전관리에 커밋해 변경 이력 자체를 만든다"는 발상**. 우리는 GitHub Actions 대신 로컬 Task Scheduler + 로컬 git 리포로 `data/snapshots` 를 커밋 |
| 20 | `Bwhiz/Auto-Excel-Reports` | https://github.com/Bwhiz/Auto-Excel-Reports | 1 | 0 | Python | — | openpyxl 로 엑셀 리포트 생성 + GitHub Actions cron + SMTP 발송 | `report_script.py`(생성) / `auto_mail.py`(배포) **분리**, cron `0 0 * * *`, 자격증명은 GitHub Secrets → 우리는 `.env` |
| 21 | `HasData/playwright-scraping` | https://github.com/HasData/playwright-scraping | 15 | 4 | Python/Node.js | 5 commits | Playwright 스크래핑 레시피 모음 | **디렉터리 분류 자체가 체크리스트**: `basics/ scraping/ selectors/ interactions/ save_data/ auth/ browser/ errors/ debug/`. 특히 `errors/`(재시도·타임아웃)와 `debug/`(video/trace 녹화)를 우리 fetch 모듈 설계 항목으로 채택 |
| 22 | `jshchnz/claude-code-scheduler` | https://github.com/jshchnz/claude-code-scheduler | 510 | 37 | TypeScript | — | AI CLI 를 OS 네이티브 스케줄러에 등록해 `claude -p` 자동 실행 | **① OS별 스케줄러 어댑터 분리(`src/schedulers/base.ts`, `darwin.ts`, `linux.ts`, `windows.ts`, `index.ts`) ② 스케줄 정의를 JSON 파일로(`.claude/schedules.json`) ③ 로그를 태스크 ID별 파일로(`~/.claude/logs/<task-id>.log`)**. Windows 는 Task Scheduler 사용 |
| 23 | `addyosmani/gemini-cli-tips` | https://github.com/addyosmani/gemini-cli-tips | 2.4k | 105 | — | — | Gemini CLI 팁 모음 | headless `gemini -p "..."`, stdin 파이프, `--format=json`, `GEMINI_SYSTEM_MD` 환경변수로 시스템 프롬프트 교체 — **agy 의 동등 플래그를 찾을 때의 탐색 체크리스트** |
| 24 | `winsw/winsw` | https://github.com/winsw/winsw | 14.3k | 1.7k | C# | v3 브랜치 841 commits / 최신 `v3.0.0-alpha.11`(29 Jan 02:20), 안정 `v2.12.0`(28 Jan 16:22) | 임의 실행파일을 Windows 서비스로 감싸는 래퍼 | **XML 한 장으로 서비스 정의**: `<executable>`, `<arguments>`, `<onfailure action="restart" delay="10 sec"/>`, `<resetfailure>1 hour</resetfailure>`, `<startmode>Automatic</startmode>`, `<delayedAutoStart>true</delayedAutoStart>`, `<log mode="roll">`, `<env name= value=>`, `<workingdirectory>`, `<priority>`, `<serviceaccount>` |
| 25 | `kirillkovalenko/nssm` | https://github.com/kirillkovalenko/nssm | 1.2k | 169 | C++ | v2.24 (2014-08-31) | NSSM — 애플리케이션을 NT 서비스로 실행, 실패 시 재시작 | WinSW 대안. CLI 로 `AppDirectory`/`AppParameters`/`AppStdout`/`AppStderr`/`AppThrottle`/`AppRestartDelay`/`AppRotateFiles`/`Start` 설정 (§10.2 에 전체 명령 보존) |
| 26 | `larsekje/PythonWindowsServices` | https://github.com/larsekje/PythonWindowsServices | 1 | 0 | Python | — | NSSM 으로 파이썬 스크립트를 서비스로 돌리는 PoC | `/logs`, `/scripts`, `/windows_service` 3분할. **"NSSM 은 로그 파일을 자동 생성하지 않으므로 사전 생성 필요"**라는 실전 함정 |
| 27 | `HaroldMills/Python-Windows-Service-Example` | https://github.com/HaroldMills/Python-Windows-Service-Example | 21 | 11 | Python | — | pywin32 + PyInstaller 로 파이썬 서비스 빌드 | 반면교사. `example_service.exe install` / `start`**관리자 권한 프롬프트에서** 실행해야 함. PyInstaller 가 Python 3.5 까지만 지원한다는 오래된 주석(2016) → 이 경로는 기각 근거 |
| 28 | `mhammond/pywin32` (issue #1563) | https://github.com/mhammond/pywin32/issues/1563 | — | — | Python | — | 서비스 크래시 시 Windows 복구 액션이 트리거되지 않는 문제 | **pywin32 서비스 기각의 결정적 근거**: `SvcRun()` 이 예외를 던지거나 `sys.exit()` 해도 pywin32 정리 코드가 `SERVICE_STOPPED` 를 보고해버려 복구 액션이 발동하지 않는다. 우회책이 `os.kill(os.getpid(), signal.SIGABRT)` 수준 |
| 29 | `Windos/BurntToast` | https://github.com/Windos/BurntToast | 1.7k | 126 | PowerShell | v1.1.0 | Windows 10/Server 2019+ 토스트 알림 PowerShell 모듈 | `Install-Module -Name BurntToast`, `New-BurntToastNotification -Text ...`, `New-BTButton`, `-AppLogo`. **핵심 제약: SYSTEM/서비스 계정에서는 데스크톱 세션 요구 때문에 동작 제한** → 워치독 2단 분리의 근거 |
| 30 | `michalzobec/autorunsalerts` | https://github.com/michalzobec/autorunsalerts | 0 | — | PowerShell | — | autoruns 설정 변경을 감지해 토스트로 알림 | **운영 패턴 정본.**`AutorunsAlert`(SYSTEM, 60분마다): 스캔→`state.json` 과 비교→`audit.log` 기록 ② `AutorunsAlertToast`(사용자 컨텍스트, 15분마다): 플래그 확인 후 토스트. 파일 구성 `autorunsalert.ps1`/`autorunstoast.ps1`/`configuration.json`/`state.json`/`audit.log`/`install.ps1`/`uninstall.ps1` |
| 31 | `DatGuy1/Windows-Toasts` | https://github.com/DatGuy1/Windows-Toasts | 142 | 9 | Python | — | WinRT 기반 파이썬 토스트 | `python -m pip install windows-toasts`. `Toast()`, `WindowsToaster('Python')`, `text_fields`, `on_activated` 콜백. duration 이 short/long 만 지원(pywin32 대비 제약) |
| 32 | `GitHub30/win11toast` | https://github.com/GitHub30/win11toast | 333 | 24 | Python | — | Windows 10/11 토스트 (WinRT) | `pip install win11toast`. `toast()`, `notify()`(논블로킹), `toast_async()`, `buttons=[...]`, `on_click='https://...'`, `image=`, `duration='long'`. **함정: 스크립트 실행 시 현재 디렉터리가 `C:\Windows\system32` 이므로 `os.chdir()` 필요** |
| 33 | `ysfchn/toasted` | https://github.com/ysfchn/toasted | 31 | 2 | Python | — | 리치 토스트(이미지/select/input/progress) | `Progress(value="{value}", status="...")`**진행률 토스트** — 백필 실행처럼 오래 걸리는 작업의 진행 표시에 사용 가능 |
| 34 | `caronc/apprise` | https://github.com/caronc/apprise | 17.2k | 652 | Python | 1,178 commits | 100+ 알림 서비스를 URL 문자열 하나로 통합 | `pip install apprise`; `apobj.add('...')` / `apobj.notify(body=, title=)`. URL: `windows://`(pywin32 필요, `?duration=5`, 250자 제한, 타 PC 전송 불가), `slack://TokenA/TokenB/TokenC/Channel`, `tgram://bottoken/ChatID`, `mailto://`/`mailtos://` |
| 35 | `786raees/task-scheduler-python` | https://github.com/786raees/task-scheduler-python | 2 | 0 | Python | 2 commits | `win32com.client` 로 Windows Task Scheduler 제어 | `create_task()`(실행경로/인자/ISO 8601 트리거), `get_all_tasks()`, `toggle_task()`, `run_task()`, `delete_task()`**설치 스크립트에서 schtasks 문자열 조립 대신 COM 으로 다루는 대안** |
| 36 | `lorien/awesome-web-scraping` | https://github.com/lorien/awesome-web-scraping | 8.1k | 934 | — | 640 commits | 스크래핑 라이브러리/도구/API 큐레이션 | `python.md` / `javascript.md` / `php.md` / `ruby.md` / `golang.md` / `cli.md` / `manuals.md` 언어별 분할. **변경감지·스케줄링·엑셀 섹션은 없음**(확인함) → 라이브러리 선정 참고용으로만 |
| 37 | `testing-in-production/gemini-jobs` | https://github.com/testing-in-production/gemini-jobs | — | — | — | — | (블로그가 언급한 cron+Gemini CLI 예제 저장소) | **HTTP 404 — 존재하지 않음.** ⚠️ 미검증. 참조 금지 |
### 3.1 검색 결과에만 등장한 저장소 (⚠️ 미검증 — 페이지를 직접 열지 않음)
버리지 않고 남긴다. 필요 시 실측 후 승격.
| 저장소 | URL | 범주 | 검색에서 파악된 내용 |
|---|---|---|---|
| `DarpitPatel/OpenFDA` | https://github.com/DarpitPatel/OpenFDA | (b) | openFDA API 를 파이썬으로 스크래핑해 txt + CSV 출력 [S#3] |
| `shaayohn/fda-drug-aproval-data-scraping` | https://github.com/shaayohn/fda-drug-aproval-data-scraping | (b) | Drugs@FDA 에서 특정 기준의 승인 상세를 가져옴 [S#6] |
| `tsbischof/fda` | https://github.com/tsbischof/fda | (b) | FDA 510(k) 및 관련 문서 스크래퍼·집계, predicate device 분석 [S#19] |
| `sheetalkalburgi/web-scraping` | https://github.com/sheetalkalburgi/web-scraping | (b)(c) | BeautifulSoup 로 FDA + Health Canada 사이트 스크래핑 [S#19] |
| `Norbaeocystin/FDA` | https://github.com/Norbaeocystin/FDA | (b) | FDA 의약품 승인 데이터 스크래핑·분석 [S#19] |
| `vshah1016/pharma_scraper` | https://github.com/vshah1016/pharma_scraper | (b) | Biopharmcatalyst PDUFA 캘린더 → CSV [S#19][S#27] |
| `arpitamangal/pharma-scrape-and-analysis` | https://github.com/arpitamangal/pharma-scrape-and-analysis | (b) | 대체 브랜드 식별, FDA 40개 카테고리 분류 [S#8] |
| `rOpenHealth/openfda` | https://github.com/rOpenHealth/openfda | (b) | **R 패키지**. jsonlite/magrittr 로 openFDA 접근 [S#23] |
| `roivant/openfda` | https://github.com/roivant/openfda | (b) | openFDA 관련 포크 [S#23] |
| `betagouv/api-medicaments` | https://github.com/betagouv/api-medicaments | (a 유사) | 프랑스 ANSM 공식 의약품 공개 DB API [S#13] |
| `kawsarlog/AmerisourceBergen` | https://github.com/kawsarlog/AmerisourceBergen | (b) | ★4, Python, Updated Aug 5, 2023. AmerisourceBergen 제품 가격 추출 자동화 [F#19] |
| `MohammedAhmed-01/DataDoseProject` | https://github.com/MohammedAhmed-01/DataDoseProject | (b) | ★0, Jupyter Notebook, Updated Mar 22, 2026. 성분 검증 + OpenFDA 라벨 보강 + DDI 탐지 파이프라인 [F#19] |
| `Dagiayy/kara-medical-telegram-data-platform` | https://github.com/Dagiayy/kara-medical-telegram-data-platform | (e) | ★0, Python, Updated Aug 23, 2026. Telegram 스크래핑 + PostgreSQL + **Dagster 오케스트레이션** [F#19] |
| `bdmorris238/pharmaco-database-project` | https://github.com/bdmorris238/pharmaco-database-project | (b) | ★0, PLpgSQL, Updated Aug 15, 2025. 제약 시장 분석용 관계형/DW 설계 [F#19] |
| `khushihajiyani-dotcom/drug-spending-analysis` | https://github.com/khushihajiyani-dotcom/drug-spending-analysis | (b) | ★0, SQLite, Updated May 3, 2026. 캐나다 주별 약품 지출 분석 2020-2024 [F#19] |
| `Tanguy9862/new-drug-approvals-dashboard` | https://github.com/Tanguy9862/new-drug-approvals-dashboard | (e) | 위 #9 의 자매 프로젝트. Dash 로 실시간 대시보드 [F#42] |
| `patrickloeber/llm-data-scrapers` | https://github.com/patrickloeber/llm-data-scrapers | (h) | LLM 용 데이터 수집 오픈소스 도구 목록 [S#18] |
| `ManiMozaffar/linkedIn-scraper` | https://github.com/ManiMozaffar/linkedIn-scraper | (e) | Playwright + FastAPI, 결과를 DB 와 Telegram 채널로 [S#17] |
| `dineshk-qa/playwright.slack.reporter` | https://github.com/dineshk-qa/playwright.slack.reporter | (e) | Playwright 결과를 Slack 웹훅으로 리포팅 [S#17] |
| `god233012yamil/Excel-Automation-Using-Python` | https://github.com/god233012yamil/Excel-Automation-Using-Python | (e) | openpyxl 엑셀 자동화 예제 모음 [S#7] |
| `prabudevarajan/Task-Reminder-Automation-Python-Excel-CSV-Email-Alerts` | https://github.com/prabudevarajan/Task-Reminder-Automation-Python-Excel-CSV-Email-Alerts | (e) | Tkinter UI + Excel/CSV 저장 + 15/7/3/1일 전 이메일 리마인더 + daily scheduler + 로깅 [S#7] |
| `jithurjacob/Windows-10-Toast-Notifications` | https://github.com/jithurjacob/Windows-10-Toast-Notifications | (g) | `win10toast` 원본. 커스텀 아이콘, threaded 알림 [S#9] |
| `jacobcolbert/Windows-10-Toast-Notifications` | https://github.com/jacobcolbert/Windows-10-Toast-Notifications | (g) | 위 포크 [S#9] |
| `NakedPowerShell/BurntToast` | https://github.com/NakedPowerShell/BurntToast | (g) | BurntToast 포크 [S#25] |
| `Badgerati/Hook` | https://github.com/Badgerati/Hook | (g) | PowerShell 모듈. 서비스 상태 감시 후 BurntToast 팝업 [S#25] |
| `mattwolfe/changedetection` | https://github.com/mattwolfe/changedetection | (d) | changedetection.io 포크 [S#5] |
| `huginn/huginn_agent` | https://github.com/huginn/huginn_agent | (d) | Huginn 에이전트를 Gem 으로 만드는 베이스 [S#12] |
| `SublimeText/Pywin32` | https://github.com/SublimeText/Pywin32 | (g) | pywin32 번들 [S#16] |
| `WinSW-Windows` (org) | https://github.com/WinSW-Windows | (g) | WinSW 관련 조직 계정 [S#15] |
| `google-gemini/gemini-cli` Discussion #3215 | https://github.com/google-gemini/gemini-cli/discussions/3215 | (f) | "Headless execution" 논의 스레드 [S#20] |
| `realpython/list-of-python-api-wrappers` | https://github.com/realpython/list-of-python-api-wrappers | (h) | 파이썬 API 래퍼 목록 [S#23] |
| `noirquant/awesome-web-scraping` | https://github.com/noirquant/awesome-web-scraping | (h) | lorien 포크 [S#8] |
| `jjwangnlp/awesome-web-scraping` | https://github.com/jjwangnlp/awesome-web-scraping | (h) | lorien 포크 [S#8] |
| `luminati-io/Awesome-Web-Scraping` | https://github.com/luminati-io/Awesome-Web-Scraping | (h) | HTTP 라이브러리·브라우저 자동화·프록시 서비스 포함 목록 [S#8] |
| `spinov001-art/awesome-web-scraping-2026` | https://github.com/spinov001-art/awesome-web-scraping-2026 | (h) | 130+ 도구, Python/JS/Go/Rust, 안티디텍션·프록시·클라우드, 주간 갱신 [S#8] |
| `duyet/awesome-web-scraper` | https://github.com/duyet/awesome-web-scraper | (h) | 스크래퍼/크롤러 모음 [S#8] |
| `firecrawl/firecrawl` | https://github.com/firecrawl/firecrawl/releases | (d) | 릴리스 페이지가 검색에 노출 [S#24] |
| `diakes/coupang_crawler_python` | https://github.com/diakes/coupang_crawler_python | (a) | 쿠팡 크롤러 — 무관 [S#1] |
| `FareedKhan-dev/best-llm-finder-pipeline` | https://github.com/FareedKhan-dev/best-llm-finder-pipeline | (c) | Agentic RAG / 멀티에이전트 파이프라인 [S#18] |
| `architkaila/Fine-Tuning-LLMs-for-Medical-Entity-Extraction` | https://github.com/architkaila/Fine-Tuning-LLMs-for-Medical-Entity-Extraction | (c) | Llama2/StableLM PEFT·LoRA 로 약물명·부작용 추출 [S#18] |
| `guilopgar/Medication-Detection-LLM` | https://github.com/guilopgar/Medication-Detection-LLM | (c) | 소셜미디어 텍스트에서 약물 언급 탐지 [S#18] |
| `FDA` (org) | https://github.com/FDA | (b) | FDA 공식 GitHub 조직 [S#4] |
### 3.2 Gist (⚠️ 코드 조각, 저장소 아님)
| Gist | URL | 내용 |
|---|---|---|
| `drmalex07/10554232` | https://gist.github.com/drmalex07/10554232 | pywin32 서비스 예제. `class HelloWorldSvc(win32serviceutil.ServiceFramework)`, `_svc_name_`, `_svc_display_name_`, `SvcStop`, `SvcDoRun`, `win32event.CreateEvent`, `win32serviceutil.HandleCommandLine` [F#40] |
| `seanherron/5997278` | https://gist.github.com/seanherron/5997278 | drugs@fda scraper [S#3][S#19] |
| `nmpowell/dc8e7187948788c5c126f01755252164` | https://gist.github.com/nmpowell/dc8e7187948788c5c126f01755252164 | 기존 Windows Task Scheduler 태스크와 상호작용하는 파이썬 스크립트 [S#29] |
| `hygull/32a742339a416dcfa2990504c848c1a9` | https://gist.github.com/hygull/32a742339a416dcfa2990504c848c1a9 | Windows 10 토스트 생성 [S#9] |
| `HainanZhao/92b43e68850189bfee8f39a2c2581ca6` | https://gist.github.com/HainanZhao/92b43e68850189bfee8f39a2c2581ca6 | "Gemini CLI Job" 으로 반복 작업 자동화 [S#20] |
---
## 4. (a) 한국 식약처 / 공공데이터 크롤러
### 4.1 결론: **DMF 전용 오픈소스는 없다**
WebSearch #1, #2, #11, #13, #21, #22, #30 을 통해 다음 질의를 던졌으나 `nedrug`/`MFDS`/DMF 전용 크롤러 저장소는 **하나도 나오지 않았다**:
- `github nedrug 식약처 크롤링 의약품 crawler python`
- `github 공공데이터포털 식약처 의약품 API python wrapper mfds`
- `github 원료의약품 DMF 등록 식약처 크롤러 파이썬`
- `github 의약품안전나라 e약은요 API 파이썬 오픈API 크롤링 selenium 의약품 허가`
- `"nedrug.mfds.go.kr" github python selenium requests 크롤링 프로젝트`
- `github Korea MFDS drug approval scraper "nedrug" OR "mfds" python`
- `github 공공데이터포털 data.go.kr 파이썬 라이브러리 PublicDataReader 식약처 의약품`
검색 엔진이 반환한 것은 대부분 식약처 공식 포털 페이지와 블로그 튜토리얼이었다. 따라서 **fetch/parse 계층은 우리가 최초 구현자**라고 전제하고 설계한다.
### 4.2 `Q00/data.go.kr-crawling` [F#1][F#62][F#64]
| 항목 | 값 |
|---|---|
| URL | https://github.com/Q00/data.go.kr-crawling |
| 설명 | "건강정보, 의약품 크롤링, 멀티쓰레딩 gevent" |
| ★ / Fork | 4 / 0 |
| 언어 | Python |
| 커밋 | development 브랜치 55 commits |
| 토픽 | crawling, gevent, Python, python-lock, threading |
파일 구조:
```
apis/
async_data_crawler.py
go_data_crwaler.py # 오타 그대로 (crwaler)
column.py
url.py
config.py.example
requirements.txt
README.md
.gitignore
```
**`go_data_crwaler.py` 실측 내용 [F#64]** — 호출하는 data.go.kr MFDS 엔드포인트:
- `getDurPrdlstInfoList` (메인 품목 목록)
- `getSeobangjeongPartitnAtentInfoList`
- `getEfcyDplctInfoList`
- `getOdsnAtentInfoList`
- `getMdctnPdAtentInfoList`
- `getCpctyAtentInfoList`
- `getPwnmTabooInfoList`
- `getSpcifyAgrdeTabooInfoList`
- `getUsjntTabooInfoList`
핵심 코드 라인(원문 인용):
- 인증: `config.go_data_api_key`
- 파라미터 조립: `params.update({'typeName' : column.typeName[addUrl]})`
- 페이지네이션: `params_str2 += '&pageNo=' + str(i+1)`
- 총 페이지 계산: `page = int(totalCount/100) + 1`
- 출력: `wb.save(column.typeName[addUrl]+'.xlsx')`
**중요한 반증**: 저장소 설명과 토픽은 gevent 를 내세우지만, 실제 `go_data_crwaler.py` 에는 **gevent 사용이 없고 동기 `requests.get()` 만 쓴다**. 저장소 설명을 믿지 말고 코드를 읽어야 한다는 교훈.
**`url.py` 실측 내용 [F#62]** — 이 파일은 크롤링이 아니라 **API 명세 스크래핑**을 한다:
- 대상: `https://www.data.go.kr/pubn/lab/gui/IrosDevGuide/selectReqResPrmList.do`
- POST payload: `publicDataDetailPk: "uddi:9a60503c-b31c-4879-9028-a4250f0f6998"`, `paramtrSe: "2"`, `oprtinSeqNo: 15920`
- 응답 JSON 의 `RESULT_RE_LIST` 를 순회(인덱스 8은 건너뜀)하며 파라미터명과 한글명을 추출
- 결과를 **`column.py` 파일로 써낸다**
- import: gevent, base64, requests, BeautifulSoup, json
> **베낄 것**: 공공데이터포털의 "요청/응답 파라미터 목록" 화면을 긁어 **필드 사전을 코드로 자동 생성**하는 발상. DMF API 의 영문 필드명(`DMF_PERMIT_NO` 등)을 한글 헤더로 바꿔야 하는 우리 리포트 요구와 정확히 맞물린다. → `config/field_map.yaml` 을 손으로 쓰되, 생성 스크립트를 `scripts/gen_field_map.py` 로 둔다.
### 4.3 `jjscan/data.go.kr-1` [F#46]
| 항목 | 값 |
|---|---|
| URL | https://github.com/jjscan/data.go.kr-1 |
| ★ / Fork | 0 / 0 |
| 언어 | **R** |
| 파일 | `DURPrdlstInfoService.R`, `README.md` |
| 대상 API | `DURPrdlstInfoService` / `getUsjntTabooInfoList` (병용금기정보조회) |
응답 구조 실측: `numOfRows: 100`, `pageNo: 895`(예), `totalCount: 351010`. XML 응답을 `xmlSApply` / `xpathSApply` 로 파싱 후 CSV 로 내보냄. HTTP 는 `httr` 패키지.
README 가 기록한 **구현 난제 4가지**(우리가 그대로 대비해야 할 항목):
1. **연결 실패** — 폴링(재시도) 메커니즘으로 해결
2. **널 데이터 처리**`totalCount == 0` 검증으로 탐지
3. **R 세션 크래시** — 간헐적, 미해결
4. **성능 병목** — 단일 스레드로 약 350,000건에 **17시간**. Rmpi 로 MPI 병렬화하여 해결
> **베낄 것**: (1) 재시도 정책의 필요성, (2) `totalCount == 0` 을 "데이터 없음"의 공식 판정 기준으로 삼기, (3) 전체 백필은 반드시 병렬/증분으로 설계 — DMF 현황 전량 백필 시에도 동일한 함정이 있다.
### 4.4 `WooilJeong/PublicDataReader` [F#37]
| 항목 | 값 |
|---|---|
| URL | https://github.com/WooilJeong/PublicDataReader |
| 설명 | "공공 데이터 조회를 위한 오픈소스 파이썬 라이브러리" |
| ★ / Fork | 597 / 113 |
| 언어 | Python |
| 커밋 | 168 |
| 설치 | `pip install PublicDataReader --upgrade` |
지원 provider: FRED, 공공데이터포털(국토교통부 실거래가/건축물대장/건축인허가/주택인허가/토지임야/토지소유, 소상공인시장진흥공단 상권정보, 한국자산관리공사 공매물건, 국세청 사업자등록 확인, 한국부동산원), KOSIS, ECOS, 서울시 교통, V-World, KB부동산.
**결정적 사실: 식약처/MFDS/의약품 커버리지가 없다**(문서에 명시). 따라서 **의존성으로 채택하지 않는다.** 다만 "provider 별 모듈 + 공통 조회 인터페이스" 라는 패키지 레이아웃은 우리 `fetch/` 의 소스별 어댑터 설계 참고가 된다.
### 4.5 `NomaDamas/k-skill` — `docs/features/mfds-food-safety.md` [F#36]
| 항목 | 값 |
|---|---|
| URL | https://github.com/NomaDamas/k-skill/blob/main/docs/features/mfds-food-safety.md |
| 부모 저장소 ★ | 7.4k |
| 문서 주제 | "식품 안전 체크 가이드" |
| 스크립트 경로 | `scripts/mfds_food_safety.py` |
| 참조 데이터 소스 | ① 공공데이터포털 "부적합 식품" 엔드포인트 ② 식품안전나라 회수·판매중지 API |
| API 키 처리 | `DATA_GO_KR_API_KEY`, `FOODSAFETYKOREA_API_KEY`**"프록시 운영 서버" 환경변수**에 두고, 사용자는 `k-skill-proxy``/v1/mfds/food-safety/search` 로 접근 |
| 안전 규범 | "이 helper 는 **직접 진단**을 하지 않는다" — 자동 판정보다 전문가 상담 우선 |
> **베낄 것**: ① 키를 사용자 머신 코드에 박지 않는 분리 원칙(우리는 로컬 단독이므로 `.env` + `.gitignore`) ② **"자동화가 판정하지 않고 근거만 제시한다"는 안전 규범** — DMF 변경 탐지 결과도 "규제 판단"이 아니라 "차이 보고"로 문구를 통일한다.
### 4.6 이 범주에서 확인되지 않은 것 (⚠️ 미검증)
- `nedrug.mfds.go.kr` 를 WebFetch 로 열었을 때 `https://nedrug.mfds.go.kr/searchDmf`**에러 페이지**("The requested page cannot be found")를 반환했다 [F#63]. DMF 검색 화면의 실제 URL·파라미터는 브라우저로 실측해야 한다.
- data.go.kr 의 DMF OpenAPI 는 페이지 자체는 열렸다 [F#29]. §15 참조.
---
## 5. (b) FDA / openFDA / 규제 데이터
한국 DMF 에 직접 쓸 코드는 없지만, **"규제 데이터셋을 주기적으로 받아 정규화하고 저장한다"** 는 문제를 가장 오래 푼 집단이 여기다. 레이아웃과 파이프라인 분할을 여기서 가져온다.
### 5.1 `FDA/openfda` (공식) [F#2]
| 항목 | 값 |
|---|---|
| URL | https://github.com/FDA/openfda |
| 설명 | "openFDA is a research project to provide open APIs, raw data downloads, documentation and examples, and a developer community for an important collection of FDA public datasets." |
| ★ / Fork | **705 / 166** |
| 언어 | Python |
| 최상위 디렉터리 | `api/faers`, `config`, `dependencies`, `openfda`, `schemas`, `scripts` + `Dockerfile`, `docker-compose.yml`, `requirements.txt`, `setup.py` |
| 파이프라인 기술 | **Luigi** — "Python pipelines written with Luigi for processing public FDA data sets (drugs, foods, medical devices, and other) into a JSON format that can be loaded into Elasticsearch." |
| 데이터셋 | NSDE, CAERS, Substance Data, Device Clearance, Device PMA, Device Event 파이프라인 명시 |
| 실행 | `docker-compose up` → Elasticsearch + API 컨테이너(포트 8000). "the API container starts right away, it will not serve any data until some or all of the pipelines above have finished running." |
| 전제 조건 | Elasticsearch 7, Python 3.10, Node 16+ |
> **베낄 것**
> - `config/`(설정) · `schemas/`(데이터 계약) · `scripts/`(운영 스크립트) · `<pkg>/`(코드) **4분할**. 우리 트리에 그대로 반영한다.
> - `schemas/` 를 별도 최상위로 두는 것 — 응답 스키마와 스냅샷 스키마를 코드와 분리해 버전 관리하면, 식약처가 필드를 바꿨을 때 diff 로 즉시 감지된다.
> - "데이터 적재 전에는 API 가 아무것도 서빙하지 않는다"는 명시 — 우리도 스냅샷이 2개 미만이면 리포트를 만들지 않고 "기준선 수립" 상태로 종료해야 한다.
>
> **기각할 것**: Luigi / Elasticsearch / Docker. 단일 Windows PC 에 하루 1회 소량 데이터. 오버엔지니어링이다.
### 5.2 `jbremz/FDA-Analysis` [F#3]
| 항목 | 값 |
|---|---|
| URL | https://github.com/jbremz/FDA-Analysis |
| 설명 | "Scraping and analysis of the (now retired) Drugs@FDA site - with scrapy and pandas" |
| ★ / Fork | 5 / 2 |
| 언어 | Python |
| 파일 | `FDA Spider/`(Scrapy 프로젝트, 메인 스파이더는 `FDASpider`), `masterDrugList2.csv`(원시 산출물), `FDA_Data_Analysis.py`(pandas 분석), `Drugs@FDA Analysis.ipynb`(결론 노트북), `.ipynb_checkpoints/` |
| 규모 | "over 22,000 different products" — British Medical Journal 의뢰로 데이터 품질·누락 보고 평가 |
> **베낄 것**: **"스파이더 / 원시 CSV / 분석 스크립트 / 결론 노트북" 4단 분리.** 특히 원시 산출물을 저장소에 남겨 재현 가능하게 한 점. 우리는 `data/raw/YYYY-MM-DD/` 에 원시 응답을 그대로 남긴다.
>
> **경고 신호**: 대상 사이트(Drugs@FDA 구 버전)가 **은퇴하면서 이 프로젝트도 죽었다.** 우리도 nedrug 게시판 구조 변경에 대비해 파서를 소스별로 격리하고, 파싱 실패를 즉시 알림으로 올려야 한다.
### 5.3 `logiover/fda-data-scraper` [F#4]
| 항목 | 값 |
|---|---|
| URL | https://github.com/logiover/fda-data-scraper |
| 설명 | "FDA data scraper — openFDA drug/device/food recalls, adverse events & 510(k) clearances as JSON/CSV" |
| ★ / Fork | 0 / 0 (master 1 commit) |
| 라이선스 | MIT |
| 파일 | `/examples`(CLI, API, JavaScript, Python 사용 예), `.gitignore`, `LICENSE`, `README.md` |
지원 9개 데이터셋: ① Drug recalls (enforcement) ② Drug adverse events (20M+ records) ③ Drug labels (258K records) ④ Device recalls ⑤ Device adverse events (24M+ records) ⑥ Device 510(k) clearances ⑦ Food recalls ⑧ Food adverse events ⑨ Animal & veterinary adverse events
출력 포맷: **"JSON, CSV, Excel (XLSX), JSONL, XML or HTML"**
자동화 기능: 일일 스케줄 실행, 완료 웹훅, Google Sheets/Excel 연동, 클라우드 스토리지 내보내기(S3, GCS), "Zapier, Make, n8n or Pipedream" 호환. 실행 경로는 Apify Console / Apify CLI / API(curl) / apify-client(JS·Python) 4가지.
> **베낄 것**: **출력 포맷을 데이터 파이프라인이 아니라 최종 어댑터에서 스위칭**하는 설계. `report` 모듈이 동일한 `DiffResult` 를 받아 xlsx/csv/json 중 하나로 렌더링한다.
>
> **기각할 것**: Apify 플랫폼 의존. 로컬 실행이 요구사항이다.
### 5.4 `coderxio/OpenFDA` [F#18]
| 항목 | 값 |
|---|---|
| URL | https://github.com/coderxio/OpenFDA |
| 설명 | "Python scripts for capturing OpenFDA data in a database." |
| ★ / Fork | 3 / 3 |
| 언어 | Python |
| 파일 | `openfda/`, `.gitignore`, `README.md`, `docker-compose.override.yml`, `docker-compose.yml` |
| 데이터 | drug NDC 데이터셋 (`https://open.fda.gov/apis/drug/ndc/download/`) 을 압축 해제 후 `drug-ndc.json` 으로 이름 바꿔 `./data/` 에 배치 |
| 실행 (venv) | venv 생성 → requirements 설치 → `python app/load_data.py``python app/serve_data.py` |
| 실행 (Docker) | "First run to load DB: `docker-compose up --build`" → 운영은 별도 compose 파일 → 정리는 `docker-compose down -v` |
| 서버 | CherryPy |
> **베낄 것**: **`load_data`(적재) 와 `serve_data`(제공) 의 엔트리포인트 분리**, 그리고 "최초 실행"과 "이후 실행"이 다른 명령이라는 명시. 우리 CLI 에서 `dmf backfill` 과 `dmf daily` 로 구현한다. 또한 원시 다운로드물을 `./data/` 에 고정 파일명으로 두는 규칙.
### 5.5 `Tanguy9862/AI-Powered-FDA-Drug-Scraper` [F#42]
| 항목 | 값 |
|---|---|
| URL | https://github.com/Tanguy9862/AI-Powered-FDA-Drug-Scraper |
| 설명 | "Python-based web scraper leveraging generative AI with LangChain and GPT-4o-mini to extract and classify FDA drug approval data" |
| ★ / Fork | 3 / 0 |
| 언어 | Python |
| 파일 | `new_drug_approvals_scraper/`(패키지), `img_readme/`, `scraper.py`, `classification.py`, `utils.py`, `__init__.py`, `requirements.txt`, `setup.py`, `.gitignore`, `LICENSE`, `README.md` |
| 대상 | https://www.drugs.com/newdrugs.html |
| 규모 | "over 1,770 records" |
| 정규화 | 회사명 표기 변형을 접미사·약어·협업 서술 표준화로 **약 1000종 → 700종**으로 축소 |
| 자매 | https://github.com/Tanguy9862/new-drug-approvals-dashboard (Dash 대시보드) |
> **베낄 것**
> - **`scraper.py` / `classification.py` / `utils.py` 분리** — 수집(결정론)과 LLM 분류(비결정론)를 절대 같은 파일에 두지 않는다. 우리 구조에서는 `fetch/`(결정론) 와 `enrich/`(agy 호출) 로 대응.
> - **업체명 정규화가 필수 작업이라는 실증.** DMF `ENTP_NAME`/`MNFCTR_NAME` 도 "(주)"·"주식회사"·영문/한글 혼용 때문에 같은 문제를 겪는다. `parse/normalize.py` 에 정규화 규칙 테이블을 둔다.
### 5.6 FDA DMF 목록 자체에 대해 확인된 것 / 확인 실패한 것
- FDA 는 DMF 목록을 **분기별**로 갱신한다. 검색 결과가 인용한 문장: "The list of DMFs, which is updated quarterly, contains DMFs received by June 30, 2026, for which acknowledgment letters were sent before July 19, 2026." [S#6]
- 컬럼(검색 결과 기준, ⚠️ 미검증): DMF 번호, submitter, file type, acknowledgment date, review division [S#27]
- **`https://www.fda.gov/drugs/drug-master-files-dmfs/list-drug-master-files-dmfs``https://www.fda.gov/drugs/drug-master-files-dmfs` 는 둘 다 WebFetch 에서 HTTP 404 를 반환했다** [F#48][F#53]. 실제 xls/xlsx/zip 다운로드 URL 은 확인하지 못했다. ⚠️ 미검증.
- 이 프로젝트는 **한국 DMF** 가 대상이므로 FDA DMF 는 범위 밖이다. 다만 "분기 갱신 스프레드시트를 받아 이전 분기와 비교" 라는 문제 형태는 우리와 동형이므로 참고로만 남긴다.
---
## 6. (c) 규제 변경 감지 · 인텔리전스
이 범주가 **우리 프로젝트와 문제 정의가 가장 가깝다**. 세 저장소가 각각 다른 축을 보여준다.
### 6.1 `anton-semerenko/pharma-radar` — 에이전트 파이프라인의 정본 [F#6]
| 항목 | 값 |
|---|---|
| URL | https://github.com/anton-semerenko/pharma-radar |
| 설명 | "Autonomous LLM agent delivering a daily, source-verified pharma regulatory & competitive intelligence briefing to Telegram." |
| ★ / Fork | 0 / 0 |
| 언어 | Python |
| 커밋 | main 브랜치 2 commits |
| 라이선스 | MIT |
디렉터리 구조 (원문 그대로):
```
├── prompts/system_prompt.md (agent methodology)
├── config/sources.yaml (source hierarchy & rubrics)
├── src/deliver.py (Telegraph + Telegram publishing)
└── examples/sample_digest.md (sample output)
```
**감시 소스**
- 규제기관: FDA, EMA, WHO, 우크라이나 당국(МОЗ, ДЕЦ, Держлікслужба)
- 업계지: Endpoints News, FiercePharma, STAT News
- 기업 IR 페이지 및 등록부
**에이전트 로직 (원문 인용)**: Claude(Opus급)를 agentic loop 로 사용하여 "retrieves broadly, **verifies every item against ≥2 independent sources or one official regulatory primary**, attributes every number, and states what each development _means_."
**검증 정책**: 2개 독립 출처 또는 1개 공식 규제 1차 출처가 있어야 항목에 포함. 단일 2차 출처는 불충분으로 간주.
**출력 구조**: 4개 루브릭 — Regulatory & Approvals / Clinical & Pipeline / Market·Access & Ukraine / Competitive & Corporate — 에 Forward Agenda 와 Signals to Watch 섹션 추가. 각 항목은 4~7문장 + 출처 명시.
**배포·스케줄**: 롤링 Telegraph 페이지에 게시 + Telegram 푸시. **매일 06:00 스케줄드 태스크로 무인 실행.** 배포 계층은 **파이썬 표준 라이브러리만** 사용.
> **베낄 것 (5가지, 전부 채택)**
> 1. **`prompts/` 를 최상위 디렉터리로.** 에이전트 프롬프트는 코드가 아니라 자산이다. `prompts/dmf_summarize.md`, `prompts/dmf_classify.md` 로 분리하고 버전 관리한다.
> 2. **`config/sources.yaml` 로 소스 계층·루브릭을 데이터로 선언.** 소스를 추가할 때 코드를 고치지 않는다.
> 3. **배포(notify) 계층은 의존성 최소화.** pharma-radar 는 표준 라이브러리만 썼다. 우리는 Apprise 하나만 허용하고 그 외 SDK 는 금지한다.
> 4. **검증 정책을 문서로 명시.** 우리 버전: "DMF 변경 건은 ① 공공 API 응답과 ② 공고 게시판 중 최소 1개의 공식 1차 출처로 뒷받침되어야 리포트에 '확정' 으로 표기한다. 한쪽만 있으면 '관찰 중(pending)' 으로 표기한다."
> 5. **매일 06:00 + 무인 + 스케줄드 태스크** — 우리 요구사항과 동일한 운영 형태가 실제로 굴러가고 있다는 존재 증명.
>
> **주의**: ★0 / 2 commits 로 성숙도는 매우 낮다. **구조만 참고하고 코드 재사용은 하지 않는다.**
### 6.2 `mrueda/nomenclator-delta` — 모듈 경계의 정본 [F#30]
| 항목 | 값 |
|---|---|
| URL | https://github.com/mrueda/nomenclator-delta |
| 설명 | "Compare monthly changes in medicines and health products from the Ministerio de Sanidad's Nomenclátor de Facturación." |
| ★ / Fork | **1 / 0** |
| 언어 | Python |
| 최근 갱신 | Updated Aug 9, 2026 [F#19] |
| 라이선스 | MIT |
디렉터리 구조 (원문 그대로):
```
src/nomenclator_delta/ (collection, normalization, diffing, validation)
data/ (snapshots and change history)
site/ (Spanish static application)
docs-site/ (documentation and Pages build)
tests/ (unit and integration tests)
```
**데이터 소스**: 스페인 보건부 "Nomenclátor de facturación" — 국가보건시스템 의약품 공개 DB
**델타 계산**: "compares consecutive monthly releases" 하고 "finds changes by Código Nacional, medicine name, active ingredient, or laboratory." 즉 **국가코드(고유 키) + 3개 부가 축**으로 변경을 찾는다. added/removed/changed 를 식별한다고 README 가 기술.
**출력**: 백엔드 없는 정적 브라우저 앱 (GitHub Pages 호스팅)
**운영**: "a monthly update runbook" 문서가 존재 — 유지보수자가 정기 릴리스를 다루는 절차서
**CLI**:
```bash
python3 -m nomenclator_delta validate data
python3 -m nomenclator_delta dist
```
> **베낄 것 (이 프로젝트의 골격)**
> - **`collection / normalization / diffing / validation` 4단 모듈 분리를 그대로 채택.** 우리 명칭으로는 `fetch / parse(normalize) / diff / validate`.
> - **`data/` 에 "스냅샷"과 "변경 이력"을 함께 둔다.** 스냅샷은 시점의 진실, 변경 이력은 시점 간의 진실. 둘 다 보존해야 재계산이 가능하다.
> - **`validate` 를 별도 CLI 서브커맨드로.** 파이프라인을 돌리기 전에 데이터 무결성을 먼저 검사한다. 우리는 `dmf validate data` 로 동일하게 만든다.
> - **`docs-site/`(문서) 와 `site/`(산출물 뷰어) 를 분리.** 우리는 산출물이 xlsx 이므로 `site/` 대신 `reports/` 를 둔다.
> - **runbook 문서를 리포지토리 안에.** `docs/ops/runbook.md` 로 채택.
> - **`python3 -m <pkg> <subcommand>` 형태의 진입점.** Windows 에서도 `python -m dmf_crawler daily` 가 배치 파일보다 견고하다.
### 6.3 `Mzands2622/Zanalytix` — LLM 기반 변경 감지의 실물 [F#31]
| 항목 | 값 |
|---|---|
| URL | https://github.com/Mzands2622/Zanalytix |
| 설명 | "AI-powered pipeline for scraping and tracking pharmaceutical clinical pipeline data with LLM-based change detection." |
| ★ / Fork | 0 / 0 |
| 언어 | Python |
| 커밋 | main 브랜치 1 commit / Updated Mar 9, 2026 |
**파일 구조**: 60+ 개의 회사별 파서 모듈(`abbvie_pipeline.py`, `pfizer_pipeline.py` …) + 코어:
- `db.py` (DB 관리)
- `function_app.py`, `master_scheduler.py` (API 계층 / 스케줄러)
- `login.py`, `sign_up.py` (인증)
- `notifications.py` (알림)
- `cleanup_text.py`, `treatment_visualizer.py` (유틸)
**수집 흐름 (원문)**: "HTML Scraping (Zyte API) --> 60+ Company Parsers (BeautifulSoup)" → 표준화 파이프라인. 각 회사 모듈은 **두 함수를 반드시 구현**:
- `fetch_{company}_html()` — 파이프라인 페이지 획득
- `process_{company}_html()` — 구조화 객체로 파싱
**LLM 변경 감지 (원문)**: "GPT-4o compares old vs new treatment data, assigns priority (1-5), categorizes changes" — 비교 큐를 통해 수행. 날짜별 스냅샷을 JSON 으로 유지해 시계열 분석 가능.
**저장 구조**: `Revised_MasterTable` 이 treatment 를 키로, `Treatment_Data` 에 타임스탬프 스냅샷을 담는다. 별도 `Stream` 테이블이 변경 이벤트별 GPT-4o 응답과 메타데이터를 추적.
**스케줄·알림**: "Calendar-based scraping schedules with recurrence rules and auto-extension" 이 Azure Functions 를 트리거. 매칭된 변경은 사용자 저장 선호에 따라 "Twilio SMS/Calls, Email" 로 라우팅.
> **베낄 것 (3가지)**
> 1. **소스별 파서 모듈의 함수 시그니처를 강제 통일** (`fetch_*` / `process_*`). 우리는 `fetch(source_id) -> RawPayload`, `parse(RawPayload) -> list[DmfRecord]` 프로토콜로 명문화한다.
> 2. **변경 이벤트에 우선순위 점수(1~5)를 부여**하고, 그 점수로 알림 대상/채널을 결정. DMF 에서는 예: 신규 등록=3, 등록 취하=5, 제조소 주소 변경=2, 오탈자 수정=1.
> 3. **원본 스냅샷 테이블과 "변경 이벤트 + LLM 응답" 테이블을 분리**. 우리 SQLite 스키마에 `snapshots` / `changes` / `agent_runs` 3테이블로 반영.
>
> **기각할 것**: Azure Functions, Zyte API, Twilio, 로그인/회원가입. 로컬 단일 사용자에겐 전부 불필요.
### 6.4 `suriyadeepan/WebScraping-for-Healthcare` [F#22]
| 항목 | 값 |
|---|---|
| URL | https://github.com/suriyadeepan/WebScraping-for-Healthcare |
| 설명 | "Scraping the internet for extracting healthcare and pharma data." |
| ★ / Fork | 8 / 1 |
| 언어 | Python |
| 커밋 | main 브랜치 44 commits |
| 라이선스 | GPL-3.0 |
구조:
```
├── data/
├── mhra/
├── notebooks/
├── phscrape/
├── twitter/
├── .gitignore
├── LICENSE (GPL-3.0)
├── README.md
├── requirements.txt
└── tests.py
```
수집 대상: Drug Bank, ClinicalTrials.gov, COVID-19 API, Twitter(#remdesivir), EMC(Electronic Medicines Compendium — SMPC/PIL 추출), HPRA(아일랜드), MHRA(영국).
`phscrape` 모듈이 노출하는 함수: `drugbank.fetch()`, `clinicaltrials.fetch()`, `emc.crawl_k()`, `hpra.crawl_k()`.
> **베낄 것**: **소스별 서브모듈이 동일 인터페이스(`fetch()` / `crawl_k()`)를 노출**하는 규약. 위 Zanalytix 와 동일한 결론에 독립적으로 도달했다는 점이 이 패턴의 타당성을 보강한다.
>
> **주의**: GPL-3.0 이므로 **코드를 복사하면 안 된다.** 설계만 참고.
### 6.5 상업 도구 및 업계 동향 (배경, 코드 없음)
- Clarivate 의 Biopharma Regulatory Compliance 서비스 [S#4]
- Vistaar 의 Regulatory Intelligence Database 소개 [S#4]
- IntuitionLabs: "AI and the Future of Regulatory Affairs in the U.S. Pharmaceutical Industry", "Open Source Pharma: Tools & Trends in Drug Development" [S#4]
- **2026년 1월, FDA 와 EMA 가 공동으로 "Guiding Principles for Good AI Practice in Drug Development" 를 발표** — 규제 당국이 새로운 도구 패러다임에 개방적임을 시사 [S#4]. ⚠️ 미검증(검색 요약문 기준)
---
## 7. (d) 범용 변경 감지 도구
### 7.0 이 범주에 대한 총평 — **도입이 아니라 설계 차용**
| 도구 | 감지 단위 | 우리 요구와의 불일치 |
|---|---|---|
| changedetection.io | 페이지 텍스트 블록 | DMF 는 **레코드 집합**이다. "이 페이지의 3줄이 바뀜"이 아니라 "등록번호 XXX 가 신규/변경/취하"를 알아야 한다 |
| urlwatch | URL·명령 출력의 unified diff | 위와 동일. 또한 xlsx 리포트 생성 기능이 없다 |
| huginn | 이벤트 그래프 | Ruby/Rails + DB. Windows 단일 PC 에 얹기엔 과중 |
| csvdiff | **레코드 키 기반 added/removed/changed** | ✅ 맞음. 단 2021 아카이브 |
**결정: 우리 diff 모듈은 csvdiff 의 출력 스키마를 채택하고, 필터/알림/스케줄 개념은 urlwatch·changedetection.io 에서 가져오되, 도구 자체는 도입하지 않는다.**
### 7.1 `dgtlmoon/changedetection.io` [F#7][F#55][F#61]
| 항목 | 값 |
|---|---|
| URL | https://github.com/dgtlmoon/changedetection.io |
| 설명 | "Best and simplest tool for website change detection, web page monitoring, and website change alerts. Perfect for tracking content changes, price drops, restock alerts, and website defacement monitoring—all for free or enjoy our SaaS plan!" |
| ★ / Fork | **33.5k / 2.0k** |
| 언어 | Python |
| 커밋 | master 2,448 commits |
| 최신 릴리스 | **`0.55.8`** (13 Jul 09:26) |
**콘텐츠 필터·감지**
- CSS Selectors, XPath (1.0 & 2.0)
- JSONPath, jq 필터 (API 모니터링용)
- HTML 페이지 내 embedded JSON 추출
- Visual Selector 도구
**모니터링**
- **word / line / character 레벨 diff 시각화**
- 인터랙티브 브라우저 스텝 (로그인, 폼 채우기, 버튼 클릭)
- PDF 변경 추적
- 커스터마이즈 가능한 체크 간격
**알림**: Apprise 통합 — Discord, Email, Slack, Telegram, webhooks 등 90+ 서비스, Jinja2 템플릿
**스케줄**: 타임존 인식, 요일·시간 제한 지원
**고급**: LiteLLM 연동 AI 변경 감지·요약, REST API, Chrome 확장, 프록시(Bright Data 포함)
**설치**
```bash
# Docker Compose
docker compose up -d
# Docker standalone
docker run -d --restart always -p "127.0.0.1:5000:5000" -v datastore-volume:/datastore --name changedetection.io dgtlmoon/changedetection.io
# pip
pip3 install changedetection.io
changedetection.io -d /path/to/empty/data/dir -p 5000
```
접속: `http://127.0.0.1:5000`
**REST API (x-api-key 헤더)** [F#27] — Settings > API 에서 키 발급
```bash
# 감시 목록
curl -X GET "http://localhost:5000/api/v1/watch" \
-H "x-api-key: YOUR_API_KEY"
# 감시 생성
curl -X POST "http://localhost:5000/api/v1/watch" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"title": "My Monitor",
"time_between_check": {"hours": 1}
}'
# 단건 조회
curl -X GET "http://localhost:5000/api/v1/watch/{uuid}" \
-H "x-api-key: YOUR_API_KEY"
# 수정
curl -X PUT "http://localhost:5000/api/v1/watch/{uuid}" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"notification_muted": true}'
# 삭제
curl -X DELETE "http://localhost:5000/api/v1/watch/{uuid}" \
-H "x-api-key: YOUR_API_KEY"
# 이력
curl -X GET "http://localhost:5000/api/v1/watch/{uuid}/history" \
-H "x-api-key: YOUR_API_KEY"
# 최신 스냅샷
curl -X GET "http://localhost:5000/api/v1/watch/{uuid}/history/latest" \
-H "x-api-key: YOUR_API_KEY"
# 이전↔최신 비교
curl -X GET "http://localhost:5000/api/v1/watch/{uuid}/difference/previous/latest?format=htmlcolor" \
-H "x-api-key: YOUR_API_KEY"
# 전역 알림 URL 등록
curl -X POST "http://localhost:5000/api/v1/notifications" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"notification_urls": ["mailto:admin@example.com"]}'
```
추가 동작: 강제 재확인은 GET watch 엔드포인트에 `?recheck=1`, 일시정지/음소거는 `?paused=paused` / `?muted=muted`. 연결 URL 형식은 로컬 `http://localhost:5000/api/v1/`, 호스티드 `https://<your-domain>/api/v1/`.
**알림 템플릿 토큰** [F#55] (정확히 인용):
```
{{base_url}}
{{current_snapshot}}
{{diff}}
{{diff_full}}
{{diff_added}}
{{diff_removed}}
{{watch_url}}
{{triggered_text}}
```
예시 본문:
```json
{
'myKey': 1234,
'url': '{{watch_url|tojson}}'
}
```
주의: `{{current_snapshot}}`, `{{diff}}`, `{{diff_full}}` 은 길어서 서비스 길이 제한(Discord 2,000자)을 넘길 수 있다. "your notification body contains at least something" 을 보장할 것.
> **베낄 것**
> - **`{{diff_added}}` / `{{diff_removed}}` 로 알림 본문을 조립하는 토큰 설계** → 우리 notify 모듈의 템플릿 변수명을 이것과 동일하게 맞춘다(`added`, `removed`, `changed`, `report_path`, `run_date`).
> - **알림 본문 길이 상한을 서비스별로 강제**하는 규칙. Apprise `windows://` 는 250자 제한이므로 토스트에는 요약 카운트만, 상세는 xlsx 링크로.
> - **타임존 인식 + 요일/시간 제한 스케줄** 개념. 우리는 매일 06:00 고정이지만, 공휴일 스킵 옵션을 `config/schedule.yaml` 로 열어둔다.
> - 강제 재확인(`?recheck=1`) 에 대응하는 수동 실행 커맨드 `dmf daily --force`.
### 7.2 `thp/urlwatch` [F#5][F#26][F#50][F#60]
| 항목 | 값 |
|---|---|
| URL | https://github.com/thp/urlwatch |
| 설명 | "Watch (parts of) webpages and get notified when something changes via e-mail, on your phone or via other means. Highly configurable." |
| ★ / Fork | **3.1k / 354** |
| 언어 | Python |
| 커밋 | master 974 commits |
| 릴리스 | **"There aren't any releases here"** — 공식 릴리스 없음 [F#60] |
| 문서 | https://urlwatch.readthedocs.io/ |
| 홈페이지 | https://thp.io/2008/urlwatch/ |
| 토픽 | automation, monitor, python, webpage |
**URL 잡 형식** [F#26]:
```yaml
name: "urlwatch homepage"
url: "https://thp.io/2008/urlwatch/"
```
URL 잡 옵션: `url`(필수), `name`, `method`(기본 GET), `data`(POST/PUT 페이로드), `headers`, `cookies`, `encoding`, `filter`, `ignore_connection_errors`.
**셸 잡 형식**:
```yaml
name: "What is in my Home Directory?"
command: "ls -al ~"
```
**설정 저장/실행**: 잡은 `urls.yaml` 에 저장하고 `urlwatch --edit` 로 편집. `urlwatch --list` 로 인덱스 번호와 함께 목록 표시. 각 잡은 `---` 만 있는 줄로 구분. 메인 설정 파일에 `job_defaults` 섹션을 두어 전 잡에 공통 설정 적용 가능(키 반복 제거).
**내장 필터 28종** [F#50] (정확히 인용):
```
beautify, css, csv2text, element-by-class, element-by-id, element-by-style,
element-by-tag, format-json, grep, grepi, hexdump, html2text, pdf2text,
pretty-xml, ical2text, ocr, re.sub, re.findall, reverse, sha1sum, shellpipe,
sort, remove-duplicate-lines, strip, striplines, xpath, jq
```
필터 체인 예시:
```yaml
url: https://example.net/css.html
filter:
- css: ul#groceries > li.unchecked
- html2text
```
diff 관련: `diff_filter` 는 "applied to the diff result before reporting the changes" 이며, `--test-diff-filter` 로 캐시된 과거 데이터로 테스트 가능. `diff_tool` 은 Filters 페이지에 문서화되어 있지 않음(⚠️ 미검증 — Configuration 페이지 확인 필요).
> **베낄 것**
> - **`config/sources.yaml` 의 스키마를 urlwatch 잡 스키마에 맞춘다**: `name`, `url`, `method`, `headers`, `params`, `encoding`, `ignore_connection_errors`, `filter`(체인).
> - **`job_defaults` 상속 개념** — 타임아웃·User-Agent·재시도 횟수를 소스마다 반복하지 않는다.
> - **필터를 이름 붙은 체인으로 선언**하고 코드가 아니라 설정으로 조합. 우리는 `strip`, `sort`, `re.sub`, `jq` 정도만 있으면 충분하다.
> - **`ignore_connection_errors`** — nedrug 게시판이 일시 장애일 때 파이프라인 전체를 죽이지 않는 플래그. 채택.
> - **`--test-diff-filter` 처럼 과거 캐시로 diff 로직을 테스트하는 서브커맨드** → `dmf diff --replay 2026-09-01 2026-09-02`.
>
> **경고**: 릴리스가 없는 저장소다. 의존성으로 채택하지 않는다.
### 7.3 `huginn/huginn` [F#8][F#65][F#68]
| 항목 | 값 |
|---|---|
| URL | https://github.com/huginn/huginn |
| 설명 | "Create agents that monitor and act on your behalf. Your agents are standing by!" |
| ★ / Fork | **49.9k / 4.3k** |
| 언어 | Ruby (Rails) |
| 커밋 | master 4,134 commits |
| 라이선스 | MIT |
에이전트 타입(확인된 것): WeatherAgent, WebsiteAgent, EmailAgent, TwitterAgent, 그리고 HipChat / FTP / IMAP / Jabber / JIRA / MQTT 커넥터, JavaScript 실행 에이전트, 위치 추적.
동작 원리: "Huginn's Agents create and consume events, propagating them along a directed graph." "send digest email with things that you care about at specific times during the day"
**`WebsiteAgent` 실측** [F#68]:
- 정의: "The Website Agent scrapes a website, XML document, or JSON feed and creates Events based on the results."
- **mode 옵션 3종**:
- `all` — 모든 추출 결과에 대해 이벤트 생성
- `on_change` — 이전 결과와 다를 때만 이벤트 생성
- `merge` — 기존 페이로드를 유지하며 새 값으로 갱신
- **기본 스케줄: `every_12h`**
- extract 설정 예시:
```json
"extract": {
"url": { "css": "#comic img", "value": "@src" },
"title": { "css": "#comic img", "value": "@alt" },
"hovertext": { "css": "#comic img", "value": "@title" }
}
```
CSS selector, XPath, JSON path, regex 를 문서 타입에 따라 지원.
**주의**: Huginn wiki 의 Agent-Types 페이지는 로딩 오류로 `ChangeDetectorAgent`, `DeDuplicationAgent`, `DigestAgent`, `EmailDigestAgent`, `SchedulerAgent`, `ShellCommandAgent` 의 설명을 확보하지 못했다 [F#65]. ⚠️ 미검증.
> **베낄 것**
> - **`mode: all / on_change / merge` 3분류를 diff 모듈의 출력 모드로 그대로 채택.**
> - `all` = 전체 스냅샷 시트
> - `on_change` = 변경 건만 담은 시트 (일일 리포트의 메인)
> - `merge` = 마스터 테이블 갱신 (누적 현황 시트)
> - **`extract` 를 선언적 데이터로 표현**하는 방식 — 게시판 파싱 규칙을 `config/sources.yaml` 안에 `{"필드": {"css": "...", "value": "..."}}` 형태로 넣는다.
>
> **기각**: Ruby/Rails 스택 전체.
### 7.4 `larsyencken/csvdiff` — diff 출력 스키마의 정본 [F#32]
| 항목 | 값 |
|---|---|
| URL | https://github.com/larsyencken/csvdiff |
| 설명 | "Generate a diff between two tabular datasets expressed in CSV files." |
| ★ / Fork | **131 / 31** |
| 언어 | Python |
| 상태 | **2021년 2월 18일 아카이브 (read-only, 유지보수 종료)** |
| 라이선스 | BSD-3-Clause |
CLI:
```bash
csvdiff --style=summary KEY file1.csv file2.csv
csvdiff --style=summary id a.csv b.csv
```
옵션: `--style`(summary, pretty), `--output`(JSON 출력 파일), `--ignore-columns`(제외 컬럼 콤마 목록), `--significance`(수치 비교 정밀도, 음수는 자릿수)
**JSON 출력 구조** (이것을 채택한다):
- `_index` — 키 컬럼 배열
- `added` — 새 행 전체 필드
- `removed` — 삭제된 행 전체 필드
- `changed` — 변경 행. 키별로 필드 레벨 "from/to"
Python API:
```python
import csvdiff
patch = csvdiff.diff_files('a.csv', 'b.csv', ['id'])
patch = csvdiff.diff_records(records_a, records_b, ['id'])
```
patch 적용 메서드도 제공.
> **채택 결정**: **JSON 스키마와 CLI 옵션 이름은 채택, 라이브러리는 미채택(아카이브).** 우리 `diff/` 모듈이 동일 구조를 만들어낸다:
> ```json
> {
> "_index": ["DMF_PERMIT_NO"],
> "added": [ { "DMF_PERMIT_NO": "...", "INGR_KOR_NAME": "...", ... } ],
> "removed": [ { "DMF_PERMIT_NO": "...", ... } ],
> "changed": {
> "20250001": {
> "MNFCTR_PLACE": { "from": "구주소", "to": "신주소" }
> }
> }
> }
> ```
> `--ignore-columns` 는 필수다 — 조회수·수집시각 같은 노이즈 필드를 diff 에서 빼야 오탐이 사라진다.
### 7.5 `ecprice/newsdiffs` [F#43]
| 항목 | 값 |
|---|---|
| URL | https://github.com/ecprice/newsdiffs |
| 설명 | "A website and framework that tracks changes in online news articles over time." |
| ★ / Fork | **506 / 136** |
| 언어 | Python (Django) |
아키텍처:
- **스냅샷 저장**: 기사별 git 저장소가 아니라 **디렉터리 기반**. 셋업 시 생성되는 `articles` 디렉터리에 저장
- **diff 생성**: 스크래퍼가 주기적으로 실행되어 버전을 캡처하고 스냅샷 간 변경 탐지
- **스케줄링**: cron 또는 루프
```bash
while true; do python website/manage.py scraper; sleep 60m; done
```
- **파서 프레임워크**: `parsers/` 디렉터리에 모듈식, `BaseParser` 를 상속한 사이트별 서브클래스
- **로깅**: 진행 로그 `/tmp/newsdiffs_logging`(per-run), 에러 로그 `/tmp/newsdiffs/logging_errs`(cumulative)
> **베낄 것**: **per-run 로그와 누적 에러 로그의 분리.** 우리는 `logs/runs/YYYY-MM-DD.log`(실행별) 와 `logs/errors.log`(누적)로 구현한다. 워치독이 감시할 대상은 후자다. 또한 `BaseParser` 상속 구조 — 소스가 늘어날 때 파서만 추가하면 되는 형태.
### 7.6 `simonw/git-scraper-template` [F#47]
| 항목 | 값 |
|---|---|
| URL | https://github.com/simonw/git-scraper-template |
| 설명 | "Template repository for setting up a new Git scraper using GitHub Actions." |
| ★ / Fork | **132 / 10** |
동작: GitHub Actions 가 `./download.sh`(curl) 로 URL 내용을 받아 변경이 있으면 저장소에 커밋. 기본 스케줄 24시간마다. 파일: `.github/workflows/scrape.yml`, `README.md`, `download.sh`, `scrape.sh`(템플릿 생성 시 만들어짐). 파이썬 스크래퍼는 `scrape.yml` 의 주석 블록을 풀고 `requirements.txt` 를 두면 지원.
관련 개념 [S#24]: "Git scrapers can grab data periodically, commit it to a repository if changed, creating a commit log of changes to information over time." Simon Willison 의 `git-history` 도구가 이렇게 수집한 데이터를 분석하는 데 쓰인다.
> **베낄 것**: **`data/snapshots/` 를 로컬 git 리포지토리로 만들어 매일 커밋한다.** 이러면 (1) 변경 이력이 공짜로 생기고 (2) `git diff` 로 언제든 재검증 가능하며 (3) 스냅샷 파일이 무한히 쌓이지 않는다. 원격 push 는 하지 않는다(사내 데이터).
---
## 8. (e) 크롤링 → 엑셀/시트 리포트 파이프라인
### 8.1 `Bwhiz/Auto-Excel-Reports` [F#16]
| 항목 | 값 |
|---|---|
| URL | https://github.com/Bwhiz/Auto-Excel-Reports |
| 설명 | "Scripts and workflows to automate the generation and distribution of Excel reports using Python's openpyxl library" + GitHub Actions 로 "scheduled and event-triggered report generation and email distribution" |
| ★ / Fork | 1 / 0 |
| 언어 | Python |
| 파일 | `.github/workflows/`, `assets/`, `report_script.py`, `auto_mail.py`, `requirements.txt`, `.gitignore`, `README.md` |
핵심:
1. **리포트 생성**: openpyxl 로 워크북 생성·서식
2. **스케줄**: GitHub Actions cron — 예시 워크플로가 `schedule: - cron: '0 0 * * *'` (매일 실행)
3. **배포**: `auto_mail.py` 가 SMTP 로 발송, 발신자 자격증명·수신자 주소는 환경변수
4. **보안**: GitHub Secrets 로 "securely handle sensitive information like email credentials"
> **베낄 것**: **`report_script.py`(생성)와 `auto_mail.py`(배포) 파일 분리** — 리포트를 만들 수 있는데 배포에서 실패하는 경우와, 애초에 리포트를 못 만드는 경우를 로그에서 구분할 수 있어야 한다. 우리는 `report/` 와 `notify/` 로 모듈 분리.
> **기각**: GitHub Actions(로컬 실행 요구), SMTP(1차 채널은 Windows 토스트).
### 8.2 XlsxWriter — 탭 연동 리포트의 실제 API [F#58]
우리 요구사항 "**탭(시트)별로 연동된 보기 좋은 xlsx**" 를 만족시키는 정확한 API 는 다음과 같다. (출처: https://xlsxwriter.readthedocs.io/worksheet.html)
**내부 하이퍼링크 — `write_url()`**
```python
write_url(row, col, url[, cell_format[, string[, tip]]])
```
```python
# 현재 워크시트의 셀로 링크
worksheet.write_url('A1', 'internal:Sheet2!A1')
# 다른 워크시트의 셀로 링크
worksheet.write_url('A2', 'internal:Sheet2!A1:B2')
# 시트명에 공백이 있으면 작은따옴표로 감싼다
worksheet.write_url('A3', "internal:'Sales Data'!A1")
```
**표 — `add_table()`**
```python
add_table(first_row, first_col, last_row, last_col, options)
```
```python
worksheet.add_table('B3:F7', { ... })
# 또는 행-열 표기
worksheet.add_table(2, 1, 6, 5, { ... })
```
**컬럼 너비 — `set_column()`**
```python
set_column(first_col, last_col, width, cell_format, options)
```
```python
worksheet.set_column(0, 0, 20) # A열 너비 20
worksheet.set_column(1, 3, 30) # B-D열 너비 30
worksheet.set_column('E:E', 20) # E열 너비 20
worksheet.set_column('F:H', 30) # F-H열 너비 30
```
**조건부 서식 — `conditional_format()`**
```python
conditional_format(first_row, first_col, last_row, last_col, options)
```
```python
worksheet.conditional_format('B3:K12', {'type': 'cell',
'criteria': '>=',
'value': 50,
'format': format1})
```
**확인 실패**: `autofilter()` 와 `freeze_panes()` 는 위 fetch 에서 문서 내용을 확보하지 못했다. ⚠️ 미검증 — 다만 XlsxWriter 에 두 메서드가 존재한다는 것은 널리 알려져 있으므로 구현 시 문서 재확인 필요.
> **채택**: 리포트 엔진은 **XlsxWriter**. 근거는 위 4개 API 가 한 라이브러리에서 나오고, 특히 `write_url('internal:...')` 이 "탭 간 연동"의 유일한 정공법이기 때문이다.
> **리포트 시트 설계(초안)**
> | 시트명 | 내용 | 연동 |
> |---|---|---|
> | `요약` | 실행일자, 신규/변경/취하 건수, 각 카운트 셀이 해당 시트로 internal 링크 | → 신규/변경/취하 |
> | `신규` | `added` 레코드. `add_table` + `autofilter` | 각 행의 등록번호 → `전체현황` 해당 행 |
> | `변경` | `changed` 레코드. from/to 2열 병기, 조건부 서식으로 변경 필드 강조 | 동일 |
> | `취하` | `removed` 레코드 | 동일 |
> | `전체현황` | 최신 스냅샷 전량(merge 모드) | — |
> | `실행로그` | 소스별 HTTP 상태, 건수, 소요시간, 에러 | — |
### 8.3 `HasData/playwright-scraping` [F#41]
| 항목 | 값 |
|---|---|
| URL | https://github.com/HasData/playwright-scraping |
| 설명 | "Web scraping and browser automation using Playwright in both Python and Node.js. It includes scripts for common tasks such as scraping data, interacting with web elements, handling authentication, and managing errors." |
| ★ / Fork | **15 / 4** |
| 커밋 | main 5 commits |
`Python/` 과 `NodeJS/` 가 동일한 하위 구조:
```
basics/ (브라우저 실행, headless 모드, 멀티 탭)
scraping/ (텍스트, 링크, 이미지, Shadow DOM, 대기)
selectors/ (CSS, XPath, role 기반, text 기반)
interactions/ (클릭, 폼, 드롭다운, 페이지네이션, 스크롤)
save_data/ (JSON, CSV, PDF, 다운로드, 스크린샷)
auth/ (basic auth, 쿠키)
browser/ (user agent, 프록시, 디바이스 에뮬레이션)
errors/ (재시도 로직, 타임아웃 처리)
debug/ (video/trace 녹화, 일시정지, 콘솔 검사)
```
시연 대상: Amazon, WooCommerce 상품 데이터, 요소 선택, 폼 상호작용, 페이지네이션, 무한 스크롤.
> **베낄 것**: 이 **디렉터리 목록을 fetch 모듈의 요구사항 체크리스트로 사용**한다. 특히
> - `errors/` — 재시도·타임아웃은 처음부터 설계에 넣는다
> - `debug/` — **실패 시 trace/스크린샷 자동 저장**. nedrug 게시판 구조가 바뀌었을 때 원인 파악의 유일한 단서가 된다. `data/debug/YYYY-MM-DD/` 에 저장.
> - `browser/` — User-Agent 설정. 공공기관 사이트는 기본 UA 를 차단하는 경우가 있다.
>
> **의사결정**: DMF **API 호출은 `requests` 로 충분**하다. Playwright 는 **nedrug 게시판(`/bbs/117`) 파싱에만** 조건부로 도입한다(정적 HTML 이면 requests + BeautifulSoup 로 끝낸다). ⚠️ 미검증 — 게시판이 JS 렌더링인지 실측 필요.
### 8.4 검색 결과에만 있는 파이프라인 예시들 [S#7][S#17][S#29]
| 프로젝트 | 요점 |
|---|---|
| `god233012yamil/Excel-Automation-Using-Python` | openpyxl 로 엑셀 읽기/쓰기/수정 예제 모음 |
| `prabudevarajan/Task-Reminder-Automation-...` | pandas + openpyxl + `schedule` 라이브러리. Excel/CSV 에 태스크 저장, 15/7/3/1일 전 이메일, **daily scheduler + 로깅** |
| `ManiMozaffar/linkedIn-scraper` | Playwright 봇 + FastAPI. 결과를 DB 와 **Telegram 채널**에 저장 |
| `dineshk-qa/playwright.slack.reporter` | Playwright 결과(통과/실패/flaky 수)를 Slack 웹훅으로 |
| `nmpowell` gist | 기존 Windows Task Scheduler 태스크와 상호작용하는 파이썬 스크립트 |
**`schtasks` 로 일일 태스크를 만드는 표준 구문** [S#29]:
```
schtasks /create /tn "Task Name" /tr path_to_bat_file/run.bat /sc DAILY /st 16:00
```
일반적 접근: 파이썬을 호출하는 배치 파일을 만들고 그것을 Task Scheduler 에 등록.
> **채택**: 우리 06:00 실행 등록 명령의 기본형은 다음과 같다(⚠️ 실측 필요 — 계정/권한/`/ru` `/rl` 옵션).
> ```
> schtasks /create /tn "DMF_Crawler_Daily" /tr "D:\workspace\DMF_Crawler\ops\tasks\run_daily.cmd" /sc DAILY /st 06:00 /rl HIGHEST /f
> ```
### 8.5 대안 경로: `786raees/task-scheduler-python` [F#45]
| 항목 | 값 |
|---|---|
| URL | https://github.com/786raees/task-scheduler-python |
| 설명 | "The Task Scheduler Python project provides a convenient way to interact with the Windows Task Scheduler using Python and the `win32com.client` library." |
| ★ / Fork | 2 / 0 |
| 언어 | Python |
| 커밋 | main 2 commits |
| 파일 | `.vscode/`, `app/`, `.gitignore`, `LICENSE`(MIT), `README.md`, `main.py`, `requirements.txt` |
| 의존성 | `pip install pywin32` |
`TaskScheduler` 클래스 메서드:
- `create_task()` — 실행 경로, 인자, **ISO 8601 트리거 시각**
- `get_all_tasks()` — 태스크 열거
- `toggle_task()` — 활성/비활성 토글
- `run_task()` — 즉시 실행
- `delete_task()` — 삭제
> **부분채택**: 설치/제거 스크립트에서 `schtasks` 문자열을 조립하는 대신 COM 으로 다루면 **태스크 존재 여부 확인·재등록·상태 조회**가 훨씬 안정적이다. 우리 `ops/tasks/install_tasks.py` 의 구현 방식으로 이 API 형태를 참고한다. 단 코드 복사는 하지 않는다(★2, 2 commits).
---
## 9. (f) AI CLI headless 자동화
> **중요한 전제 정정**: 이 프로젝트가 실제로 사용할 CLI 는 **Google Antigravity CLI (`agy`)** 이며 headless 모드는 `agy -p` 다. 그러나 raw dump 는 `agy` 를 조사하지 않았다 — `claude -p`, `gemini -p`, `codex exec` 세 가지만 조사되었다. 따라서 **아래 내용은 "agy 에 사상해야 할 공통 규약"으로 읽어야 하며, `agy` 의 실제 플래그는 전부 ⚠️ 미검증이다.**
### 9.1 `claude -p` (Claude Code headless) — 가장 상세히 문서화된 레퍼런스 [F#25]
출처: https://code.claude.com/docs/en/headless
**기본 사용**
```bash
claude -p "Find and fix the bug in auth.py" --allowedTools "Read,Edit,Bash"
claude -p "What does the auth module do?"
```
- `-p` (= `--print`) 로 비대화형 실행
- **종료 코드**: 성공 0, 실패 시 0 이 아닌 값. 잘못된 플래그는 실행 전 stderr 로 보고. 실행 중 발생한 실패(예: 인증 누락)는 결과로 stdout 에 출력
- `-p` 와 자주 조합: `--continue`, `--allowedTools`, `--output-format`
- `--bg` 는 거부됨. `--cloud` + 태스크 설명도 거부. `--cloud` + 세션 ID + `-p` 는 클라우드 세션에 메시지를 큐잉하고 종료
**`--bare` 모드 (CI/스크립트 권장)**
```bash
claude --bare -p "Summarize README.md" --allowedTools "Read"
```
- hooks, skills, custom commands, subagents, plugins, MCP servers, auto memory, CLAUDE.md 자동 탐색을 **건너뛴다** → 모든 머신에서 동일 결과
- bare 모드에서는 OAuth 자격증명/시스템 키체인을 읽지 않는다. `ANTHROPIC_API_KEY` 를 환경변수로 설정하거나 `--settings` JSON 에 `apiKeyHelper` 제공
- bare 모드 기본 도구: Bash, file read, file edit
- 컨텍스트 주입 플래그:
| 로드할 것 | 플래그 |
|---|---|
| 시스템 프롬프트 추가 | `--append-system-prompt`, `--append-system-prompt-file` |
| 설정 | `--settings <file-or-json>` |
| MCP 서버 | `--mcp-config <file-or-json>` |
| 커스텀 에이전트 | `--agents <json>` |
| 플러그인 | `--plugin-dir <path>`, `--plugin-url <url>` |
- 문서 주석: "`--bare` is the recommended mode for scripted and SDK calls, and will become the default for `-p` in a future release."
- **경고**: `--bare` 없이는 신뢰하지 않은 폴더에서도 프로젝트 `.claude/settings.json` 의 hooks 를 실행하고 `.mcp.json` 의 서버에 연결한다. `-p` 세션은 워크스페이스 신뢰 대화상자도, 서버별 승인 프롬프트도 표시하지 않는다.
**stdin 파이프**
```bash
cat build-error.txt | claude -p 'concisely explain the root cause of this build error' > output.txt
```
- 파이프 stdin 상한 **10MB**. 초과 시 명확한 에러와 함께 비정상 종료. 더 큰 입력은 파일로 쓰고 경로를 프롬프트에 참조
- stdin 을 읽을 수 없으면 stderr 에 경고 후 커맨드라인 프롬프트로 계속. **v2.1.211 이전에는 Windows 에서 읽을 수 없는 stdin 이 세션을 크래시시키거나 출력 없이 조용히 종료시켰다**
**빌드 스크립트 통합 예 (Windows 이식성 고려한 이스케이프)**
```json
{
"scripts": {
"lint:claude": "git diff main | claude -p \"you are a typo linter. for each typo in this diff, report filename:line on one line and the issue on the next. return nothing else.\""
}
}
```
**구조화 출력 — 이 프로젝트의 핵심 규약**
- `--output-format` 값: `text`(기본), `json`(result·session ID·metadata 포함), `stream-json`(개행 구분 JSON 스트리밍)
```bash
claude -p "Summarize this project" --output-format json
```
```bash
claude -p "Extract the main function names from auth.py" \
--output-format json \
--json-schema '{"type":"object","properties":{"functions":{"type":"array","items":{"type":"string"}}},"required":["functions"]}'
```
- 구조화 결과는 응답의 **`structured_output` 필드**에 담긴다. 텍스트 결과는 `result` 필드
- 스키마가 유효하지 않으면 `Error: --json-schema is not a valid JSON Schema` + 검증기 진단 출력. `format` 키워드(예: `"format": "email"`)는 허용되지만 **주석으로만 취급하고 강제하지 않는다**. v2.1.205 이전에는 잘못된 스키마를 조용히 무시하고 비구조화 텍스트를 반환했다
- `--output-format json` 사용 시 응답 페이로드에 `total_cost_usd` 와 모델별 비용 내역 포함(클라이언트 측 추정치)
jq 로 파싱:
```bash
# 텍스트 결과 추출
claude -p "Summarize this project" --output-format json | jq -r '.result'
# 구조화 출력 추출
claude -p "Extract function names from auth.py" \
--output-format json \
--json-schema '{"type":"object","properties":{"functions":{"type":"array","items":{"type":"string"}}},"required":["functions"]}' \
| jq '.structured_output'
```
**스트리밍**
```bash
claude -p "Explain recursion" --output-format stream-json --verbose --include-partial-messages
```
```bash
claude -p "Write a poem" --output-format stream-json --verbose --include-partial-messages | \
jq -rj 'select(.type == "stream_event" and .event.delta.type? == "text_delta") | .event.delta.text'
```
- 스트림 마지막 줄은 최종 응답 텍스트·비용·세션 메타데이터를 담은 `result` 메시지
- 소비자가 느리게 읽으면 큐 배출까지 대기, 최대 30초(v2.1.214 이전엔 약 2초라 대용량 응답 끝이 잘렸다)
- 서브에이전트 메시지는 `parent_tool_use_id` 로 구분(메인은 `null`). `--forward-subagent-text` 또는 `CLAUDE_CODE_FORWARD_SUBAGENT_TEXT` 로 서브에이전트 텍스트·사고 블록도 방출(v2.1.211+)
**프로세스 수명**
- 백그라운드 Bash 태스크는 최종 결과 반환 + stdin 닫힘 **약 5초 후** 종료. v2.1.163 이전에는 종료하지 않는 백그라운드 프로세스가 `claude -p` 를 무한정 붙잡았다
- 백그라운드 서브에이전트/워크플로는 5초 유예에서 면제되어 완료까지 대기. v2.1.182 부터 연속 유휴 대기 **최대 10분**으로 상한. `CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS` 로 조정, `0` 이면 무제한
- **SIGTERM 으로 중단하면 종료 코드 143.** 진행 중이던 턴은 미완료로 남고 결과가 기록되지 않는다. 턴을 끝내려면 SIGINT 를 보내거나 SDK `interrupt()` 호출. SIGTERM 시 실행 중 Bash 명령의 프로세스 트리를 종료하고 `SessionEnd` 훅을 실행 후 종료
> **agy 로 사상할 규약 (⚠️ 플래그명은 실측 필요)**
> | 개념 | claude | gemini | codex | **agy (미검증)** |
> |---|---|---|---|---|
> | 비대화형 프롬프트 | `-p` / `--print` | `-p` | `codex exec` | `agy -p` |
> | JSON 출력 | `--output-format json` | `--format=json` | (문서 미확인) | `?` |
> | 스키마 강제 | `--json-schema` | 없음 | 없음 | `?` |
> | 권한 우회 | `--dangerously-skip-permissions` | — | `--ask-for-approval never` | `?` |
> | 쓰기 허용 | `--allowedTools` | — | `--sandbox workspace-write` | `?` |
> | 전자동 | — | — | `--full-auto` | `?` |
> | 시스템 프롬프트 | `--append-system-prompt-file` | `GEMINI_SYSTEM_MD` 환경변수 | — | `?` |
> | 컨텍스트 최소화 | `--bare` | — | `--ephemeral` | `?` |
>
> **반드시 지킬 것**: (1) 프롬프트는 파일에 두고 `$(cat ...)` 로 주입, (2) 출력은 파일로 리다이렉트 후 파싱, (3) 종료 코드로 성공/실패 분기, (4) **타임아웃을 반드시 걸 것**, (5) 실패해도 파이프라인 전체가 죽지 않게 — AI 요약은 **선택적 보강**이지 필수 경로가 아니다.
### 9.2 실전 호출 예시 — drew.tech [F#21]
출처: https://drew.tech/posts/claude-code-as-a-cron-job
```sh
claude \
--dangerously-skip-permissions \
--output-format json \
--json-schema "$(cat /tmp/schema.json)" \
-p "$(cat /tmp/prompt.txt)" \
> /tmp/output.json
```
| 플래그 | 용도 |
|---|---|
| `--dangerously-skip-permissions` | 권한 프롬프트 우회 |
| `--output-format json` | 구조화 JSON 반환 |
| `--json-schema` | 제공된 스키마로 출력 검증 |
| `-p` | 파일 또는 stdin 에서 프롬프트 수용 |
출력 처리: stdout 을 `/tmp/output.json` 으로 리다이렉트 → `sandbox.readFileToBuffer()` 로 읽기 → JSON 파싱 후 `structured_output` 필드 추출 → Zod 스키마로 검증.
함정: **"snapshots preserve auth state"** — MCP·통합은 자동화 시작 전에 스냅샷에 미리 설정해두어야 한다. PATH/환경변수 이슈는 이 글에서 언급되지 않았다.
> **이 스니펫이 우리 `orchestrate` 모듈의 원형이다.** 4개 요소 — 스키마 파일, 프롬프트 파일, 출력 파일, 권한 우회 플래그 — 를 그대로 `ops/agent/run_agy.cmd` 에 옮긴다.
### 9.3 `jshchnz/claude-code-scheduler` [F#9][F#39][F#44][F#57][F#66]
| 항목 | 값 |
|---|---|
| URL | https://github.com/jshchnz/claude-code-scheduler |
| 설명 | "Put Claude on autopilot. Schedule code reviews, security audits, and anything else - Claude Code runs them automatically, even while you sleep." |
| ★ / Fork | **510 / 37** |
| 언어 | TypeScript |
| 요구사항 | "Claude Code v1.0.33+" |
파일 구조:
```
.claude-plugin/ 플러그인 설정
commands/ CLI 명령 구현
src/ 소스
skills/scheduler/ 스케줄러 스킬 모듈
examples/ 사용 예시
dist/ 빌드 산출물
package.json, tsconfig.json, vitest.config.ts
```
`src/` 하위 [F#57]:
```
__tests__/ cron/ history/ logs/ schedulers/ utils/ vcs/
config.ts index.ts types.ts
```
`src/schedulers/` 하위 [F#66] (정확히 인용):
```
base.ts
darwin.ts
index.ts
linux.ts
windows.ts
```
**동작**: 태스크를 OS 네이티브 스케줄러에 등록하고, 예정 시각에 `claude -p "your command"` 를 실행. 출력은 `~/.claude/logs/<task-id>.log` 에 로깅.
**OS 지원**: "macOS (launchd), Linux (crontab), Windows (Task Scheduler)"
**설정 형식**: `.claude/schedules.json`(프로젝트) 또는 `~/.claude/schedules.json`(전역). 속성: `id`, `name`, `trigger`(cron 표현식), `execution`(command, timeout, skipPermissions), 선택적 `worktree`.
**지원 플래그**: `--dangerously-skip-permissions`(파일 편집·명령 실행 자율 수행). `--output-format` 은 README 에 명시되지 않음.
**실제 스케줄 JSON 전문** [F#44] (`examples/daily-review.json`, 원문 그대로):
```json
{
"version": 1,
"tasks": [
{
"id": "daily-code-review",
"name": "Daily Code Review",
"description": "Review commits from the previous day for code quality and potential issues",
"enabled": true,
"trigger": {
"type": "cron",
"expression": "0 9 * * 1-5",
"timezone": "local"
},
"execution": {
"command": "Review all commits from yesterday. Check for: 1) Code quality issues, 2) Security vulnerabilities, 3) Performance concerns, 4) Missing tests. Summarize findings and suggest improvements.",
"workingDirectory": ".",
"timeout": 300
},
"tags": ["code-quality", "daily"],
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
}
],
"settings": {
"defaultTimezone": "local",
"logRetentionDays": 30,
"maxExecutionHistory": 100
}
}
```
`examples/` 에는 `daily-review.json` 과 `weekly-audit.json` 두 파일이 있다 [F#39].
Windows 에서 스케줄된 Claude 태스크 확인 [S#28]:
```
schtasks /query /tn "ClaudeSchedule*"
```
플러그인은 태스크 생성 시 **래퍼(wrapper) 셸 스크립트**를 생성해 Claude Code 를 프롬프트와 함께 호출하고, 그 래퍼를 Task Scheduler 에 등록한다. Task Scheduler 는 시스템 레벨 프로세스이므로 앱을 열지 않아도 실행된다.
> **베낄 것 (3가지, 전부 채택)**
> 1. **`src/schedulers/{base,darwin,linux,windows}` 어댑터 분리** → 우리는 Windows 만 필요하지만 `ops/tasks/` 에 동일한 인터페이스를 두어 나중에 서버 이관이 가능하게 한다.
> 2. **스케줄 정의를 JSON/YAML 파일로.** 위 JSON 의 필드 구성(`id`, `name`, `enabled`, `trigger.{type,expression,timezone}`, `execution.{command,workingDirectory,timeout}`, `tags`, `settings.{defaultTimezone,logRetentionDays,maxExecutionHistory}`)을 거의 그대로 `config/schedule.yaml` 로 옮긴다. 특히 **`timeout`(초)과 `logRetentionDays`** 는 필수다.
> 3. **태스크 ID 별 로그 파일**(`logs/<task-id>.log`). 우리는 `logs/runs/<task-id>-YYYYMMDD.log`.
### 9.4 Gemini CLI [F#35][S#20]
`addyosmani/gemini-cli-tips` (★2.4k / Fork 105):
```bash
# 원샷
gemini -p "Your prompt here"
# stdin 파이프
echo "Count to 10" | gemini
```
- "output a single response and exit" — 대화형 REPL 진입 없음
- `--format=json` 으로 프로그램적 소비. "parse the JSON to get the answer or any tool actions details"
- 시스템 프롬프트 교체:
```bash
export GEMINI_SYSTEM_MD="/path/to/custom_system.md"
```
- 위치: "It transforms Gemini CLI from an interactive assistant into a **backend service** or utility that other programs can call."
- 관련 문서/논의: https://github.com/google-gemini/gemini-cli/discussions/3215 (Headless execution), https://geminicli.com/docs/issue-and-pr-automation/
**주의**: 블로그가 소개한 예제 저장소 `github.com/testing-in-production/gemini-jobs` 는 **404** 다 [F#17].
### 9.5 Codex CLI [S#26]
- `codex exec` 는 대화형 TUI 없이 실행 — CI/CD, Git hooks, cron job, 스크립트 자동화용
- 단일 에이전트 세션을 시작해 태스크를 완료까지 실행, 진행 상황은 stderr 로 스트리밍, 최종 에이전트 메시지는 stdout 으로, 그리고 종료. **승인 프롬프트 없음**
- 기본 샌드박스는 **read-only**. 파일 수정이 필요하면 `--sandbox workspace-write`, 무인 실행은 `--ask-for-approval never`
- GitHub Actions 예시: `codex exec --full-auto` 를 `cron: '0 8 * * 1-5'` 스케줄로
- 배치 패턴: bash 루프로 `codex exec --full-auto --ephemeral` 를 태스크 목록마다 별도 세션으로 실행
> **agy 에 대한 시사점**: 세 CLI 모두 **"기본은 안전(읽기 전용/승인 요구), 자동화는 명시적 플래그로 해제"** 구조다. `agy` 도 동일할 가능성이 높으므로, 부트스트랩 시 `agy --help` 로 (a) 비대화형 프롬프트 플래그, (b) 쓰기 권한 플래그, (c) 승인 우회 플래그, (d) JSON 출력 플래그 4가지를 반드시 식별해 `docs/research/` 에 기록해야 한다.
### 9.6 Claude Code Desktop 스케줄드 태스크 — 우리가 쓰지 않을 경로 [F#49]
출처: https://code.claude.com/docs/en/desktop-scheduled-tasks
세 가지 스케줄링 옵션 비교(문서 표 원문):
| | Cloud (routines) | Desktop | `/loop` |
|---|---|---|---|
| 실행 위치 | 클라우드, 기본 Anthropic 관리 | 사용자 머신 | 사용자 머신 |
| 머신 켜짐 필요 | No | **Yes** | Yes |
| 열린 세션 필요 | No | No | **Yes** |
| 재시작 후 지속 | Yes | Yes | 만료 전이면 `--resume` 시 복원 |
| 로컬 파일 접근 | No (fresh clone) | **Yes** | Yes |
| MCP 서버 | 태스크별 커넥터 | 설정 파일 + 커넥터 | 세션 상속 |
| 권한 프롬프트 | No (자율 실행) | 태스크별 설정 가능 | 세션 상속 |
| 스케줄 커스터마이즈 | CLI `/schedule` | Yes | Yes |
| 최소 간격 | **1시간** | 1분 | 1분 |
주요 제약:
- **로컬 태스크는 앱이 열려 있고 컴퓨터가 깨어 있을 때만 발화한다.** 컴퓨터가 자면 실행은 스킵
- Settings → Desktop app → General 의 **Keep computer awake** 로 유휴 절전 방지 가능. 노트북 뚜껑을 닫으면 여전히 잠듦
- **놓친 실행**: 앱 시작/기기 깨어남 시 지난 7일 내 놓친 실행을 확인해 **가장 최근에 놓친 1회만** 캐치업 실행하고 나머지는 폐기. "A task scheduled for 9am might run at 11pm if your computer was asleep all day."
- 태스크마다 몇 분의 결정론적 지연 오프셋이 붙어 API 트래픽을 분산
- 프롬프트 파일: `~/.claude/scheduled-tasks/<task-name>/SKILL.md` (YAML frontmatter 로 `name`/`description`, 본문이 프롬프트). 스케줄·폴더·모델·활성 상태는 이 파일에 없음
- 태스크가 실행 중 `update_scheduled_task` MCP 도구로 자기 스케줄/프롬프트를 수정 가능
> **기각 결정**: Desktop 스케줄드 태스크는 **앱이 열려 있어야 한다**는 치명적 제약 때문에 "재부팅 후에도 자동 복구되는 서비스" 요구를 만족하지 못한다. **Windows Task Scheduler + 워치독**으로 간다.
> **다만 채택할 개념 2가지**: ① **놓친 실행 캐치업 로직** — 06:00 에 PC 가 꺼져 있었다면 부팅 후 1회만 따라잡는다(중복 실행 금지). ② **프롬프트를 YAML frontmatter + 본문 마크다운 파일로 관리**.
---
## 10. (g) Windows 서비스화 · 워치독 · 토스트
### 10.1 `winsw/winsw` — 서비스화 1순위 [F#10][F#23][F#59]
| 항목 | 값 |
|---|---|
| URL | https://github.com/winsw/winsw |
| 설명 | "A wrapper executable that can run any executable as a Windows service, in a permissive license." |
| ★ / Fork | **14.3k / 1.7k** |
| 언어 | C# |
| 커밋 | v3 브랜치 841 commits |
| 최신 릴리스 | **`v3.0.0-alpha.11`** — "29 Jan 02:20" |
| 최신 안정 | **`v2.12.0`** — "28 Jan 16:22" |
| 배포 | GitHub Releases + NuGet/Maven(2.x) |
**설치 절차** (README 원문 요약): WinSW.exe 또는 WinSW.zip 을 받아 → `myapp.xml` 작성 → `winsw install myapp.xml` → `winsw start myapp.xml` → `winsw status myapp.xml`. 또는 WinSW.exe 를 `myapp.exe` 로 리네임하고 `myapp.xml` 을 나란히 두면 자동 발견된다.
**XML 설정 요소 전문** [F#23]:
```xml
<!-- 실패 시 동작: restart / reboot / none. delay 단위: sec/secs/min/mins/hour/hours/day/days -->
<onfailure action="restart" delay="10 sec"/>
<onfailure action="restart" delay="20 sec"/>
<onfailure action="reboot" />
<!-- Windows SCM 이 실패 카운트를 리셋하는 시점. 기본 1 day -->
<resetfailure>1 hour</resetfailure>
<!-- Automatic | Manual. 기본 Automatic -->
<startmode>Automatic</startmode>
<!-- Automatic 일 때 지연 시작 -->
<delayedAutoStart>true</delayedAutoStart>
<!-- 로그 모드: append(기본) | reset | ignore | roll -->
<log mode="roll"></log>
<!-- 시작/정지 실행 파일과 인자 -->
<executable>catalina.sh</executable>
<startarguments>jpda run</startarguments>
<stopexecutable>catalina.sh</stopexecutable>
<stoparguments>stop</stoparguments>
<!-- 환경변수 -->
<env name="HOME" value="c:\\abc" />
<!-- 작업 디렉터리 -->
<workingdirectory>C:\\application</workingdirectory>
<!-- 프로세스 우선순위: idle | belownormal | normal | abovenormal | high | realtime -->
<priority>idle</priority>
<!-- 서비스 계정. gMSA 는 username 끝에 $ 를 붙이고 password 생략 -->
<serviceaccount>
<username>DomainName\\UserName</username>
<password>Pa55w0rd</password>
<allowservicelogon>true</allowservicelogon>
</serviceaccount>
```
설정 XML 은 `%Name%` 형태의 환경변수 확장을 지원한다.
샘플 파일: https://github.com/winsw/winsw/blob/v3/samples/minimal.xml (필수 옵션만), https://github.com/winsw/winsw/blob/v3/samples/complete.xml (전체 옵션)
> **채택 (1순위)** — 근거:
> - `<onfailure action="restart" delay="10 sec"/>` 를 **여러 개 나열**해 1차/2차 재시도 지연을 다르게 줄 수 있다. 요구사항 "서비스가 죽으면 복구" 를 선언적으로 만족.
> - `<resetfailure>1 hour</resetfailure>` 로 "1시간 정상 동작하면 실패 카운트 리셋" — 무한 재시작 루프 방지.
> - `<log mode="roll">` 로 로그 로테이션 내장.
> - `<startmode>Automatic</startmode>` + `<delayedAutoStart>true</delayedAutoStart>` 로 **재부팅 후 자동 복구** 요구를 만족. 지연 시작은 부팅 직후 네트워크 미준비 상태를 회피.
> - 파이썬 런타임 의존이 없다(C# 단일 exe).
>
> **우리 XML 초안** (`ops/winsw/dmf-watchdog.xml`):
> ```xml
> <service>
> <id>DMFCrawlerWatchdog</id>
> <name>DMF Crawler Watchdog</name>
> <description>DMF_Crawler 일일 파이프라인 감시 및 복구 안내</description>
> <executable>D:\workspace\DMF_Crawler\.venv\Scripts\python.exe</executable>
> <arguments>-m dmf_crawler watchdog</arguments>
> <workingdirectory>D:\workspace\DMF_Crawler</workingdirectory>
> <startmode>Automatic</startmode>
> <delayedAutoStart>true</delayedAutoStart>
> <onfailure action="restart" delay="10 sec"/>
> <onfailure action="restart" delay="60 sec"/>
> <onfailure action="restart" delay="300 sec"/>
> <resetfailure>1 hour</resetfailure>
> <log mode="roll"></log>
> <env name="PYTHONUTF8" value="1"/>
> <priority>belownormal</priority>
> </service>
> ```
> ⚠️ 미검증 — `<id>`, `<name>`, `<description>`, `<startarguments>` vs `<arguments>` 의 정확한 요구 여부는 `samples/minimal.xml` 로 실측 필요.
### 10.2 NSSM — 서비스화 2순위 [F#24][F#51][F#14]
`kirillkovalenko/nssm` (★1.2k / Fork 169, C++, 버전 **2.24 / 2014-08-31**). README 는 `http://nssm.cc/` 를 공식 문서로 지목하며, 이 저장소는 **커뮤니티 포크로 보인다**(공식 소스 미러 여부 불확실 — ⚠️ 미검증). 핵심 문장: NSSM "can start any application as an NT service and will restart the service if it fails for any reason."
**CLI 사용법 전문** [F#24] (출처: https://nssm.cc/usage):
```
nssm install <servicename> <application> [<options>]
```
```
nssm set <servicename> Application C:\path\to\app.exe
nssm set <servicename> AppDirectory C:\startup\directory
nssm set <servicename> AppParameters argument1 argument2
nssm set <servicename> AppStdout C:\path\to\output.log
nssm set <servicename> AppStderr C:\path\to\error.log
nssm set <servicename> Start SERVICE_AUTO_START
nssm set <servicename> Start SERVICE_DELAYED_AUTO_START
```
**종료 액션** (애플리케이션 종료 시 반응): `Restart`(자동 재실행) / `Ignore`(중지 상태 유지) / `Exit`(서비스 중지). 레지스트리 경로: `HKLM\System\CurrentControlSet\Services\<servicename>\Parameters\AppExit`
**재시작 스로틀링** (CPU 루프 방지 — 임계 밀리초 내 종료 시 재시작 지연):
```
nssm set <servicename> AppThrottle 1500
```
**재시작 지연** (재시작 간 강제 간격):
```
nssm set <servicename> AppRestartDelay 3000
```
**로그 로테이션**:
```
nssm set <servicename> AppRotateFiles 1
```
**제거**:
```
nssm remove <servicename> confirm
```
**전체 구성 예시**:
```
nssm install MyService "C:\Program Files\app.exe"
nssm set MyService AppDirectory C:\Program Files
nssm set MyService AppParameters --config settings.ini
nssm set MyService AppStdout C:\logs\output.log
nssm set MyService AppStderr C:\logs\error.log
nssm set MyService Start SERVICE_AUTO_START
nssm set MyService AppThrottle 2000
nssm set MyService AppRestartDelay 5000
```
**`larsekje/PythonWindowsServices` 의 실전 지식** [F#14] (★1 / Fork 0, Python. 파일: `/logs`, `/scripts`, `/windows_service`, `.gitignore`, `readme.md`, `requirements.txt`):
```
nssm install "SERVICE_NAME" "PATH_TO_PYTHON.exe" "PATH_TO_PYTHON_SCRIPT.py"
nssm start SERVICE_NAME
```
요구사항: NSSM 이 시스템 PATH 에 있을 것, requirements.txt 로 파이썬 환경 구성, 그리고 **"NSSM 이 로그 파일을 자동 생성하지 않으므로 로그 파일을 미리 만들어 둘 것"**.
> **부분채택 (폴백)**: WinSW 설치가 실패하거나 .NET 런타임 문제가 생기면 NSSM 으로 전환한다. `AppThrottle`(1500~2000ms) + `AppRestartDelay`(3000~5000ms) 조합이 WinSW 의 `onfailure delay` 와 등가다. **버전이 2014년이라는 점이 감점 요인.**
### 10.3 pywin32 서비스 — **기각** [F#15][F#40][F#52][S#16]
`HaroldMills/Python-Windows-Service-Example` (★21 / Fork 11, Python, MIT). 파일: `.gitignore`, `LICENSE`, `README.md`, `example_service.py`, `example_service.spec`.
- 의존성: "The service should be built in a Python environment that includes the `pywin32` and `pyinstaller` packages."
- 빌드: `pyinstaller example_service.spec` (저장소 루트에서)
- 설치: `build\example_service` 에서 `example_service.exe install` → `example_service.exe start`
- **"The commands must be issued from a command prompt that was run as administrator."**
- 제거: `example_service.exe stop` → `example_service.exe remove` (관리자 권한)
- **치명적 주석**: "as of this writing (2016-03-37), PyInstaller supports Python versions only through 3.5"
`drmalex07/10554232` gist 의 표준 골격 [F#40]:
```python
class HelloWorldSvc (win32serviceutil.ServiceFramework):
_svc_name_ = "HelloWorld-Service"
_svc_display_name_ = "HelloWorld Service"
def __init__(self, args):
win32serviceutil.ServiceFramework.__init__(self, args)
self.stop_event = win32event.CreateEvent(None, 0, 0, None)
socket.setdefaulttimeout(60)
self.stop_requested = False
```
`SvcStop` 은 `self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)` 후 정지 플래그를 세운다. `SvcDoRun` 이 `self.main()` 을 호출하고, 메인 루프는 `if self.stop_requested:` 로 탈출한다.
```python
if __name__ == '__main__':
win32serviceutil.HandleCommandLine(HelloWorldSvc)
```
스레드 코멘트: 이 기본 패턴은 타임아웃 에러를 만날 수 있으며, 개선판은 `len(sys.argv)` 로 커맨드라인 설치와 실제 서비스 실행을 구분한다.
**기각의 결정적 근거 — pywin32 issue #1563** [F#52]:
> 요청 내용: `win32serviceutil.ServiceFramework` 서비스가 Windows 복구 액션(자동 재시작 등)을 트리거하는 방식으로 종료되기를 원한다. 복구 액션은 서비스가 `SERVICE_STOPPED` 를 보고하지 않고 끝날 때 발동한다.
> 문제: **`SvcRun()` 이 예외를 던지거나 `sys.exit()` 를 호출하면, pywin32 의 정리 코드가 자동으로 서비스 상태를 `SERVICE_STOPPED` 로 설정해버려 복구 액션이 트리거되지 않는다.**
> 현재 우회책: `os.kill(os.getpid(), signal.SIGABRT)` — 보고자 본인도 "극단적 조치"라고 인정.
> 요청: `SvcRun()` 이 자동 정리(특히 `SetServiceStatus()` 호출)를 건너뛰도록 플래그를 걸 수 있게 해달라.
관련 [S#16]: 크래시한 서비스의 재시작은 `ChangeServiceConfig2` 로 실패 액션 딕셔너리를 설정해 구성할 수 있다. `win32service` 모듈로 SCM 에 연결해 서비스를 열거하고 상태(Running/Stopped/Paused)를 확인하며 시작·중지·일시정지·재시작할 수 있다.
> **결정: pywin32 로 서비스를 직접 구현하지 않는다.** 서비스 껍데기는 WinSW/NSSM 에 맡기고, pywin32 는 **워치독이 서비스 상태를 조회할 때만**(`win32service` 열거/상태 조회) 사용한다.
### 10.4 `michalzobec/autorunsalerts` — 운영 패턴 정본 [F#34]
| 항목 | 값 |
|---|---|
| URL | https://github.com/michalzobec/autorunsalerts |
| 설명 | "Simple toast notifications for changes to autoruns configurations on windows" |
| ★ | 0 |
| 언어 | PowerShell |
**2개 스케줄드 태스크 구조**:
1. **`AutorunsAlert` (SYSTEM 컨텍스트, 60분마다)** — `autorunsc.exe` 로 현재 상태 스캔 → `state.json` 의 이전 기준선과 비교 → 탐지된 모든 변경을 `audit.log` 에 기록
2. **`AutorunsAlertToast` (사용자 컨텍스트, 15분마다)** — 스캐너가 세운 알림 플래그를 확인하고 토스트를 띄우며 `audit.log` 로 연결
파일:
- `autorunsalert.ps1` — 메인 스캔 스크립트
- `autorunstoast.ps1` — 토스트 전달 스크립트
- `configuration.json` — 공유 설정 변수
- `state.json` — 비교용 이전 스캔 기준선
- `audit.log` — 변경 이력 및 조사 기록
- `install.ps1` / `uninstall.ps1` — 설치/제거
원문 결론: "This separation ensures alerts appear in the user's active session rather than the system session where detection occurs."
> **채택 (핵심 운영 패턴)**. 우리 대응:
>
> | autorunsalerts | DMF_Crawler |
> |---|---|
> | `AutorunsAlert` (SYSTEM, 60분) | `DMF_Crawler_Daily` (Task Scheduler, 매일 06:00) — 수집·diff·리포트 생성 |
> | `AutorunsAlertToast` (User, 15분) | `DMF_Crawler_Notify` (User 컨텍스트, 15분) — `state/notify_queue.json` 확인 후 토스트 |
> | `state.json` | `data/state.json` (마지막 성공 실행 시각, 마지막 스냅샷 해시) |
> | `audit.log` | `logs/audit.log` (누적 변경 이력) |
> | `configuration.json` | `config/settings.yaml` |
> | `install.ps1` / `uninstall.ps1` | `ops/tasks/install.ps1` / `uninstall.ps1` |
>
> **이 2단 분리가 필수인 이유**: 아래 10.5 참조.
### 10.5 `Windos/BurntToast` — 그리고 SYSTEM 컨텍스트 제약 [F#33][S#25]
| 항목 | 값 |
|---|---|
| URL | https://github.com/Windos/BurntToast |
| 설명 | "PowerShell Module for displaying Toast Notifications on Windows 10 and Windows Server 2019 and above." |
| ★ / Fork | **1.7k / 126** |
| 언어 | PowerShell |
| 최신 릴리스 | **v1.1.0** — Urgent 스위치(긴급 알림), 버튼 색상 커스터마이즈 추가 |
| 라이선스 | MIT |
```powershell
Install-Module -Name BurntToast
New-BurntToastNotification -Text "제목", "본문"
New-BTButton # 인터랙티브 버튼
# -AppLogo 로 앱 브랜딩
```
**결정적 제약 (원문)**: "The module targets user-context notifications and has limitations when running from SYSTEM or service accounts due to the Windows notification framework's desktop session requirements."
관련 사례 [S#25]:
- `Badgerati/Hook` — 서비스 상태를 감시하다 중지되면 BurntToast 팝업(MongoDB 서비스 예시)
- Windows 업데이트 알림 패턴 (cyberdrain.com), 재부팅 알림 가이드 (dearing.dev), PDQ 블로그
> **채택**: 워치독의 사용자 알림은 **BurntToast(PowerShell) 또는 win11toast(Python)** 로 하되, **반드시 사용자 컨텍스트 태스크에서 실행**한다. 서비스(SYSTEM)에서 직접 토스트를 띄우려는 시도는 하지 않는다.
### 10.6 파이썬 토스트 라이브러리 3종 비교
| 라이브러리 | ★ | Fork | 설치 | 기반 | 특징 | 함정 |
|---|---|---|---|---|---|---|
| `GitHub30/win11toast` [F#12][F#67] | **333** | 24 | `pip install win11toast` | WinRT | `toast()`, `notify()`(논블로킹), `toast_async()`, `buttons`, `on_click`, `image`, `duration` | **실행 시 CWD 가 `C:\Windows\system32` 이므로 `os.chdir()` 필요.** `app_id` 파라미터는 문서에 없음 |
| `DatGuy1/Windows-Toasts` [F#11] | 142 | 9 | `python -m pip install windows-toasts` | WinRT (pywin32 아님) | `Toast()`, `WindowsToaster()`, `text_fields`, `on_activated` | duration 이 short/long 만 (pywin32 대비 제약) |
| `ysfchn/toasted` [F#20] | 31 | 2 | `python -m pip install toasted` | WinRT | **Windows 가 제공하는 모든 요소 지원** — 이미지, select, input, progress | Python 버전 요구사항 미문서화 |
**win11toast 코드** [F#12][F#67]:
```python
from win11toast import toast
toast('Hello Python🐍')
toast('Hello Python', 'Click to open url', on_click='https://www.python.org')
toast('Hello', 'Click a button', buttons=['Approve', 'Dismiss', 'Other'])
toast('Hello', 'Hello from Python', image='https://example.com/image.png')
toast('Hello Python🐍', duration='long')
```
```python
from win11toast import notify
notify('Hello Python', 'Click to open url', on_click='https://www.python.org')
from win11toast import toast_async
async def main():
await toast_async('Hello Python', 'Click to open url',
on_click='https://www.python.org')
```
버튼은 프로토콜 활성화 지원: `{'activationType': 'protocol', 'arguments': 'https://google.com', 'content': 'Open Google'}` → 클릭 시 `{'arguments': 'https://google.com', 'user_input': {}}` 반환. 라이선스 MIT. 선행 프로젝트로 winsdk_toast, Windows-Toasts, MarcAlx/notification.py 를 인정.
**Windows-Toasts 코드** [F#11]:
```python
from windows_toasts import Toast, WindowsToaster
toaster = WindowsToaster('Python')
newToast = Toast()
newToast.text_fields = ['Hello, world!']
newToast.on_activated = lambda _: print('Toast clicked!')
toaster.show_toast(newToast)
```
**toasted 코드** [F#20]:
```python
from toasted import Toast, Progress, Text
import asyncio
async def main():
toast = Toast()
toast.elements = [
Text("File downloader"),
Progress(value="{value}", status="Downloading files...")
]
await toast.show(dict(value=75/100))
```
`win10toast` (jithurjacob/Windows-10-Toast-Notifications) [S#9] — pip 설치 가능, 커스텀 아이콘·스레드 알림 지원. 가장 널리 쓰이지만 오래된 라이브러리.
> **채택**: **`win11toast`** 를 1순위(별 수 가장 많고 `on_click` 으로 xlsx 파일 열기 연결 가능), `toasted` 를 백필 진행률 표시용 2순위. **`os.chdir()` 함정은 워치독 스크립트 첫 줄에 반드시 반영.**
### 10.7 Apprise — 알림 추상화 계층 [F#38][F#56]
| 항목 | 값 |
|---|---|
| URL | https://github.com/caronc/apprise |
| 설명 | "Push Notifications that work with just about every platform!" |
| ★ / Fork | **17.2k / 652** |
| 언어 | Python |
| 커밋 | master 1,178 commits |
| 설치 | `pip install apprise` |
```python
import apprise
apobj = apprise.Apprise()
apobj.add('mailto://myuserid:mypass@gmail.com')
apobj.notify(body='notification text', title='my title')
```
URL 형식:
| 서비스 | URL |
|---|---|
| Windows Toast | `windows://` |
| Slack | `slack://TokenA/TokenB/TokenC/Channel` |
| Telegram | `tgram://bottoken/ChatID` |
| Email | `mailto://userid:pass@domain.com`, `mailtos://`(보안) |
**`windows://` 상세** [F#56]:
- 필요 의존성: `pip install pywin32`
- 파라미터: `duration` — "Optionally set the duration of the popup message in seconds. By default this value is set to `12`". 예: `windows://?duration=5`
- **제약: "this notification can not be sent from one PC to another."** — 같은 시스템에만 전송 가능
- 메시지 사양: 아이콘 지원, 텍스트 포맷, **메시지당 최대 250자**
> **채택**: 알림 채널을 `config/settings.yaml` 의 URL 문자열 리스트로 선언하고 Apprise 로 일괄 전송한다. 100+ 서비스를 코드 수정 없이 갈아끼울 수 있다.
> ```yaml
> notify:
> urls:
> - "windows://?duration=8"
> # - "tgram://<bottoken>/<ChatID>"
> # - "mailto://user:pass@company.co.kr"
> max_body_chars: 250 # windows:// 제약에 맞춤
> ```
> **단, 리치 토스트(버튼/이미지/클릭 시 xlsx 열기)는 Apprise 로 불가능**하므로 `notify/toast.py` 에서 `win11toast` 를 직접 호출하는 이중 경로를 유지한다.
### 10.8 Windows 워치독 최종 설계 (종합)
```
[Task Scheduler]
├─ DMF_Crawler_Daily 매일 06:00, 최고 권한
│ → run_daily.cmd → python -m dmf_crawler daily
│ 성공: state.json 갱신 + notify_queue.json 에 요약 push
│ 실패: logs/errors.log 기록 + notify_queue.json 에 실패 push
├─ DMF_Crawler_Notify 15분마다, 사용자 컨텍스트(로그온 시)
│ → python -m dmf_crawler notify-drain
│ notify_queue.json 을 비우며 win11toast 로 표시
└─ DMF_Crawler_Catchup 로그온 시 1회
→ python -m dmf_crawler daily --catchup
state.json 의 마지막 성공일이 오늘 이전이면 1회만 따라잡기
[WinSW 서비스: DMFCrawlerWatchdog] (선택, 상시 감시가 필요할 때)
→ python -m dmf_crawler watchdog
· Task Scheduler 태스크 3개의 존재/활성 상태를 주기 확인 (win32com/win32service)
· 마지막 성공 실행이 26시간을 넘으면 notify_queue.json 에 경보 push
· 자기 자신이 죽으면 WinSW <onfailure action="restart"> 가 복구
```
**왜 서비스와 스케줄드 태스크를 둘 다 쓰는가**: 스케줄드 태스크는 "정시에 한 번 도는 일"에 최적이고, 서비스는 "죽었는지 지켜보는 일"에 최적이다. autorunsalerts 가 검증한 SYSTEM/User 분리에, WinSW 의 `onfailure` 자동 복구를 얹은 형태다.
---
## 11. (h) awesome 리스트 및 기타 참고
### 11.1 `lorien/awesome-web-scraping` [F#13]
| 항목 | 값 |
|---|---|
| URL | https://github.com/lorien/awesome-web-scraping |
| 설명 | "List of libraries, tools and APIs for web scraping and data processing." |
| ★ / Fork | **8.1k / 934** |
| 커밋 | master 640 commits |
구조:
```
python.md Python 패키지
javascript.md JavaScript 패키지
php.md PHP 패키지
ruby.md Ruby 패키지
golang.md Go 패키지
cli.md 커맨드라인 도구
manuals.md 교육 자료·서적
```
README 링크: https://github.com/lorien/awesome-web-scraping/blob/master/README.md
**직접 확인한 사실**: 이 저장소에는 **변경 감지·스케줄링·엑셀/리포팅 섹션이 없다.** 큐레이션 목록이지 모니터링 도구가 아니다. 캡차 해결 서비스, 프록시 마켓플레이스 참조, Telegram 커뮤니티 링크, `CONTRIBUTING.md` 를 포함.
> **활용 범위**: `python.md` 를 라이브러리 선정 시 참고. 그 이상은 없다.
### 11.2 awesome 파생 리스트 (⚠️ 미검증)
| 저장소 | 요점 |
|---|---|
| `noirquant/awesome-web-scraping` | lorien 포크 |
| `jjwangnlp/awesome-web-scraping` | lorien 포크 |
| `luminati-io/Awesome-Web-Scraping` | HTTP 라이브러리·브라우저 자동화·프록시 서비스 포함 |
| `spinov001-art/awesome-web-scraping-2026` | "130+ web scraping tools — Python, JavaScript, Go, Rust. Anti-detection, proxies, cloud platforms. Updated weekly. Includes free API alternatives." |
| `duyet/awesome-web-scraper` | "A collection of awesome web scaper, crawler." |
| `patrickloeber/llm-data-scrapers` | "A list of useful Open Source tools and scrapers to gather data for LLMs" |
| `realpython/list-of-python-api-wrappers` | 파이썬 API 래퍼 목록 |
**awesome-pharma-data 같은 제약 전용 awesome 리스트는 존재가 확인되지 않았다.** 대신 GitHub Topics 페이지가 그 역할을 한다:
| Topic | URL |
|---|---|
| `pharmaceutical-data` | https://github.com/topics/pharmaceutical-data (7개 공개 저장소 — [F#19] 에서 전량 열거) |
| `pharmaceuticals` | https://github.com/topics/pharmaceuticals?l=python |
| `pharma` | https://github.com/topics/pharma?o=desc&s=updated |
| `fda` | https://github.com/topics/fda |
| `open-fda` | https://github.com/topics/open-fda |
| `openfda` | https://github.com/topics/openfda?l=r&o=desc&s=updated |
| `openpyxl` | https://github.com/topics/openpyxl?o=asc&s=forks |
| `openpyxl-python` | https://github.com/topics/openpyxl-python |
| `excelwriter` | https://github.com/topics/excelwriter?l=python |
| `python-excel` | https://github.com/topics/python-excel |
| `xlsxwriter` | https://github.com/topics/xlsxwriter?l=python |
| `scheduled-tasks` | https://github.com/topics/scheduled-tasks?l=python&o=desc&s=updated |
| `task-scheduler` | https://github.com/topics/task-scheduler?l=powershell |
| `playwright-python` | https://github.com/topics/playwright-python?o=asc&s=updated |
| `playwright` | https://github.com/topics/playwright?l=python |
| `python-scraper` | https://github.com/topics/python-scraper |
| `huginn` | https://github.com/topics/huginn?o=asc&s=stars |
| `llm-pipeline` | https://github.com/topics/llm-pipeline |
> **활용**: `pharmaceutical-data` 토픽에는 저장소가 **7개뿐**이다(2026-09 기준). 이 분야에 오픈소스가 거의 없다는 사실 자체가, 우리가 직접 만들어야 한다는 결론을 강화한다.
### 11.3 벤더/블로그 자료 (코드 없음, 배경 지식)
| 자료 | URL | 요점 |
|---|---|---|
| PageCrawl.io: 오픈소스 변경감지 도구 비교 | https://pagecrawl.io/blog/open-source-website-change-detection-tools | 도구를 **목적형 모니터(changedetection.io, urlwatch)** 와 **범용 자동화 플랫폼(Huginn, n8n)** 으로 이분. 선택 기준: JavaScript 렌더링, 노이즈 필터링, 알림 폭, 실패 가시성, 유지보수 부담 |
| GIGAZINE: changedetection.io 리뷰 | https://gigazine.net/gsc_news/en/20260517-changedetection-io | 셀프호스트 모니터링 도구 리뷰 |
| alternativeto: urlwatch / changedetection.io 대안 | https://alternativeto.net/software/urlwatch , https://alternativeto.net/software/changedetection-io/ | Huginn 이 changedetection.io 의 최선 대안으로 언급 |
| Simon Willison: Git scraping | https://simonwillison.net/2020/Oct/9/git-scraping/ | "track changes over time by scraping to a Git repository". `git-history` 도구 |
| Oxylabs / Flipnode / JC Chouinard / Biztory / Oreate AI | (부록 A 참조) | Python 스크래퍼 + Windows Task Scheduler 자동화 튜토리얼 |
| PDQ: BurntToast | https://www.pdq.com/blog/display-toast-notifications-with-powershell-burnt-toast-module/ | "perfect for script completion alerts, Pomodoro timers, or nudging users to reboot" |
| Infonautics / usro.net / ehmiiz.se | (부록 A 참조) | WinSW 로 프로그램을 서비스화하는 단계별 가이드, PowerShell 스크립트 서비스화 |
| Codex Knowledge Base (danielvaughan.com) 4편 | (부록 A 참조) | `codex exec` 헤드리스·배치·CI·스케줄드 에이전트 |
| SmartScope / MindStudio / wmedia.es / hidekazu-konishi / StackNotice / Usagebar / DevShelfHub / HeyClaude / LikeOne / BuildThisNow | (부록 A 참조) | Claude Code 헤드리스·cron 가이드 다수 |
| Level Up Coding: Claude Code Routines | https://levelup.gitconnected.com/claude-code-routines-the-cron-replacement-i-didnt-know-i-needed-6f53cf476577 | Routines 를 cron 대체로 |
| MCP Market: Windows Task Scheduler Skill | https://mcpmarket.com/tools/skills/windows-task-scheduler | Claude Code 로 Windows Task Scheduler 잡을 만들고 관리하는 스킬 |
| Apify 스크래퍼들 | https://apify.com/labrat011/fda-orange-book-scraper/api , https://apify.com/fortuitous_pirate/openfda-scraper/api/python , https://apify.com/benthepythondev/openfda-drug-intelligence/api/python | FDA Orange Book / openFDA 상용 스크래퍼. Drugs@FDA 28,000+ 신청(NDA/ANDA/BLA) |
| John Snow Labs / PharmaCompass / pharmaexcipients / fdapals / dmf-list.backgroundscheck.info | (부록 A 참조) | FDA DMF 디렉터리 상용 데이터 |
| 한국어 크롤링 블로그 | velog `naverPillCrawling`, samslow.github.io 식품안전나라 크롤링 가이드, velog 공공데이터 포털API사용하기 | Selenium/BeautifulSoup 기반 국내 의약품 크롤링 사례 |
### 11.4 라이선스 주의 표
코드를 참고할 때 반드시 확인해야 할 항목이다.
| 저장소 | 라이선스 | 코드 복사 가능? |
|---|---|---|
| `anton-semerenko/pharma-radar` | MIT | 가능(출처 표기) |
| `mrueda/nomenclator-delta` | MIT | 가능(출처 표기) |
| `logiover/fda-data-scraper` | MIT | 가능 |
| `huginn/huginn` | MIT | 가능 |
| `GitHub30/win11toast` | MIT | 가능 |
| `Windos/BurntToast` | MIT | 가능 |
| `HaroldMills/Python-Windows-Service-Example` | MIT | 가능 |
| `786raees/task-scheduler-python` | MIT | 가능 |
| `larsyencken/csvdiff` | BSD-3-Clause | 가능(고지 유지) |
| **`suriyadeepan/WebScraping-for-Healthcare`** | **GPL-3.0** | **불가 — 설계만 참고** |
| 나머지 | 미확인 | ⚠️ 복사 전 확인 필수 |
---
## 12. 채택 결정 표 (채택 / 부분채택 / 기각)
### 12.1 저장소·도구별 결정
| 후보 | 결정 | 이유 (1~2문장) |
|---|---|---|
| `mrueda/nomenclator-delta` | **채택 (구조 정본)** | collection/normalization/diffing/validation 4단 분리와 `data/`(스냅샷+이력) 레이아웃이 우리 문제와 1:1 대응한다. CLI 서브커맨드 형태(`python -m pkg validate data`)까지 그대로 가져온다. |
| `anton-semerenko/pharma-radar` | **채택 (에이전트 계층 정본)** | `prompts/` + `config/sources.yaml` + 의존성 최소 `deliver` 계층, 그리고 "≥2 독립 출처 또는 1 공식 1차 출처" 검증 정책. 매일 06:00 무인 실행이라는 동일한 운영 형태의 존재 증명. |
| `larsyencken/csvdiff` | **부분채택 (스키마만)** | `_index`/`added`/`removed`/`changed` JSON 구조와 `--ignore-columns`/`--significance` 옵션은 우리 diff 요구에 정확히 맞다. 단 2021-02-18 아카이브라 **라이브러리 의존은 금지**하고 자체 구현한다. |
| `huginn/huginn` | **부분채택 (개념만)** | `mode: all/on_change/merge` 3분류를 diff 출력 모드로 채택. Ruby/Rails 스택 전체는 Windows 단일 PC 에 과중하므로 기각. |
| `dgtlmoon/changedetection.io` | **부분채택 (개념만)** | 알림 템플릿 토큰(`{{diff_added}}` 등), 필터 체인, 타임존 스케줄, 본문 길이 제한 규칙을 차용. 도구 자체는 텍스트 블록 diff 라서 레코드 키 기반 판정을 못 한다. |
| `thp/urlwatch` | **부분채택 (설정 스키마만)** | `urls.yaml` 잡 스키마와 `job_defaults` 상속, `ignore_connection_errors`, `--test-diff-filter` 개념을 `config/sources.yaml` 에 이식. **릴리스가 하나도 없는 저장소라 의존성 채택은 기각.** |
| `Mzands2622/Zanalytix` | **부분채택 (패턴만)** | 소스별 파서 함수 시그니처 통일, 변경 우선순위 1~5 점수, 스냅샷/변경 테이블 분리를 채택. Azure Functions·Zyte·Twilio·로그인 기능은 전부 기각. |
| `FDA/openfda` | **부분채택 (레이아웃만)** | `config/`·`schemas/`·`scripts/`·`<pkg>/` 4분할을 채택. Luigi/Elasticsearch/Docker 는 하루 1회 소량 데이터에 과잉이므로 기각. |
| `winsw/winsw` | **채택 (서비스화 1순위)** | `<onfailure action="restart">` 다단 지연 + `<resetfailure>` + `<startmode>Automatic</startmode>` + `<delayedAutoStart>` 로 "재부팅 후 자동 복구"를 XML 한 장으로 만족한다. ★14.3k, 안정판 v2.12.0. |
| `kirillkovalenko/nssm` / nssm.cc | **부분채택 (폴백)** | `AppThrottle`/`AppRestartDelay`/`AppExit Restart` 로 동등한 복구를 제공하지만 버전이 2014년(v2.24)이라 2순위. WinSW 가 실패할 때만 전환. |
| pywin32 서비스 (`HaroldMills/...`, gist `drmalex07`) | **기각** | issue #1563 — `SvcRun()` 예외/`sys.exit()` 시 pywin32 가 `SERVICE_STOPPED` 를 보고해 **Windows 복구 액션이 트리거되지 않는다.** 우회책이 `SIGABRT` 수준이면 운영에 못 쓴다. PyInstaller 도 Python 3.5 까지라는 낡은 제약. |
| `michalzobec/autorunsalerts` | **채택 (운영 패턴 정본)** | SYSTEM 스캔 태스크 + 사용자 컨텍스트 토스트 태스크 분리, `state.json`/`audit.log`/`configuration.json`/`install.ps1` 파일 구성을 그대로 매핑한다. |
| `Windos/BurntToast` | **부분채택** | 사용자 컨텍스트 알림용 대안 경로로 유지. "SYSTEM/서비스 계정에서 제약" 이라는 문서가 2단 태스크 분리의 근거가 되었다는 점이 더 큰 기여. |
| `GitHub30/win11toast` | **채택 (토스트 1순위)** | ★333 으로 파이썬 토스트 중 최다. `on_click='<xlsx 경로>'` 로 리포트 바로 열기가 가능하고 buttons/image/duration 을 모두 지원한다. |
| `ysfchn/toasted` | **부분채택** | `Progress()` 요소로 장시간 백필의 진행률 토스트를 띄울 때만 사용. |
| `DatGuy1/Windows-Toasts` | **기각** | win11toast 대비 기능이 좁고(duration short/long), 별도 채택 이유가 없다. |
| `caronc/apprise` | **채택 (알림 추상화)** | 알림 채널을 URL 문자열로 선언해 Telegram/Slack/Email 로 코드 수정 없이 확장. `windows://` 는 250자 제한이 있으므로 리치 토스트는 win11toast 이중 경로로 보완. |
| XlsxWriter | **채택 (리포트 엔진)** | `write_url('internal:Sheet2!A1')` 이 "탭 간 연동" 요구를 만족하는 유일한 정공법이고, `add_table`/`conditional_format`/`set_column` 이 같은 API 안에 있다. |
| openpyxl (`Bwhiz/Auto-Excel-Reports`) | **기각 (엔진으로서)** | 생성/배포 파일 분리 아이디어만 채택. 서식·내부링크 품질 요구가 우리 쪽이 높아 XlsxWriter 가 낫다. |
| `jshchnz/claude-code-scheduler` | **채택 (스케줄 설정 스키마)** | `schedules.json` 필드 구성(`trigger.expression`, `execution.timeout`, `settings.logRetentionDays`)과 OS별 스케줄러 어댑터 분리, 태스크 ID별 로그 파일을 채택. TypeScript 구현체 자체는 미사용. |
| Claude Code Desktop 스케줄드 태스크 | **기각** | "앱이 열려 있고 컴퓨터가 깨어 있어야 발화" 라는 제약이 "재부팅 후 자동 복구" 요구와 충돌. 단 **놓친 실행 캐치업 1회 규칙**은 채택. |
| Claude Code Routines (cloud) | **기각** | 로컬 파일 접근 불가(fresh clone), 최소 간격 1시간. 로컬 xlsx 생성이 목적인 우리와 맞지 않는다. |
| `claude -p` / `gemini -p` / `codex exec` | **부분채택 (규약만)** | 세 CLI 의 공통 호출 규약(프롬프트 파일 → JSON 출력 → 파일 리다이렉트 → 종료코드 분기)을 `agy -p` 에 사상한다. **agy 실제 플래그는 실측 필요.** |
| `786raees/task-scheduler-python` | **부분채택** | `win32com.client` 로 태스크를 CRUD 하는 API 형태만 참고. 코드 복사는 안 함(★2, 2 commits). |
| `simonw/git-scraper-template` | **부분채택 (개념)** | `data/snapshots/` 를 로컬 git 리포로 두고 매일 커밋해 변경 이력을 무상으로 얻는다. GitHub Actions 부분은 기각(로컬 실행 요구). |
| `HasData/playwright-scraping` | **부분채택 (체크리스트)** | 디렉터리 목록을 fetch 모듈 요구사항 체크리스트로 사용. Playwright 도입 자체는 게시판이 JS 렌더링일 때만 조건부. |
| `ecprice/newsdiffs` | **부분채택** | per-run 로그와 누적 에러 로그 분리, `BaseParser` 상속 구조를 채택. Django 스택은 기각. |
| `WooilJeong/PublicDataReader` | **기각 (의존성으로)** | 식약처/의약품 커버리지가 없다고 문서에 명시. provider 별 모듈 레이아웃만 참고. |
| `Q00/data.go.kr-crawling` | **부분채택** | `config.py.example` 분리, 필드 사전 자동 생성, `page = int(totalCount/100)+1` 공식. **저장소 설명(gevent)과 실제 코드(동기 requests)가 다르므로 코드 신뢰 금지.** |
| `jjscan/data.go.kr-1` | **부분채택 (교훈만)** | 재시도 폴링, `totalCount == 0` 공백 판정, 대량 백필 병목(350k건 17시간) 경고를 설계에 반영. R 코드는 무관. |
| `NomaDamas/k-skill` | **부분채택 (규범만)** | API 키 분리 원칙과 "자동화가 직접 진단하지 않는다"는 안전 규범을 채택. |
| `Tanguy9862/AI-Powered-FDA-Drug-Scraper` | **부분채택** | scraper/classification/utils 3분리와 업체명 정규화 필요성. LangChain/GPT 의존은 agy 로 대체. |
| `suriyadeepan/WebScraping-for-Healthcare` | **부분채택 (설계만)** | 소스별 동일 인터페이스 규약만. **GPL-3.0 이므로 코드 복사 금지.** |
| `jbremz/FDA-Analysis` | **부분채택** | 스파이더/원시CSV/분석/노트북 4분할. 대상 사이트 은퇴로 프로젝트가 죽은 것은 파서 격리의 필요성 경고. |
| `coderxio/OpenFDA` | **부분채택** | `load_data`/`serve_data` 엔트리포인트 분리 → `dmf backfill` / `dmf daily`. |
| `logiover/fda-data-scraper` | **부분채택** | 출력 포맷을 최종 어댑터에서 스위칭하는 설계. Apify 플랫폼 의존은 기각. |
| `lorien/awesome-web-scraping` | **부분채택 (참고용)** | `python.md` 를 라이브러리 선정 참고로만. 변경감지/스케줄/엑셀 섹션이 없음을 확인했다. |
| `testing-in-production/gemini-jobs` | **기각** | HTTP 404. 존재하지 않는다. |
### 12.2 기술 스택 최종 결정
| 계층 | 채택 | 대안(폴백) | 기각한 것 |
|---|---|---|---|
| 언어/런타임 | Python 3.11+ (venv) | — | — |
| HTTP | `requests` | `httpx` | Scrapy(단일 API 호출에 과잉) |
| HTML 파싱 | `beautifulsoup4` + `lxml` | Playwright(JS 렌더링 시) | Selenium |
| 데이터 프레임 | `pandas` | — | — |
| 저장 | SQLite (`sqlite3` 표준 라이브러리) + JSON 스냅샷 파일 | — | PostgreSQL, Elasticsearch |
| diff | 자체 구현 (csvdiff 스키마) | — | csvdiff 라이브러리(아카이브), changedetection.io |
| 리포트 | `XlsxWriter` | — | openpyxl, Google Sheets API |
| 알림 | `apprise` + `win11toast` | BurntToast(PowerShell) | win10toast, Windows-Toasts |
| 스케줄 | Windows Task Scheduler (`schtasks` / `win32com.client`) | — | cron, GitHub Actions, Claude Desktop tasks |
| 서비스 | WinSW v2.12.0 | NSSM 2.24 | pywin32 ServiceFramework |
| AI CLI | **Google Antigravity CLI `agy -p`** | (미사용 시 파이프라인은 정상 동작) | claude/gemini/codex (규약만 차용) |
| 설정 | YAML (`config/*.yaml`) + `.env` | — | 하드코딩, 레지스트리 |
| 버전관리 | 로컬 git (`data/snapshots` 포함) | — | 원격 push |
---
## 13. 최종 디렉터리 구조 제안
각 줄 끝의 `` 는 **어느 저장소의 어느 레이아웃을 근거로 했는지**를 표시한다.
```text
D:\workspace\DMF_Crawler\
├─ README.md 프로젝트 개요·빠른 시작
├─ pyproject.toml 패키지 메타/의존성 ← FDA/openfda(setup.py), Tanguy9862(setup.py)
├─ requirements.txt 고정 의존성 (운영 재현용) ← Q00/data.go.kr-crawling, larsekje/PythonWindowsServices
├─ .env.example API 키 템플릿(실제 .env 는 .gitignore) ← Q00 의 config.py.example, NomaDamas/k-skill 의 키 분리 원칙
├─ .gitignore .env, data/raw, logs, reports 제외
├─ config\ ★ 설정은 전부 데이터. 코드에 상수 금지 ← FDA/openfda 의 config/, pharma-radar 의 config/
│ ├─ sources.yaml 소스 정의(엔드포인트·파라미터·필터·job_defaults) ← pharma-radar sources.yaml + urlwatch urls.yaml 스키마
│ ├─ schedule.yaml 태스크 정의(cron, timezone, timeout, logRetentionDays) ← claude-code-scheduler schedules.json
│ ├─ settings.yaml 전역 설정(알림 URL, 재시도, 임계값, 우선순위 규칙) ← autorunsalerts configuration.json
│ └─ field_map.yaml API 영문 필드 ↔ 리포트 한글 헤더 매핑 ← Q00 의 column.py 자동 생성 발상
├─ schemas\ ★ 데이터 계약을 코드와 분리해 버전 관리 ← FDA/openfda 의 schemas/
│ ├─ dmf_record.schema.json 정규화된 DMF 레코드 스키마
│ ├─ diff_result.schema.json diff 산출물 스키마(_index/added/removed/changed) ← csvdiff JSON 구조
│ └─ agent_output.schema.json agy --output-format json 에 넘길 JSON Schema ← drew.tech 의 --json-schema 패턴
├─ prompts\ ★ 에이전트 프롬프트는 자산이지 코드가 아니다 ← pharma-radar 의 prompts/system_prompt.md
│ ├─ system.md agy 시스템 프롬프트(YAML frontmatter + 본문) ← Claude Desktop 의 SKILL.md 형식
│ ├─ summarize_changes.md 변경 건 요약 프롬프트
│ └─ classify_priority.md 변경 우선순위(1~5) 분류 프롬프트 ← Zanalytix 의 priority 1-5
├─ src\
│ └─ dmf_crawler\ ★ python -m dmf_crawler <subcommand> ← nomenclator-delta 의 python3 -m nomenclator_delta
│ ├─ __init__.py
│ ├─ __main__.py CLI 진입점(argparse 서브커맨드)
│ ├─ cli.py daily / backfill / diff / report / notify-drain / validate / watchdog / doctor
│ ├─ settings.py config/*.yaml + .env 로딩, 경로 상수
│ │
│ ├─ fetch\ ★ 수집 (결정론) ← nomenclator-delta 의 collection
│ │ ├─ __init__.py
│ │ ├─ base.py BaseFetcher — 재시도·타임아웃·UA·디버그 덤프 ← newsdiffs BaseParser, playwright-scraping errors//browser//debug/
│ │ ├─ dmf_api.py data.go.kr getMdcDmfList01 페이지네이션 수집
│ │ ├─ nedrug_board.py nedrug.mfds.go.kr/bbs/117 공고 게시판 수집
│ │ └─ registry.py source_id → Fetcher 매핑 ← Zanalytix 의 fetch_{company}_html() 규약
│ │
│ ├─ parse\ ★ 정규화 ← nomenclator-delta 의 normalization
│ │ ├─ __init__.py
│ │ ├─ dmf_api.py JSON/XML → DmfRecord
│ │ ├─ nedrug_board.py HTML → BoardPost (css/xpath 규칙은 sources.yaml 에서) ← huginn WebsiteAgent 의 extract 선언
│ │ └─ normalize.py 업체명·주소·성분명 표기 정규화 ← Tanguy9862 의 회사명 1000→700 정규화
│ │
│ ├─ diff\ ★ 차이 판정 ← nomenclator-delta 의 diffing
│ │ ├─ __init__.py
│ │ ├─ engine.py 키 기반 added/removed/changed 산출 ← csvdiff diff_records()
│ │ ├─ modes.py all / on_change / merge ← huginn WebsiteAgent mode
│ │ └─ priority.py 변경 유형별 우선순위 1~5 부여 ← Zanalytix
│ │
│ ├─ store\ ★ 저장
│ │ ├─ __init__.py
│ │ ├─ db.py SQLite: snapshots / changes / runs / agent_runs ← Zanalytix db.py, Revised_MasterTable+Stream 분리
│ │ ├─ snapshots.py data/snapshots/ 읽기·쓰기·해시
│ │ └─ gitlog.py 스냅샷 디렉터리 자동 커밋 ← simonw/git-scraper-template
│ │
│ ├─ report\ ★ 리포트 ← Bwhiz report_script.py
│ │ ├─ __init__.py
│ │ ├─ xlsx.py XlsxWriter 멀티시트 + internal 링크 + add_table + conditional_format
│ │ ├─ sheets.py 시트별 빌더(요약/신규/변경/취하/전체현황/실행로그)
│ │ └─ styles.py 서식 상수(폰트·색·너비)
│ │
│ ├─ notify\ ★ 알림 ← Bwhiz auto_mail.py, pharma-radar src/deliver.py
│ │ ├─ __init__.py
│ │ ├─ queue.py state/notify_queue.json push/drain ← autorunsalerts 의 플래그 파일 패턴
│ │ ├─ toast.py win11toast (os.chdir 처리 포함)
│ │ └─ apprise_sink.py Apprise URL 리스트 전송, 250자 트리밍
│ │
│ ├─ orchestrate\ ★ 오케스트레이션
│ │ ├─ __init__.py
│ │ ├─ pipeline.py fetch→parse→diff→store→report→notify 순서 제어·부분 실패 허용
│ │ ├─ agy.py agy -p 서브프로세스 호출(프롬프트 파일·스키마·타임아웃·종료코드) ← drew.tech 스니펫
│ │ ├─ catchup.py 놓친 실행 1회 따라잡기 ← Claude Desktop missed-runs 규칙
│ │ └─ watchdog.py 태스크 존재·최근 성공 시각 감시 ← autorunsalerts + Badgerati/Hook
│ │
│ └─ util\
│ ├─ logging.py per-run 로그 + 누적 에러 로그 분리 ← newsdiffs
│ ├─ retry.py 지수 백오프 ← jjscan/data.go.kr-1 의 폴링 재시도
│ └─ hashing.py 스냅샷 해시
├─ ops\ ★ 운영 자산 ← FDA/openfda 의 scripts/, larsekje 의 windows_service/
│ ├─ winsw\
│ │ ├─ WinSW.exe (v2.12.0 배포본, .gitignore 대상)
│ │ ├─ dmf-watchdog.xml 서비스 정의 ← winsw samples/minimal.xml
│ │ └─ install.ps1 winsw install / start
│ ├─ tasks\
│ │ ├─ install.ps1 schtasks 3종 등록 ← autorunsalerts install.ps1
│ │ ├─ uninstall.ps1 제거 ← autorunsalerts uninstall.ps1
│ │ ├─ run_daily.cmd python -m dmf_crawler daily 래퍼 ← claude-code-scheduler 의 wrapper 생성 패턴
│ │ ├─ run_notify.cmd python -m dmf_crawler notify-drain
│ │ └─ run_catchup.cmd python -m dmf_crawler daily --catchup
│ ├─ agent\
│ │ ├─ bootstrap_agy.ps1 agy 미설치 시 설치·인증·프롬프트 창 표시
│ │ └─ run_agy.cmd agy -p "$(cat prompt)" --output-format json > out.json ← drew.tech
│ └─ doctor.ps1 환경 진단(파이썬, agy, WinSW, 태스크, 권한)
├─ data\ ★ .gitignore 대상 중 snapshots 만 로컬 git 추적 ← nomenclator-delta 의 data/
│ ├─ raw\ 원시 응답 원본 보존 (YYYY-MM-DD\<source>.json|html) ← jbremz masterDrugList2.csv, coderxio ./data/
│ ├─ snapshots\ 정규화된 시점 스냅샷 (YYYY-MM-DD.json) ← nomenclator-delta, Zanalytix 날짜별 JSON
│ ├─ history\ 일자별 diff 결과 (YYYY-MM-DD.diff.json)
│ ├─ debug\ 실패 시 HTML/스크린샷/trace ← playwright-scraping debug/
│ └─ dmf.sqlite3 운영 DB
├─ state\
│ ├─ state.json 마지막 성공 실행 시각·스냅샷 해시 ← autorunsalerts state.json
│ └─ notify_queue.json 대기 중 알림(사용자 컨텍스트 태스크가 소비)
├─ reports\ ★ 산출물 xlsx (YYYY-MM-DD_DMF_리포트.xlsx) ← nomenclator-delta 의 site/ 자리
├─ logs\ ← newsdiffs 로그 분리, claude-code-scheduler 의 task-id 별 로그
│ ├─ runs\ <task-id>-YYYYMMDD.log (per-run)
│ ├─ errors.log 누적 에러 (워치독 감시 대상)
│ └─ audit.log 누적 변경 이력 ← autorunsalerts audit.log
├─ tests\ ← nomenclator-delta tests/, claude-code-scheduler src/__tests__/
│ ├─ unit\
│ ├─ integration\
│ └─ fixtures\ 고정 응답 샘플(회귀 테스트용)
├─ notebooks\ 탐색·검증용 ← jbremz 의 .ipynb, suriyadeepan 의 notebooks/
├─ scripts\ 1회성 유틸 ← FDA/openfda scripts/
│ ├─ gen_field_map.py 공공데이터포털 명세에서 field_map.yaml 생성 ← Q00 의 url.py → column.py
│ └─ inspect_board.py nedrug 게시판 구조 실측
└─ docs\
├─ research\ ★ 본 문서 등 리서치 SSOT
│ ├─ 01-*.md
│ └─ 02-benchmark-github-projects.md
├─ ops\
│ ├─ runbook.md 일일 운영 절차·장애 대응 ← nomenclator-delta 의 monthly update runbook
│ └─ install.md 최초 설치 순서
└─ decisions\ ADR (Architecture Decision Record)
```
### 13.1 이 구조가 만족하는 요구사항 대조표
| 요구사항 | 이 구조에서 어디가 담당하는가 |
|---|---|
| 매일 06:00 크롤링 | `ops/tasks/install.ps1` → `DMF_Crawler_Daily` → `run_daily.cmd` → `orchestrate/pipeline.py` |
| 신규·변경·취하 탐지 | `diff/engine.py` + `diff/modes.py`, 결과는 `data/history/*.diff.json` |
| 탭별 연동 xlsx | `report/xlsx.py` + `report/sheets.py` → `reports/YYYY-MM-DD_DMF_리포트.xlsx` |
| AI CLI headless | `orchestrate/agy.py` + `prompts/*.md` + `schemas/agent_output.schema.json` |
| agy 자동 부트스트랩 | `ops/agent/bootstrap_agy.ps1` (미설치 시 설치, 필요 시 프롬프트 창) |
| 재부팅 후 자동 복구 | WinSW `<startmode>Automatic</startmode>` + `<delayedAutoStart>` + Task Scheduler 등록 지속 |
| 서비스 사망 시 알림 | `orchestrate/watchdog.py` → `state/notify_queue.json` → `DMF_Crawler_Notify`(사용자 컨텍스트) → `notify/toast.py` |
| 놓친 실행 복구 | `DMF_Crawler_Catchup`(로그온 시) → `orchestrate/catchup.py` |
| 재현 가능성 | `data/raw` 원본 보존 + `data/snapshots` git 커밋 + `tests/fixtures` |
---
## 14. 모듈 경계 제안과 입출력 계약
### 14.0 설계 원칙 (근거 저장소 명시)
1. **결정론과 비결정론을 섞지 않는다** — `fetch`/`parse`/`diff` 는 순수 결정론. LLM 호출은 `orchestrate/agy.py` 한 곳에만. ← `Tanguy9862` 의 scraper/classification 분리
2. **소스별 어댑터는 동일 시그니처를 강제한다** ← `Zanalytix` 의 `fetch_{company}_html()`/`process_{company}_html()`, `suriyadeepan` 의 `fetch()`/`crawl_k()`
3. **각 단계는 파일에 산출물을 남긴다** — 중간 산출물이 없으면 재현도, 부분 재실행도 불가능 ← `nomenclator-delta` 의 `data/`
4. **AI 는 선택적 보강이다** — agy 가 실패해도 xlsx 는 나와야 한다 ← `pharma-radar` 의 표준 라이브러리 배포 계층
5. **부분 실패를 허용한다** — 소스 하나가 죽어도 나머지는 진행 ← urlwatch 의 `ignore_connection_errors`
### 14.1 데이터 타입 정의 (공통 계약)
```python
# src/dmf_crawler/types.py
from dataclasses import dataclass, field
from datetime import date, datetime
from typing import Any, Literal
SourceId = Literal["dmf_api", "nedrug_board"]
ChangeKind = Literal["added", "removed", "changed"]
@dataclass(frozen=True)
class RawPayload:
"""fetch 계층의 유일한 산출물. 파싱하지 않은 원본."""
source_id: SourceId
fetched_at: datetime
url: str
status_code: int
content_type: str # "application/json" | "text/html" | "application/xml"
body: bytes
meta: dict[str, Any] = field(default_factory=dict) # pageNo, totalCount 등
raw_path: str | None = None # data/raw/YYYY-MM-DD/<source>_<page>.json 로 저장된 경로
@dataclass(frozen=True)
class DmfRecord:
"""parse 계층의 산출물. 정규화된 DMF 1건."""
dmf_permit_no: str # DMF_PERMIT_NO ← 진짜 유일 키
ingr_kor_name: str # INGR_KOR_NAME 성분명
entp_name: str # ENTP_NAME 업체명 (정규화 적용됨)
mnfctr_name: str # MNFCTR_NAME 제조소명
mnfctr_place: str # MNFCTR_PLACE 제조소 소재지
manuf_country: str # MANUF_COUNTRY_CODE_NM 제조국가명
dmf_permit_date: date # DMF_PERMIT_DATE 발급일자
source_id: SourceId
raw: dict[str, Any] = field(default_factory=dict) # 원본 필드 전량 보존
@dataclass(frozen=True)
class BoardPost:
"""공고 게시판 1건."""
seq: int # 연번
title: str # 제목
view_count: int # 조회건수
registrant: str # 등록자
registered_on: date # 등록일자
detail_url: str
attachments: list[str] = field(default_factory=list)
@dataclass(frozen=True)
class FieldChange:
field_name: str
before: Any
after: Any
@dataclass(frozen=True)
class ChangeEvent:
kind: ChangeKind
key: str # dmf_permit_no
priority: int # 1~5 ← Zanalytix
record_after: DmfRecord | None
record_before: DmfRecord | None
fields: list[FieldChange] = field(default_factory=list)
@dataclass(frozen=True)
class DiffResult:
"""csvdiff 호환 구조."""
index: list[str] # == ["dmf_permit_no"] ← csvdiff _index
base_date: date # 이전 스냅샷 날짜
head_date: date # 이번 스냅샷 날짜
added: list[DmfRecord]
removed: list[DmfRecord]
changed: list[ChangeEvent]
ignored_columns: list[str] = field(default_factory=list)
@dataclass
class RunContext:
"""파이프라인 전체를 관통하는 실행 컨텍스트."""
run_id: str # "20260902-060000"
run_date: date
started_at: datetime
dry_run: bool = False
force: bool = False
catchup: bool = False
errors: list[str] = field(default_factory=list) # 부분 실패 누적
```
### 14.2 모듈별 입출력 계약
| 모듈 | 입력 | 출력 | 부수효과 | 실패 시 |
|---|---|---|---|---|
| **fetch** | `source_id`, `config/sources.yaml`, `RunContext` | `list[RawPayload]` | `data/raw/YYYY-MM-DD/` 에 원본 저장, 실패 시 `data/debug/` 에 덤프 | `ignore_connection_errors: true` 면 빈 리스트 + `ctx.errors` 에 기록하고 계속. false 면 예외 |
| **parse** | `list[RawPayload]` | `list[DmfRecord]` 또는 `list[BoardPost]` | 없음 (순수 함수) | 레코드 단위 실패는 스킵 + 카운트. 전체 실패율 20% 초과 시 예외 |
| **diff** | `list[DmfRecord]`(head), `list[DmfRecord]`(base), `ignore_columns` | `DiffResult` | 없음 (순수 함수) | base 스냅샷이 없으면 `DiffResult(added=전량, ...)` 가 아니라 **"기준선 수립" 모드**로 종료 |
| **store** | `list[DmfRecord]`, `DiffResult`, `RunContext` | `snapshot_path`, `diff_path` | `data/snapshots/*.json`, `data/history/*.diff.json`, SQLite 테이블, git commit | DB 트랜잭션 롤백 후 예외 전파(이건 치명적) |
| **report** | `DiffResult`, `list[DmfRecord]`(전체현황), `RunContext` | `report_path` (xlsx 절대경로) | `reports/*.xlsx` 생성 | 예외 전파. 단 notify 는 "리포트 생성 실패" 알림으로 계속 |
| **notify** | `DiffResult` 요약, `report_path`, `ctx.errors` | `None` | `state/notify_queue.json` 에 push, Apprise 전송 | 절대 예외를 전파하지 않는다(알림 실패로 파이프라인을 죽이지 않음). `logs/errors.log` 에만 기록 |
| **orchestrate** | CLI 인자, 설정 | 프로세스 종료 코드 | `state/state.json` 갱신, `logs/runs/*.log` | 종료 코드 0(성공) / 1(부분 실패) / 2(치명적 실패) |
### 14.3 함수 시그니처 (프로토콜)
```python
# src/dmf_crawler/fetch/base.py
from typing import Protocol, Iterable
class Fetcher(Protocol):
source_id: str
def fetch(self, ctx: RunContext) -> Iterable[RawPayload]:
"""페이지네이션을 내부에서 처리하고 RawPayload 를 순차 yield 한다.
재시도·타임아웃·User-Agent 는 BaseFetcher 가 담당한다."""
...
# src/dmf_crawler/parse/base.py
class Parser(Protocol):
source_id: str
def parse(self, payloads: Iterable[RawPayload]) -> list[DmfRecord]:
...
# src/dmf_crawler/diff/engine.py
def compute_diff(
head: list[DmfRecord],
base: list[DmfRecord],
*,
key: str = "dmf_permit_no",
ignore_columns: list[str] | None = None,
) -> DiffResult:
"""csvdiff 와 동일한 의미론. key 로 조인하고 나머지 필드를 비교한다.
ignore_columns 에 든 필드는 changed 판정에서 제외한다."""
...
# src/dmf_crawler/diff/priority.py
def assign_priority(event: ChangeEvent) -> int:
"""1~5. 규칙은 config/settings.yaml 의 priority_rules 에서 읽는다."""
...
# src/dmf_crawler/report/xlsx.py
def build_report(
diff: DiffResult,
full_snapshot: list[DmfRecord],
ctx: RunContext,
out_path: str,
) -> str:
"""XlsxWriter 로 멀티시트 리포트를 만들고 절대경로를 반환한다."""
...
# src/dmf_crawler/notify/queue.py
def push(item: dict) -> None: ...
def drain() -> list[dict]: ...
# src/dmf_crawler/orchestrate/agy.py
def run_agy(
prompt_path: str,
schema_path: str | None = None,
*,
timeout_sec: int = 300,
cwd: str | None = None,
) -> dict | None:
"""agy -p 를 서브프로세스로 호출한다.
실패·타임아웃 시 None 을 반환하고 예외를 던지지 않는다(선택적 보강 원칙)."""
...
```
### 14.4 파이프라인 시퀀스
```
python -m dmf_crawler daily
├─ 0. settings 로드 (config/*.yaml + .env) 실패 → exit 2
├─ 1. RunContext 생성, logs/runs/<run_id>.log 열기
├─ 2. catchup 판정 (state.json 의 last_success 확인) 이미 오늘 성공 → exit 0
├─ 3. for source in sources.yaml:
│ fetch() → data/raw/ ─────────── 실패 & ignore_connection_errors → ctx.errors 추가 후 continue
│ parse() → list[DmfRecord] ────── 실패율 20% 초과 → exit 2
├─ 4. 레코드 병합 + normalize() → head_snapshot
├─ 5. store.snapshots.write(head_snapshot) → data/snapshots/YYYY-MM-DD.json
│ store.gitlog.commit()
├─ 6. base = store.snapshots.read(직전 날짜)
│ base 없음 → "기준선 수립" 토스트 후 exit 0
├─ 7. diff.compute_diff(head, base, ignore_columns=settings.ignore_columns)
│ diff.priority.assign_priority(each)
│ store.db.save_changes() → SQLite changes 테이블
│ → data/history/YYYY-MM-DD.diff.json
├─ 8. (선택) orchestrate.agy.run_agy(prompts/summarize_changes.md, schemas/agent_output.schema.json)
│ → 실패 시 None. 요약 없이 계속.
├─ 9. report.build_report() → reports/YYYY-MM-DD_DMF_리포트.xlsx 실패 → ctx.errors 추가
├─ 10. notify.queue.push({kind, counts, report_path, errors})
│ notify.apprise_sink.send() 실패해도 무시
└─ 11. state.json 갱신(last_success), 로그 닫기
exit 0 (ctx.errors 없음) / exit 1 (부분 실패)
```
### 14.5 SQLite 스키마 초안
`Zanalytix` 의 `Revised_MasterTable`(스냅샷) / `Stream`(변경+LLM 응답) 분리를 따른다.
```sql
-- 실행 이력
CREATE TABLE IF NOT EXISTS runs (
run_id TEXT PRIMARY KEY, -- '20260902-060000'
run_date TEXT NOT NULL, -- '2026-09-02'
started_at TEXT NOT NULL,
finished_at TEXT,
exit_code INTEGER,
error_count INTEGER DEFAULT 0,
report_path TEXT
);
-- 시점 스냅샷 (레코드 단위)
CREATE TABLE IF NOT EXISTS snapshots (
run_id TEXT NOT NULL,
snapshot_date TEXT NOT NULL,
dmf_permit_no TEXT NOT NULL,
ingr_kor_name TEXT,
entp_name TEXT,
mnfctr_name TEXT,
mnfctr_place TEXT,
manuf_country TEXT,
dmf_permit_date TEXT,
source_id TEXT NOT NULL,
raw_json TEXT NOT NULL, -- 원본 필드 전량
PRIMARY KEY (snapshot_date, dmf_permit_no)
);
CREATE INDEX IF NOT EXISTS idx_snapshots_key ON snapshots(dmf_permit_no);
-- 변경 이벤트
CREATE TABLE IF NOT EXISTS changes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id TEXT NOT NULL,
base_date TEXT NOT NULL,
head_date TEXT NOT NULL,
kind TEXT NOT NULL CHECK (kind IN ('added','removed','changed')),
dmf_permit_no TEXT NOT NULL,
priority INTEGER NOT NULL CHECK (priority BETWEEN 1 AND 5),
fields_json TEXT, -- [{"field_name":..,"before":..,"after":..}]
notified_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_changes_run ON changes(run_id);
CREATE INDEX IF NOT EXISTS idx_changes_key ON changes(dmf_permit_no);
-- AI 에이전트 호출 기록 (선택적 보강)
CREATE TABLE IF NOT EXISTS agent_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id TEXT NOT NULL,
prompt_path TEXT NOT NULL,
schema_path TEXT,
exit_code INTEGER,
duration_ms INTEGER,
output_json TEXT,
error_text TEXT
);
```
### 14.6 `config/sources.yaml` 초안
urlwatch 잡 스키마 + pharma-radar sources.yaml + huginn extract 선언을 합친 형태.
```yaml
# job_defaults 는 urlwatch 의 개념 (모든 소스에 상속)
job_defaults:
timeout_sec: 30
retries: 3
backoff_sec: 2
user_agent: "DMF_Crawler/1.0 (+internal use)"
ignore_connection_errors: false
encoding: utf-8
sources:
- id: dmf_api
name: "식품의약품안전처_원료의약품등록(DMF)현황"
kind: api
url: "https://apis.data.go.kr/1471000/MdcDmfInfoService01/getMdcDmfList01"
method: GET
params:
serviceKey: "${DATA_GO_KR_API_KEY}" # .env 에서 주입
type: json
numOfRows: 100
pageNo: 1
pagination:
page_param: pageNo
size_param: numOfRows
page_size: 100
total_field: totalCount # totalCount == 0 이면 '데이터 없음' ← jjscan 교훈
formula: "int(totalCount/numOfRows) + 1" # ← Q00 의 공식
key_field: DMF_PERMIT_NO
fields:
- DMF_PERMIT_NO
- INGR_KOR_NAME
- ENTP_NAME
- MNFCTR_NAME
- MNFCTR_PLACE
- MANUF_COUNTRY_CODE_NM
- DMF_PERMIT_DATE
- id: nedrug_board
name: "의약품안전나라 원료의약품등록(DMF) 정보 게시판"
kind: html
url: "https://nedrug.mfds.go.kr/bbs/117"
method: GET
ignore_connection_errors: true # 게시판 장애로 파이프라인을 죽이지 않는다
# huginn WebsiteAgent 의 extract 선언 방식
extract:
seq: { css: "table tbody tr td:nth-child(1)" }
title: { css: "table tbody tr td:nth-child(2) a", value: "text" }
detail_url: { css: "table tbody tr td:nth-child(2) a", value: "@href" }
view_count: { css: "table tbody tr td:nth-child(3)" }
registrant: { css: "table tbody tr td:nth-child(4)" }
registered_on: { css: "table tbody tr td:nth-child(5)" }
filter: # urlwatch 필터 체인 개념
- strip
pagination:
page_size_options: [10, 20, 30, 40, 50]
total_posts_hint: 710 # 2026-09 실측 [F#28]
# ⚠️ 미검증: 위 CSS 선택자는 실제 DOM 으로 반드시 재확인해야 함
```
### 14.7 `config/settings.yaml` 초안
```yaml
run:
timezone: "Asia/Seoul"
daily_at: "06:00"
timeout_sec: 1800 # ← claude-code-scheduler execution.timeout
log_retention_days: 30 # ← claude-code-scheduler settings.logRetentionDays
max_execution_history: 100
diff:
key: DMF_PERMIT_NO
ignore_columns: # ← csvdiff --ignore-columns
- fetched_at
- view_count
# 우선순위 규칙 ← Zanalytix priority 1-5
priority_rules:
removed: 5 # 등록 취하 — 가장 중요
added: 3 # 신규 등록
changed:
MNFCTR_NAME: 4 # 제조소 변경
MNFCTR_PLACE: 2 # 소재지 변경
ENTP_NAME: 4 # 업체 변경
INGR_KOR_NAME: 4 # 성분명 변경
_default: 1
notify:
urls:
- "windows://?duration=8" # ← apprise windows:// (pywin32 필요, 250자 제한)
# - "tgram://<bottoken>/<ChatID>"
# - "mailto://user:pass@company.co.kr"
max_body_chars: 250
toast:
engine: win11toast
open_report_on_click: true
min_priority_to_notify: 2 # 우선순위 1은 조용히 로그만
watchdog:
stale_after_hours: 26 # 마지막 성공이 26시간 넘으면 경보
check_interval_sec: 900
agent:
enabled: true
binary: "agy" # ⚠️ 미검증: 실제 실행파일명·플래그 실측 필요
print_flag: "-p"
output_format_flag: "--output-format json"
timeout_sec: 300
prompts:
summarize: "prompts/summarize_changes.md"
classify: "prompts/classify_priority.md"
schema: "schemas/agent_output.schema.json"
fail_open: true # 에이전트 실패 시 파이프라인 계속
```
---
## 15. 데이터 소스 실측 정보 (DMF API / 공고 게시판)
이 절은 raw dump 에서 확인된 **한국 DMF 데이터 소스의 1차 사실**을 손실 없이 보존한다. 벤치마킹 결과를 실제로 꽂을 자리이므로 본 문서에 포함한다.
### 15.1 `식품의약품안전처_원료의약품등록(DMF)현황` OpenAPI [F#29]
출처: https://www.data.go.kr/data/15057075/openapi.do
| 항목 | 값 |
|---|---|
| API 명 | 식품의약품안전처_원료의약품등록(DMF)현황 |
| 서비스 URL | `https://apis.data.go.kr/1471000/MdcDmfInfoService01` |
| **엔드포인트** | `https://apis.data.go.kr/1471000/MdcDmfInfoService01/getMdcDmfList01` |
| 형식 | XML / JSON |
| 비용 | 무료 |
| 트래픽 제한(개발) | 10,000 calls |
| 최종 수정 | 2025년 9월 19일 |
| 제공기관 | 식품의약품안전처 |
**요청 파라미터**
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
| `serviceKey` | string | Yes | 데이터 포털에서 발급받은 인증키 |
| `pageNo` | integer | No | 페이지 번호 (기본 1) |
| `numOfRows` | integer | No | 페이지당 결과 수 (**기본 3**) |
| `entp_name` | string | No | 업체/제조사명 |
| `ingr_kor_name` | string | No | 성분명(한글) |
| `type` | string | No | 응답 형식: `xml` 또는 `json` |
**응답 필드**
| 필드 | 의미 |
|---|---|
| `DMF_PERMIT_NO` | 등록번호 — **유일 키** |
| `INGR_KOR_NAME` | 성분명 |
| `ENTP_NAME` | 업체명 |
| `MNFCTR_NAME` | 제조소명 |
| `MNFCTR_PLACE` | 제조소 소재지 |
| `MANUF_COUNTRY_CODE_NM` | 제조국가명 |
| `DMF_PERMIT_DATE` | 발급일자 |
| `resultCode` | 상태 코드 |
| `resultMsg` | 상태 메시지 |
| `totalCount` | 전체 결과 수 |
**동작 가능한 호출 예 (`numOfRows` 기본값 3 에 주의 — 반드시 명시할 것)**
```python
# scripts/probe_dmf_api.py — 최초 실측용
import os
import requests
SERVICE_URL = "https://apis.data.go.kr/1471000/MdcDmfInfoService01/getMdcDmfList01"
def fetch_page(page_no: int, num_of_rows: int = 100) -> dict:
params = {
"serviceKey": os.environ["DATA_GO_KR_API_KEY"], # 디코딩된 키 사용
"type": "json",
"pageNo": page_no,
"numOfRows": num_of_rows,
}
resp = requests.get(
SERVICE_URL,
params=params,
timeout=30,
headers={"User-Agent": "DMF_Crawler/1.0 (+internal use)"},
)
resp.raise_for_status()
return resp.json()
def fetch_all(num_of_rows: int = 100) -> list[dict]:
first = fetch_page(1, num_of_rows)
body = first.get("body", first)
total = int(body.get("totalCount", 0))
if total == 0: # ← jjscan/data.go.kr-1 의 공백 판정 규칙
return []
pages = int(total / num_of_rows) + 1 # ← Q00/data.go.kr-crawling 의 공식
rows = list(body.get("items", []))
for page in range(2, pages + 1):
b = fetch_page(page, num_of_rows).get("body", {})
rows.extend(b.get("items", []))
return rows
if __name__ == "__main__":
records = fetch_all()
print(f"총 {len(records)}건")
if records:
print(records[0])
```
⚠️ **미검증 사항**: 응답 JSON 의 실제 중첩 구조(`response.body.items.item` 인지 `body.items` 인지), `serviceKey` 의 인코딩/디코딩 키 구분, `resultCode` 정상값. 최초 실행 시 위 스크립트로 실측하고 `schemas/dmf_record.schema.json` 을 확정한다.
### 15.2 의약품안전나라 DMF 공고 게시판 [F#28]
출처: https://nedrug.mfds.go.kr/bbs/117
| 항목 | 값 |
|---|---|
| 게시판 | 원료의약품등록(DMF) 정보 |
| **테이블 컬럼** | 연번 / 제목 / 조회건수 / 등록자 / 등록일자 |
| 페이지네이션 | 처음 \| 이전 \| 1-10 \| 다음 \| 마지막, **총 710건** |
| 페이지당 표시 | 10, 20, 30, 40, 50 선택 |
| 검색 폼 | "제목" 검색 필드 + 검색/초기화 버튼 |
| 게시물 제목 패턴 | **"등록대상 원료의약품(DMF) 등록 공고"** + 특정 날짜 범위, **주 단위**로 게시 |
| 표시된 샘플 범위 | 2021년 2월 ~ 2020년 11월 |
| 확인 사항 | 고급 필터나 "변경/취하" 전용 게시물 타입은 화면에 보이지 않음 |
**중요**: 게시판에는 "**변경**"/"**취하**" 를 별도 구분하는 UI 가 없다. 즉 신규/변경/취하 판정은 **게시글 제목·첨부파일 내용이 아니라 API 스냅샷 diff 로 하는 것이 정공법**이다. 게시판은 (a) 공고 게시 사실의 근거 링크, (b) API 반영 지연 시의 조기 신호 로 쓴다.
관련 확인:
- 한국보건산업진흥원(KHIDI) 제약산업정보포털에도 "등록대상 원료의약품(DMF) 등록 공고(7월 둘째주)" 같은 동일 공고가 게시된다 [S#11] — 보조 소스 후보
- `[지침]원료의약품 등록(DMF) 처리 절차` 문서 [S#11]
- 식품의약품안전평가원 KDMF 페이지: https://www.nifds.go.kr/brd/m_87/list.do [S#11]
- 원료의약품 등록 현황(신규등록, 변경등록, 연차보고)은 식약처 홈페이지 **전자민원창구**에서도 확인 가능 [S#11] — ⚠️ 미검증, 별도 소스가 될 수 있음
**실측 실패**: `https://nedrug.mfds.go.kr/searchDmf` 는 **"The requested page cannot be found. The page you are looking for has been changed or is currently unavailable."** 에러 페이지를 반환했다 [F#63]. DMF 검색 화면의 실제 경로는 브라우저로 재확인 필요.
### 15.3 관련 식약처/공공데이터 소스 (보조·참고)
| 데이터 | URL | 비고 |
|---|---|---|
| 의약품 제품 허가정보 | https://www.data.go.kr/data/15095677/openapi.do | REST, JSON+XML. 필드 언급: `ITEM_SEQ`, `ITEM_NAME`, `ENTP_NAME`, `ITEM_PERMIT_DATE`, `CANCEL_DATE`, `CANCEL_NAME`, `CHANGE_DATE`. 최종수정 2025-10-31. 무료, 개발 10,000건 [F#54] — **엔드포인트 URL·파라미터는 문서에 미명시 ⚠️ 미검증** |
| 의약품개요정보(e약은요) | https://www.data.go.kr/data/15075057/openapi.do | 일반의약품 주요 정보 [S#13] |
| 의약품 낱알식별 정보 | https://www.data.go.kr/data/15057639/openapi.do | [S#13] |
| 필수의약품내역 | https://www.data.go.kr/data/15058207/openapi.do | [S#30] |
| 식품의약품안전처 의약품 관련 정보 (파일데이터) | https://www.data.go.kr/data/15020627/fileData.do | [S#30] |
| 공공데이터포털 (구 URL) | https://www.data.go.kr/dataset/15020626/openapi.do , https://www.data.go.kr/dataset/15020627/openapi.do | [S#2][S#13] |
| 연구관리 기술 분류 정보조회 | https://www.data.go.kr/data/15068423/openapi.do | [S#2] |
| 연구관리 전문기술분야코드 조회 | https://www.data.go.kr/data/15068280/openapi.do | [S#30] |
| 식의약 데이터 포털 | https://data.mfds.go.kr/ , https://data.mfds.go.kr/cntnts/20 , https://data.mfds.go.kr/OPCAA01F01 | 공공데이터 목록·이용안내 [S#2] |
| 의약품 공공데이터공개 | https://nedrug.mfds.go.kr/cntnts/80 | CSV/EXCEL + OpenAPI 제공 안내 [S#1][S#13] |
| 의약품안전나라 메인 / 검색 | https://nedrug.mfds.go.kr/ , https://nedrug.mfds.go.kr/index , https://nedrug.mfds.go.kr/searchDrug | [S#1] |
| 식품안전나라 API | https://www.foodsafetykorea.go.kr/apiMain.do , https://www.foodsafetykorea.go.kr/api/openApiAplcInfo.do | [S#13] |
| MFDS 영문 | https://nedrug.mfds.go.kr/eng/index , https://www.mfds.go.kr/eng/index.do | [S#22] |
### 15.4 이 소스들을 벤치마크 구조에 꽂는 방법
```
config/sources.yaml
├─ dmf_api (1차, 필수) → fetch/dmf_api.py → parse/dmf_api.py → DmfRecord
└─ nedrug_board (2차, 선택) → fetch/nedrug_board.py → parse/nedrug_board.py → BoardPost
ignore_connection_errors: true
diff/engine.py
key = DMF_PERMIT_NO
base = data/snapshots/<어제>.json
head = data/snapshots/<오늘>.json
→ added / removed / changed
검증 정책 (← pharma-radar 의 "≥2 독립 출처 또는 1 공식 1차 출처")
· API 에만 나타난 변경 → 리포트에 '확정' (공식 1차 출처)
· 게시판에만 나타난 공고 → 리포트에 '관찰 중(pending)'
· 양쪽 모두 일치 → 리포트에 '확정 + 공고 링크'
```
---
## 부록 A. 출처 목록
raw dump 에 등장한 **모든 URL** 이다. "확인" 열의 의미: **F#n** = WebFetch 로 실제 열어봄 / **S#n** = 검색 결과 링크로만 등장 / **404** = 열었으나 존재하지 않음.
### A.1 GitHub 저장소·페이지
| # | 제목 | URL | 확인 |
|---|---|---|---|
| 1 | Q00/data.go.kr-crawling | https://github.com/Q00/data.go.kr-crawling | F#1 |
| 2 | Q00/data.go.kr-crawling — url.py (raw) | https://raw.githubusercontent.com/Q00/data.go.kr-crawling/master/url.py | F#62 |
| 3 | Q00/data.go.kr-crawling — go_data_crwaler.py (raw) | https://raw.githubusercontent.com/Q00/data.go.kr-crawling/master/go_data_crwaler.py | F#64 |
| 4 | FDA/openfda | https://github.com/FDA/openfda | F#2 |
| 5 | FDA/openfda — faers/pipeline.py | https://github.com/FDA/openfda/blob/master/openfda/faers/pipeline.py | S#23 |
| 6 | Food and Drug Administration (조직) | https://github.com/FDA | S#4 |
| 7 | jbremz/FDA-Analysis | https://github.com/jbremz/FDA-Analysis | F#3 |
| 8 | logiover/fda-data-scraper | https://github.com/logiover/fda-data-scraper | F#4 |
| 9 | coderxio/OpenFDA | https://github.com/coderxio/OpenFDA | F#18 |
| 10 | DarpitPatel/OpenFDA | https://github.com/DarpitPatel/OpenFDA | S#3 |
| 11 | rOpenHealth/openfda | https://github.com/rOpenHealth/openfda | S#23 |
| 12 | roivant/openfda | https://github.com/roivant/openfda | S#23 |
| 13 | shaayohn/fda-drug-aproval-data-scraping | https://github.com/shaayohn/fda-drug-aproval-data-scraping | S#6 |
| 14 | tsbischof/fda | https://github.com/tsbischof/fda | S#19 |
| 15 | sheetalkalburgi/web-scraping | https://github.com/sheetalkalburgi/web-scraping | S#19 |
| 16 | Norbaeocystin/FDA | https://github.com/Norbaeocystin/FDA | S#19 |
| 17 | vshah1016/pharma_scraper | https://github.com/vshah1016/pharma_scraper | S#19 |
| 18 | Tanguy9862/AI-Powered-FDA-Drug-Scraper | https://github.com/Tanguy9862/AI-Powered-FDA-Drug-Scraper | F#42 |
| 19 | Tanguy9862/new-drug-approvals-dashboard | https://github.com/Tanguy9862/new-drug-approvals-dashboard | F#42 |
| 20 | anton-semerenko/pharma-radar | https://github.com/anton-semerenko/pharma-radar | F#6 |
| 21 | mrueda/nomenclator-delta | https://github.com/mrueda/nomenclator-delta | F#30 |
| 22 | Mzands2622/Zanalytix | https://github.com/Mzands2622/Zanalytix | F#31 |
| 23 | suriyadeepan/WebScraping-for-Healthcare | https://github.com/suriyadeepan/WebScraping-for-Healthcare | F#22 |
| 24 | arpitamangal/pharma-scrape-and-analysis | https://github.com/arpitamangal/pharma-scrape-and-analysis | S#8 |
| 25 | kawsarlog/AmerisourceBergen | https://github.com/kawsarlog/AmerisourceBergen | F#19 |
| 26 | MohammedAhmed-01/DataDoseProject | https://github.com/MohammedAhmed-01/DataDoseProject | F#19 |
| 27 | Dagiayy/kara-medical-telegram-data-platform | https://github.com/Dagiayy/kara-medical-telegram-data-platform | F#19 |
| 28 | bdmorris238/pharmaco-database-project | https://github.com/bdmorris238/pharmaco-database-project | F#19 |
| 29 | khushihajiyani-dotcom/drug-spending-analysis | https://github.com/khushihajiyani-dotcom/drug-spending-analysis | F#19 |
| 30 | betagouv/api-medicaments | https://github.com/betagouv/api-medicaments | S#13 |
| 31 | dgtlmoon/changedetection.io | https://github.com/dgtlmoon/changedetection.io | F#7 |
| 32 | changedetection.io — Releases | https://github.com/dgtlmoon/changedetection.io/releases | F#61 |
| 33 | changedetection.io — Notification configuration notes (wiki) | https://github.com/dgtlmoon/changedetection.io/wiki/Notification-configuration-notes | F#55 |
| 34 | mattwolfe/changedetection (포크) | https://github.com/mattwolfe/changedetection | S#5 |
| 35 | thp/urlwatch | https://github.com/thp/urlwatch | F#5 |
| 36 | thp/urlwatch — Releases | https://github.com/thp/urlwatch/releases | F#60 |
| 37 | thp/urlwatch — Issue #246 (GitHub repo 감시) | https://github.com/thp/urlwatch/issues/246 | S#12 |
| 38 | huginn/huginn | https://github.com/huginn/huginn | F#8 |
| 39 | huginn — Agent Types (wiki) | https://github.com/huginn/huginn/wiki/Agent-Types | F#65 (로딩 실패) |
| 40 | huginn — website_agent.rb | https://github.com/huginn/huginn/blob/master/app/models/agents/website_agent.rb | F#68 |
| 41 | huginn/huginn_agent | https://github.com/huginn/huginn_agent | S#12 |
| 42 | roxwize/huginn | https://github.com/roxwize/huginn | S#12 |
| 43 | itkevin/huginn | https://github.com/itkevin/huginn | S#12 |
| 44 | larsyencken/csvdiff | https://github.com/larsyencken/csvdiff | F#32 |
| 45 | ecprice/newsdiffs | https://github.com/ecprice/newsdiffs | F#43 |
| 46 | simonw/git-scraper-template | https://github.com/simonw/git-scraper-template | F#47 |
| 47 | firecrawl/firecrawl — Releases | https://github.com/firecrawl/firecrawl/releases | S#24 |
| 48 | Bwhiz/Auto-Excel-Reports | https://github.com/Bwhiz/Auto-Excel-Reports | F#16 |
| 49 | god233012yamil/Excel-Automation-Using-Python | https://github.com/god233012yamil/Excel-Automation-Using-Python | S#7 |
| 50 | prabudevarajan/Task-Reminder-Automation-Python-Excel-CSV-Email-Alerts | https://github.com/prabudevarajan/Task-Reminder-Automation-Python-Excel-CSV-Email-Alerts | S#7 |
| 51 | HasData/playwright-scraping | https://github.com/HasData/playwright-scraping | F#41 |
| 52 | ManiMozaffar/linkedIn-scraper | https://github.com/ManiMozaffar/linkedIn-scraper | S#17 |
| 53 | dineshk-qa/playwright.slack.reporter | https://github.com/dineshk-qa/playwright.slack.reporter | S#17 |
| 54 | jshchnz/claude-code-scheduler | https://github.com/jshchnz/claude-code-scheduler | F#9 |
| 55 | claude-code-scheduler — examples | https://github.com/jshchnz/claude-code-scheduler/tree/main/examples | F#39 |
| 56 | claude-code-scheduler — examples/daily-review.json (raw) | https://raw.githubusercontent.com/jshchnz/claude-code-scheduler/main/examples/daily-review.json | F#44 |
| 57 | claude-code-scheduler — src | https://github.com/jshchnz/claude-code-scheduler/tree/main/src | F#57 |
| 58 | claude-code-scheduler — src/schedulers | https://github.com/jshchnz/claude-code-scheduler/tree/main/src/schedulers | F#66 |
| 59 | addyosmani/gemini-cli-tips | https://github.com/addyosmani/gemini-cli-tips | F#35 |
| 60 | google-gemini/gemini-cli — Discussion #3215 (Headless execution) | https://github.com/google-gemini/gemini-cli/discussions/3215 | S#20 |
| 61 | testing-in-production/gemini-jobs | https://github.com/testing-in-production/gemini-jobs | **404** (F#17) |
| 62 | winsw/winsw | https://github.com/winsw/winsw | F#10 |
| 63 | winsw — docs/xml-config-file.md (v3) | https://github.com/winsw/winsw/blob/v3/docs/xml-config-file.md | F#23 |
| 64 | winsw — samples/minimal.xml (v3) | https://github.com/winsw/winsw/blob/v3/samples/minimal.xml | S#15 |
| 65 | winsw — samples/complete.xml (v3) | https://github.com/winsw/winsw/blob/v3/samples/complete.xml | S#15 |
| 66 | winsw — Releases | https://github.com/winsw/winsw/releases | F#59 |
| 67 | WinSW-Windows (조직) | https://github.com/WinSW-Windows | S#15 |
| 68 | kirillkovalenko/nssm | https://github.com/kirillkovalenko/nssm | F#51 |
| 69 | larsekje/PythonWindowsServices | https://github.com/larsekje/PythonWindowsServices | F#14 |
| 70 | HaroldMills/Python-Windows-Service-Example | https://github.com/HaroldMills/Python-Windows-Service-Example | F#15 |
| 71 | HaroldMills — example_service.py | https://github.com/HaroldMills/Python-Windows-Service-Example/blob/master/example_service.py | S#16 |
| 72 | mhammond/pywin32 — win32serviceutil.py | https://github.com/mhammond/pywin32/blob/main/win32/Lib/win32serviceutil.py | S#16 |
| 73 | mhammond/pywin32 — Demos/service/serviceEvents.py | https://github.com/mhammond/pywin32/blob/main/win32/Demos/service/serviceEvents.py | S#16 |
| 74 | mhammond/pywin32 — Issue #1563 | https://github.com/mhammond/pywin32/issues/1563 | F#52 |
| 75 | SublimeText/Pywin32 — win32serviceutil.py | https://github.com/SublimeText/Pywin32/blob/master/lib/x32/win32/lib/win32serviceutil.py | S#16 |
| 76 | 786raees/task-scheduler-python | https://github.com/786raees/task-scheduler-python | F#45 |
| 77 | Windos/BurntToast | https://github.com/Windos/BurntToast | F#33 |
| 78 | Windos/BurntToast — Discussion #140 (재부팅 버튼) | https://github.com/Windos/BurntToast/discussions/140 | S#25 |
| 79 | Windos/BurntToast — Discussion #179 (시간 선택 버튼) | https://github.com/Windos/BurntToast/discussions/179 | S#25 |
| 80 | NakedPowerShell/BurntToast | https://github.com/NakedPowerShell/BurntToast | S#25 |
| 81 | Badgerati/Hook | https://github.com/Badgerati/Hook | S#25 |
| 82 | michalzobec/autorunsalerts | https://github.com/michalzobec/autorunsalerts | F#34 |
| 83 | DatGuy1/Windows-Toasts | https://github.com/DatGuy1/Windows-Toasts | F#11 |
| 84 | GitHub30/win11toast | https://github.com/GitHub30/win11toast | F#12 |
| 85 | GitHub30/win11toast — README (raw) | https://raw.githubusercontent.com/GitHub30/win11toast/main/README.md | F#67 |
| 86 | ysfchn/toasted | https://github.com/ysfchn/toasted | F#20 |
| 87 | jithurjacob/Windows-10-Toast-Notifications | https://github.com/jithurjacob/Windows-10-Toast-Notifications | S#9 |
| 88 | jithurjacob — win10toast 디렉터리 | https://github.com/jithurjacob/Windows-10-Toast-Notifications/tree/master/win10toast | S#9 |
| 89 | jithurjacob — win10toast/__init__.py | https://github.com/jithurjacob/Windows-10-Toast-Notifications/blob/master/win10toast/__init__.py | S#9 |
| 90 | jacobcolbert/Windows-10-Toast-Notifications | https://github.com/jacobcolbert/Windows-10-Toast-Notifications | S#9 |
| 91 | caronc/apprise | https://github.com/caronc/apprise | F#38 |
| 92 | caronc/apprise — wiki/Notify_windows | https://github.com/caronc/apprise/wiki/Notify_windows | F#56 |
| 93 | WooilJeong/PublicDataReader | https://github.com/WooilJeong/PublicDataReader | F#37 |
| 94 | jjscan/data.go.kr-1 | https://github.com/jjscan/data.go.kr-1 | F#46 |
| 95 | NomaDamas/k-skill — mfds-food-safety.md | https://github.com/NomaDamas/k-skill/blob/main/docs/features/mfds-food-safety.md | F#36 |
| 96 | lorien/awesome-web-scraping | https://github.com/lorien/awesome-web-scraping | F#13 |
| 97 | lorien/awesome-web-scraping — README.md | https://github.com/lorien/awesome-web-scraping/blob/master/README.md | S#8 |
| 98 | noirquant/awesome-web-scraping | https://github.com/noirquant/awesome-web-scraping | S#8 |
| 99 | jjwangnlp/awesome-web-scraping | https://github.com/jjwangnlp/awesome-web-scraping | S#8 |
| 100 | luminati-io/Awesome-Web-Scraping | https://github.com/luminati-io/Awesome-Web-Scraping | S#8 |
| 101 | spinov001-art/awesome-web-scraping-2026 | https://github.com/spinov001-art/awesome-web-scraping-2026 | S#8 |
| 102 | duyet/awesome-web-scraper | https://github.com/duyet/awesome-web-scraper | S#8 |
| 103 | patrickloeber/llm-data-scrapers | https://github.com/patrickloeber/llm-data-scrapers | S#18 |
| 104 | realpython/list-of-python-api-wrappers | https://github.com/realpython/list-of-python-api-wrappers | S#23 |
| 105 | FareedKhan-dev/best-llm-finder-pipeline | https://github.com/FareedKhan-dev/best-llm-finder-pipeline | S#18 |
| 106 | architkaila/Fine-Tuning-LLMs-for-Medical-Entity-Extraction | https://github.com/architkaila/Fine-Tuning-LLMs-for-Medical-Entity-Extraction | S#18 |
| 107 | guilopgar/Medication-Detection-LLM | https://github.com/guilopgar/Medication-Detection-LLM | S#18 |
| 108 | diakes/coupang_crawler_python | https://github.com/diakes/coupang_crawler_python | S#1 |
| 109 | simbakeila123 (사용자) | https://github.com/simbakeila123 | S#12 |
### A.2 GitHub Topics
| 제목 | URL | 확인 |
|---|---|---|
| pharmaceutical-data | https://github.com/topics/pharmaceutical-data | F#19 |
| pharmaceuticals (python) | https://github.com/topics/pharmaceuticals?l=python | S#4 |
| pharma | https://github.com/topics/pharma?o=desc&s=updated | S#18 |
| fda | https://github.com/topics/fda | S#4 |
| open-fda | https://github.com/topics/open-fda | S#4 |
| openfda (R) | https://github.com/topics/openfda?l=r&o=desc&s=updated | S#23 |
| openpyxl | https://github.com/topics/openpyxl?o=asc&s=forks | S#7 |
| openpyxl-python | https://github.com/topics/openpyxl-python | S#7 |
| excelwriter | https://github.com/topics/excelwriter?l=python | S#7 |
| python-excel | https://github.com/topics/python-excel | S#7 |
| xlsxwriter | https://github.com/topics/xlsxwriter?l=python | S#17 |
| scheduled-tasks | https://github.com/topics/scheduled-tasks?l=python&o=desc&s=updated | S#7 |
| task-scheduler (powershell) | https://github.com/topics/task-scheduler?l=powershell | S#29 |
| playwright-python | https://github.com/topics/playwright-python?o=asc&s=updated | S#17 |
| playwright (python) | https://github.com/topics/playwright?l=python | S#17 |
| python-scraper | https://github.com/topics/python-scraper | S#24 |
| huginn | https://github.com/topics/huginn?o=asc&s=stars | S#12 |
| llm-pipeline | https://github.com/topics/llm-pipeline | S#18 |
### A.3 Gist
| 제목 | URL | 확인 |
|---|---|---|
| pywin32 서비스 예제 (drmalex07) | https://gist.github.com/drmalex07/10554232 | F#40 |
| drugs@fda scraper (seanherron) | https://gist.github.com/seanherron/5997278 | S#3 |
| drugs@fda scraper (단축) | https://gist.github.com/5997278 | S#19 |
| Windows Task Scheduler 상호작용 스크립트 (nmpowell) | https://gist.github.com/nmpowell/dc8e7187948788c5c126f01755252164 | S#29 |
| Windows 10 토스트 생성 (hygull) | https://gist.github.com/hygull/32a742339a416dcfa2990504c848c1a9 | S#9 |
| Gemini CLI Job (HainanZhao) | https://gist.github.com/HainanZhao/92b43e68850189bfee8f39a2c2581ca6 | S#20 |
### A.4 한국 식약처 / 공공데이터
| 제목 | URL | 확인 |
|---|---|---|
| **식품의약품안전처_원료의약품등록(DMF)현황 OpenAPI** | https://www.data.go.kr/data/15057075/openapi.do | **F#29** |
| **의약품안전나라 > 원료의약품등록(DMF) 정보 게시판** | https://nedrug.mfds.go.kr/bbs/117 | **F#28** |
| 의약품안전나라 DMF 검색 (에러 페이지) | https://nedrug.mfds.go.kr/searchDmf | F#63 (에러) |
| 식품의약품안전처_의약품 제품 허가정보 | https://www.data.go.kr/data/15095677/openapi.do | F#54 |
| 의약품안전나라 메인 | https://nedrug.mfds.go.kr/ | S#1 |
| 의약품안전나라 index | https://nedrug.mfds.go.kr/index | S#1 |
| 의약품등 검색 | https://nedrug.mfds.go.kr/searchDrug | S#1 |
| 사용자별서비스 > 일반소비자 | https://nedrug.mfds.go.kr/pbp/CCBRA01 | S#1 |
| 의약품 공공데이터공개 | https://nedrug.mfds.go.kr/cntnts/80 | S#1 |
| MFDS Drug Safety Korea (영문) | https://nedrug.mfds.go.kr/eng/index | S#22 |
| 식품의약품안전처_의약품개요정보(e약은요) | https://www.data.go.kr/data/15075057/openapi.do | S#13 |
| 식품의약품안전처_의약품 낱알식별 정보 | https://www.data.go.kr/data/15057639/openapi.do | S#13 |
| 식품의약품안전처_의약품 낱알식별 정보 (추천) | https://www.data.go.kr/data/15057639/openapi.do?recommendDataYn=Y | S#30 |
| 식품의약품안전처_필수의약품내역 | https://www.data.go.kr/data/15058207/openapi.do?recommendDataYn=Y | S#30 |
| 식품의약품안전처 의약품 관련 정보 (파일데이터) | https://www.data.go.kr/data/15020627/fileData.do | S#30 |
| 공공데이터포털 (구 URL 1) | https://www.data.go.kr/dataset/15020626/openapi.do | S#2 |
| 공공데이터포털 (구 URL 2) | https://www.data.go.kr/dataset/15020627/openapi.do | S#13 |
| 연구관리 기술 분류 정보조회 서비스 | https://www.data.go.kr/data/15068423/openapi.do | S#2 |
| 연구관리 전문기술분야코드 조회 서비스 | https://www.data.go.kr/data/15068280/openapi.do | S#30 |
| OPENAPI Detail (영문 포털) | https://www.data.go.kr/en/data/15117134/openapi.do | S#22 |
| 공공데이터포털 API 명세 조회 엔드포인트 | https://www.data.go.kr/pubn/lab/gui/IrosDevGuide/selectReqResPrmList.do | F#62 |
| 식의약 데이터 포털 | https://data.mfds.go.kr/ | S#2 |
| 식의약 데이터 포털 — 공공데이터 목록 및 이용안내 | https://data.mfds.go.kr/cntnts/20 | S#2 |
| 식의약 데이터 포털 — 공공데이터 상세 | https://data.mfds.go.kr/OPCAA01F01 | S#2 |
| 식의약 데이터 포털 — 공공데이터 검색 | https://data.mfds.go.kr/OPCAA01F01/search?selectedTab=tab1&taskDivsCd=3&taskDivsDtlCd=7&rchSrvcKorNm=&btnSearch= | S#2 |
| 식품안전나라 데이터활용서비스 | https://www.foodsafetykorea.go.kr/apiMain.do | S#13 |
| 식품안전나라 OpenAPI 신청 | https://www.foodsafetykorea.go.kr/api/openApiAplcInfo.do | S#13 |
| KHIDI — DMF 등록 공고(7월 둘째주) | https://www.khidi.or.kr/board/view?pageNum=48&rowCnt=10&menuId=MENU01872&maxIndex=00487793189998&minIndex=00487441479998&schType=0&schText=&categoryId=&continent=&country=&upDown=0&boardStyle=&no1=912&linkId=26604564 | S#11 |
| KHIDI — [지침]원료의약품 등록(DMF) 처리 절차 | https://www.khidi.or.kr/board/view?pageNum=1&rowCnt=10&menuId=MENU01872&maxIndex=99999999999999&minIndex=99999999999999&schType=0&schText=&categoryId=&continent=&country=&upDown=0&boardStyle=&no1=0&linkId=26605812 | S#11 |
| 식품의약품안전평가원 — KDMF | https://www.nifds.go.kr/brd/m_87/list.do | S#11 |
| MFDS 영문 메인 | https://www.mfds.go.kr/eng/index.do | S#22 |
| MFDS 영문 — Drugs > GIFT | https://www.mfds.go.kr/eng/wpge/m_1176/de011009l001.do | S#22 |
| MFDS 영문 — Drugs > Approval Process | https://www.mfds.go.kr/eng/wpge/m_17/denofile.do | S#22 |
### A.5 FDA (미국) 및 상용 DMF 데이터
| 제목 | URL | 확인 |
|---|---|---|
| List of Drug Master Files (DMFs) | https://www.fda.gov/drugs/drug-master-files-dmfs/list-drug-master-files-dmfs | **404** (F#48) |
| Drug Master Files (DMFs) 개요 | https://www.fda.gov/drugs/drug-master-files-dmfs | **404** (F#53) |
| Drug Master Files (DMFs) — 제출 요건 | https://www.fda.gov/drugs/forms-submission-requirements/drug-master-files-dmfs | S#6 |
| Types of Drug Master Files (DMFs) | https://www.fda.gov/drugs/drug-master-files-dmfs/types-drug-master-files-dmfs | S#6 |
| Guideline for Drug Master Files (DMF) | https://www.fda.gov/drugs/drug-master-files-dmfs/guideline-drug-master-files-dmf | S#6 |
| openFDA 메인 | https://open.fda.gov/ | S#3 |
| openFDA — Drug API Endpoints | https://open.fda.gov/apis/drug/ | S#3 |
| openFDA — Orange Book | https://open.fda.gov/apis/drug/orangebook/ | S#3 |
| openFDA — drug NDC download | https://open.fda.gov/apis/drug/ndc/download/ | F#18 |
| PharmaCompass — US DMF Database | https://www.pharmacompass.com/us-drug-master-files-dmfs | S#6 |
| John Snow Labs — FDA DMF Directory | https://www.johnsnowlabs.com/marketplace/fda-drug-master-files-directory/ | S#27 |
| pharmaexcipients — Excipient DMF List | https://www.pharmaexcipients.com/excipient-sources/excipient-dmf-list/ | S#27 |
| Dmf List FDA Quarterly Spreadsheet | https://dmf-list.backgroundscheck.info/ | S#27 |
| fdapals — DMF FDA Guidance | https://fdapals.com/services/dmf-fda-guidance/ | S#27 |
| Apify — FDA Orange Book Scraper | https://apify.com/labrat011/fda-orange-book-scraper/api | S#3 |
| Apify — OpenFDA Scraper (Python) | https://apify.com/fortuitous_pirate/openfda-scraper/api/python | S#3 |
| Apify — OpenFDA Drug Intelligence + AI (Python) | https://apify.com/benthepythondev/openfda-drug-intelligence/api/python | S#23 |
| Wikipedia — Drug Master File | https://en.wikipedia.org/wiki/Drug_Master_File | S#6 |
| Wikipedia — DMF | https://en.wikipedia.org/wiki/DMF | S#11 |
| Wikipedia — Approved Drug Products with Therapeutic Equivalence Evaluations | https://en.wikipedia.org/wiki/Approved_Drug_Products_with_Therapeutic_Equivalence_Evaluations | S#3 |
| Wikipedia — Web crawler | https://en.wikipedia.org/wiki/Web_crawler | S#1 |
| Wikipedia — Censorship of GitHub | https://en.wikipedia.org/wiki/Censorship_of_GitHub | S#11 |
| Wikipedia — Kim Gang-lip | https://en.wikipedia.org/wiki/Kim_Gang-lip | S#22 |
### A.6 공식 문서 (도구·라이브러리)
| 제목 | URL | 확인 |
|---|---|---|
| Claude Code — Run Claude Code programmatically (headless) | https://code.claude.com/docs/en/headless | **F#25** |
| Claude Code — Schedule recurring tasks in Desktop | https://code.claude.com/docs/en/desktop-scheduled-tasks | **F#49** |
| Claude Code — GitHub Actions | https://code.claude.com/docs/en/github-actions | S#14 |
| Claude Code — 문서 색인 | https://code.claude.com/docs/llms.txt | F#25 |
| ChangeDetection.io API v1 | https://changedetection.io/docs/api_v1/index.html | **F#27** |
| urlwatch — Jobs | https://urlwatch.readthedocs.io/en/latest/jobs.html | **F#26** |
| urlwatch — Filters | https://urlwatch.readthedocs.io/en/latest/filters.html | **F#50** |
| urlwatch — 문서 루트 | https://urlwatch.readthedocs.io/ | F#5 |
| urlwatch — 홈페이지 | https://thp.io/2008/urlwatch/ | F#5 |
| NSSM — Usage | https://nssm.cc/usage | **F#24** |
| NSSM — 홈페이지 | http://nssm.cc/ | F#51 |
| XlsxWriter — Worksheet | https://xlsxwriter.readthedocs.io/worksheet.html | **F#58** |
| win10toast (PyPI) | https://pypi.org/project/win10toast/ | S#9 |
| pyfda (PyPI) | https://pypi.org/project/pyfda/ | S#23 |
| github (PyPI) | https://pypi.org/project/github/ | S#23 |
| Gemini CLI — Automation and triage processes | https://geminicli.com/docs/issue-and-pr-automation/ | S#20 |
| JSON Schema | https://json-schema.org/ | F#25 |
| jq | https://jqlang.org/ | F#25 |
| Claude Console | https://platform.claude.com | F#25 |
### A.7 블로그·튜토리얼·기사
| 제목 | URL | 확인 |
|---|---|---|
| Drew Bredvick — How to Run Claude Code as a Cron Job | https://drew.tech/posts/claude-code-as-a-cron-job | **F#21** |
| MindStudio — What Is Claude Code Headless Mode? | https://www.mindstudio.ai/blog/claude-code-headless-mode-autonomous-agents | S#10 |
| wmedia.es — Claude Code Can Work While You Sleep | https://wmedia.es/en/tips/claude-code-headless-mode-autonomous-agent | S#10 |
| hidekazu-konishi — Claude Code in CI/CD and Headless Automation | https://hidekazu-konishi.com/entry/claude_code_cicd_and_headless_automation.html | S#10 |
| Claude Code for Clinicians — Ch.17 Headless Mode | https://iyadsultan.github.io/claude-code-for-clinicians/ch17-headless-mode/ | S#10 |
| StackNotice — Claude Code in Scripts (2026) | https://stacknotice.com/blog/claude-code-headless-scripting-2026 | S#10 |
| Usagebar — How to Set Up Cron Jobs with Claude Code | https://usagebar.com/blog/how-to-do-cron-job-setup-on-claude-code | S#10 |
| DevShelfHub — Claude Code Automation: Non-Interactive Mode | https://www.devshelfhub.com/tutorials/claude-code/automation/ | S#10 |
| HeyClaude — Claude Code Process Automation | https://heyclau.de/entry/guides/business-process-automation | S#10 |
| Like One — Claude Code Headless Mode Guide (2026) | https://likeone.ai/blog/claude-code-headless-mode-guide-2026/ | S#10 |
| Build This Now — Claude Code Headless Mode | https://www.buildthisnow.com/blog/guide/development/claude-code-headless-mode | S#10 |
| jannikreinhard — Claude Code in GitHub Actions | https://jannikreinhard.com/claude-code-github-actions/ | S#14 |
| Level Up Coding — Claude Code Routines | https://levelup.gitconnected.com/claude-code-routines-the-cron-replacement-i-didnt-know-i-needed-6f53cf476577?gi=f0e7b272cd2b | S#14 |
| SmartScope — Claude Code Scheduled Execution (AI development) | https://smartscope.blog/en/ai-development/claude-code-scheduled-automation-guide/ | S#14 |
| SmartScope — Claude Code + Cron 2025 | https://smartscope.blog/en/generative-ai/claude/claude-code-cron-schedule-automation-complete-guide-2025/ | S#14 |
| SmartScope — Claude Code Scheduled Execution (generative-ai) | https://smartscope.blog/en/generative-ai/claude/claude-code-scheduled-automation-guide/ | S#14 |
| SmartScope — Claude Code × Cron Complete Automation Guide | https://smartscope.blog/en/generative-ai/claude/claude-code-cron-automation-guide/ | S#14 |
| SmartScope — Codex CLI Automation: 3 Workflow Patterns | https://smartscope.blog/en/generative-ai/chatgpt/codex-cli-automation-workflow-patterns/ | S#26 |
| claudefa.st — Claude Code Scheduled Tasks (2026) | https://claudefa.st/blog/guide/development/scheduled-tasks | S#28 |
| atalupadhyay — Scheduled Tasks: How to Put Claude on Autopilot | https://atalupadhyay.wordpress.com/2026/03/02/scheduled-tasks-how-to-put-claude-on-autopilot/ | S#28 |
| aixplore — Mastering Scheduled Tasks in Claude Code | https://aixplore.in/blog_post?slug=mastering-scheduled-tasks-in-claude-code-guide | S#28 |
| Claude Cowork — Scheduled Tasks Guide | https://claudecowork.im/blog/scheduled-tasks-guide | S#28 |
| MCP Market — Windows Task Scheduler Claude Code Skill | https://mcpmarket.com/tools/skills/windows-task-scheduler | S#28 |
| leeboonstra.dev — Unleashing Gemini CLI Power in GitHub Actions | https://www.leeboonstra.dev/genai/gemini_cli_github_actions/ | S#20 |
| Testing in Production — Scheduling Jobs With Gemini CLI and Cron | https://www.testinginproduction.co/blog/automating-ai-jobs-with-gemini-cli | S#20 |
| Gemini CLI All in One — 10 Real Workflows | https://geminicli.one/blog/gemini-cli-use-cases-workflows | S#20 |
| DeployHQ — OpenAI Codex CLI: Complete Getting Started Guide | https://www.deployhq.com/blog/getting-started-with-openai-codex-cli-ai-powered-code-generation-from-your-terminal | S#26 |
| Codex KB — Headless and Batch Mode | https://codex.danielvaughan.com/2026/04/18/codex-cli-headless-batch-mode-automation/ | S#26 |
| Codex KB — Automations as Lightweight CI | https://codex.danielvaughan.com/2026/07/19/codex-automations-lightweight-ci-scheduled-agents-codex-exec-github-actions/ | S#26 |
| Codex KB — Automations and Scheduled Tasks | https://codex.danielvaughan.com/2026/03/27/codex-cli-automations-scheduled-tasks/ | S#26 |
| Codex KB — codex exec, Non-Interactive Mode | https://codex.danielvaughan.com/2026/03/26/codex-cli-cicd-non-interactive/ | S#26 |
| Developers Digest — Codex Exec in CI | https://www.developersdigest.tech/blog/codex-exec-ci-headless-guide | S#26 |
| Codexlog — How to Set Up a CI/CD Pipeline with Codex | https://codexlog.dev/guides/tasks/setup-ci-cd-pipeline/ | S#26 |
| PageCrawl.io — Best Open-Source Website Change Detection Tools | https://pagecrawl.io/blog/open-source-website-change-detection-tools | S#5 |
| GIGAZINE — changedetection.io 리뷰 | https://gigazine.net/gsc_news/en/20260517-changedetection-io | S#5 |
| alternativeto — urlwatch 대안 | https://alternativeto.net/software/urlwatch | S#5 |
| alternativeto — urlwatch 대안 p5 | https://alternativeto.net/software/urlwatch/?p=5 | S#5 |
| alternativeto — changedetection.io 대안 p2 | https://alternativeto.net/software/changedetection-io/?p=2 | S#5 |
| alternativeto — changedetection.io 대안 p3 | https://alternativeto.net/software/changedetection-io/?p=3 | S#5 |
| Simon Willison — Git scraping | https://simonwillison.net/2020/Oct/9/git-scraping/ | S#24 |
| ScraperAPI — How to Scrape GitHub Data Repository With Python | https://www.scraperapi.com/web-scraping/github/ | S#24 |
| PDQ — Display toast notifications with BurntToast | https://www.pdq.com/blog/display-toast-notifications-with-powershell-burnt-toast-module/ | S#25 |
| CyberDrain — Monitoring with PowerShell: Windows Updates 알림 | https://www.cyberdrain.com/monitoring-with-powershell-notifying-users-of-windows-updates/ | S#25 |
| Joshua Dearing — Reboot Notifications with BurntToast | https://www.dearing.dev/posts/Reboot-Notifications-with-BurntToast-A-Simple-Guide/ | S#25 |
| PowerShell Forums — burnt toast notification | https://forums.powershell.org/t/powershell-burnt-toast-notification/24222 | S#25 |
| usro.net — How to Create a Windows Service with WinSW | https://blog.usro.net/2024/10/how-to-create-a-windows-service-with-winsw-a-step-by-step-guide/ | S#15 |
| ehmiiz.se — PowerShell Guide: Script as a Windows Service | https://www.ehmiiz.se/blog/ps_scriptasaservice/ | S#15 |
| Infonautics — Run any program as a Windows background service with WinSW | https://www.infonautics.ch/blog/run-any-program-as-a-windows-background-service-with-winsw/ | S#15 |
| python-win32 메일링리스트 — automatically restart python service after crash | https://mail.python.org/pipermail/python-win32/2017-January/013807.html | S#16 |
| Woteq Zone — How to Monitor Windows Services Using Python | https://woteq.com/how-to-monitor-windows-services-using-python-on-windows | S#16 |
| DEV Community — Building a Robust Windows Service in Python with win32serviceutil | https://dev.to/demola12/building-a-robust-windows-service-in-python-with-win32serviceutil-part-13-1k6k | S#16 |
| Oxylabs — Automated Web Scraper With Python & Windows Task Scheduler | https://oxylabs.io/blog/automated-web-scraper-windows-task-scheduler | S#29 |
| Flipnode — Automated Web Scraper With Python & Windows Task Scheduler | https://flipnode.io/automated-web-scraper-windows-task-scheduler | S#29 |
| JC Chouinard — How to Automate Python Scripts with Task Scheduler | https://www.jcchouinard.com/python-automation-using-task-scheduler/ | S#29 |
| Biztory — Run a python script on a schedule using Task Scheduler | https://biztory.com/blog/run-a-python-script-on-a-schedule-using-the-in-built-task-scheduler-windows-app | S#29 |
| Medium (Vnalla) — Two ways to run Python Scripts Every Day Automatically | https://medium.com/@vineelan09/two-ways-to-run-python-scripts-every-day-automatically-3c86079fe449 | S#29 |
| Oreate AI — Making Your Python Web Scraper Work for You | https://www.oreateai.com/blog/making-your-python-web-scraper-work-for-you-automating-with-windows-task-scheduler/7efa97048b3b0b28a3513e73ed1235fd | S#29 |
| Adobe User Sync Tool — Scheduling | https://adobe-apiplatform.github.io/user-sync.py/en/success-guide/scheduling.html | S#29 |
| Medium — Playwright report to Slack | https://medium.com/@indraaristya/playwright-report-to-slack-e07e8996c9de | S#17 |
| Medium — Playwright with GitHub Actions and Slack Notification | https://medium.com/@vinayakhk9/playwright-with-github-actions-and-slack-notification-b56eb982659b | S#17 |
| ScrapeGraphAI — LLM Web Scraping | https://scrapegraphai.com/blog/llm-web-scraping | S#18 |
| Grepsr — How to Design Scraping Systems for LLM Training Pipelines | https://www.grepsr.com/blog/llm-data-pipelines-web-scraping-grepsr/ | S#18 |
| IntuitionLabs — AI and the Future of Regulatory Affairs | https://intuitionlabs.ai/articles/ai-future-regulatory-affairs-pharma | S#4 |
| IntuitionLabs — Open Source Pharma: Tools & Trends | https://intuitionlabs.ai/articles/open-source-pharma-trends | S#4 |
| Clarivate — Biopharma Regulatory Compliance Services | https://clarivate.com/life-sciences-healthcare/research-development/regulatory-compliance-intelligence/ | S#4 |
| Vistaar — Regulatory Intelligence Database, Software & Tools | https://www.vistaar.ai/blog/regulatory-intelligence-database-software-tools-for-compliance/ | S#4 |
| Precision for Medicine — How to launch a clinical trial in South Korea | https://www.precisionformedicine.com/blog/how-to-launch-a-clinical-trial-in-south-korea-investigational-new-drug-application-process | S#22 |
| velog — 네이버 의약품사전 크롤링 | https://velog.io/@xenrose/naverPillCrawling | S#1 |
| samslow.github.io — 식품안전나라 크롤링 가이드 | https://samslow.github.io/diary/2019/02/11/sickfoom-crawling-guide/ | S#1 |
| velog — 공공데이터 포털API사용하기 | https://velog.io/@almondbreez0_3/xiniel0v | S#2 |
| Medium (Sarah Na) — Selenium으로 웹사이트 크롤링하기(2) | https://2island.medium.com/python-selenium%EC%9C%BC%EB%A1%9C-%EC%9B%B9%EC%82%AC%EC%9D%B4%ED%8A%B8-%ED%81%AC%EB%A1%A4%EB%A7%81%ED%95%98%EA%B8%B0-2-%EC%9B%B9-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EC%A0%9C%EC%96%B4%ED%95%B4%EB%B3%B4%EA%B8%B0-1ffc5e05179d | S#21 |
| Steemit — Mediteam.us 개발 Python & Selenium 구글검색 크롤링 | https://steemit.com/kr/@junn/mediteam-us-python-and-selenium | S#21 |
| greeksharifa — Python Selenium 사용법 | https://greeksharifa.github.io/references/2020/10/30/python-selenium-usage/ | S#21 |
| Summer's Blog — GPT가 알려주는데로 크롤링 만들기 | https://sunmerrr.github.io/other/crawling-1/ | S#21 |
| teamlab.github.io — Selenium으로 네이버 연극 데이터 크롤링하기 | https://teamlab.github.io/jekyllDecent/blog/crawling%20with%20python/Selenium%EC%9C%BC%EB%A1%9C-%EB%84%A4%EC%9D%B4%EB%B2%84-%EC%97%B0%EA%B7%B9-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%81%AC%EB%A1%A4%EB%A7%81%ED%95%98%EA%B8%B0-with-Python | S#21 |
| JaeSeoKim's Blog — Selenium을 이용한 웹 크롤링 | https://jaeseokim.dev/Python/python-Selenium%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%9B%B9-%ED%81%AC%EB%A1%A4%EB%A7%81-%EA%B0%84%EB%8B%A8-%EC%82%AC%EC%9A%A9%EB%B2%95-%EB%B0%8F-%EC%98%88%EC%A0%9C/ | S#21 |
| Hugging Face — stack-v2-python 데이터셋 (무관) | https://huggingface.co/datasets/yushengsu/stack-v2-python-with-content-chunk1-modified/viewer/default/train?p=1 | S#24 |
| Hugging Face — Job_Knowledge_Graph commit (무관) | https://huggingface.co/spaces/nqtruong/Job_Knowledge_Graph/commit/1049d38a30e7fa8fbd52d5076e265dafd2bcb104 | S#24 |
---
## 부록 B. 미해결 질문 / 실측 필요 항목
### B.1 최우선 (설계를 바꿀 수 있는 것)
- [ ] **`agy` (Google Antigravity CLI) 의 실제 CLI 표면을 실측한다.** raw dump 는 `agy` 를 전혀 조사하지 않았다. `agy --help` 로 다음 4가지를 확인하고 `docs/research/` 에 기록: ① 비대화형 프롬프트 플래그(`-p`?), ② JSON 출력 플래그, ③ JSON Schema 강제 옵션 존재 여부, ④ 승인/샌드박스 우회 플래그. 없으면 `config/settings.yaml` 의 `agent.*` 를 전면 수정해야 한다.
- [ ] **`agy` 설치 경로·인증 방식·환경변수를 확인한다.** `claude` 는 `ANTHROPIC_API_KEY`(bare 모드), `gemini` 는 `GEMINI_SYSTEM_MD` 등을 쓴다. `agy` 의 대응물이 무엇인지, 그리고 **스케줄드 태스크(비대화형 세션)에서 인증이 유지되는지** — drew.tech 이 지적한 "snapshots preserve auth state" 문제와 동형.
- [ ] **`agy` 미설치 시 자동 부트스트랩이 가능한지.** 요구사항에 "agy 가 없으면 자동 설치하고 필요하면 Windows 프롬프트 창을 띄운다"가 있으나, 무인 설치 경로(패키지 관리자? 설치 스크립트?)가 확인되지 않았다.
- [ ] **DMF OpenAPI 응답의 실제 JSON 중첩 구조.** `response.body.items.item` 인지 `body.items` 인지. `§15.1` 의 `probe_dmf_api.py` 로 실측 후 `schemas/dmf_record.schema.json` 확정.
- [ ] **`serviceKey` 의 인코딩/디코딩 키 구분.** data.go.kr 은 두 종류를 발급한다. `requests` 의 `params=` 로 넘길 때 어느 쪽이 맞는지 실측.
- [ ] **`resultCode` 정상값과 에러 코드 목록.** 재시도 대상 에러와 즉시 실패 에러를 구분해야 한다.
- [ ] **DMF 현황 전체 레코드 수(`totalCount`).** `jjscan/data.go.kr-1` 이 351,010건에 17시간을 썼다. DMF 가 몇 건인지에 따라 백필 전략(병렬 여부)이 달라진다.
- [ ] **`nedrug.mfds.go.kr/bbs/117` 이 정적 HTML 인지 JS 렌더링인지.** 정적이면 `requests` + BeautifulSoup, 아니면 Playwright 도입. `scripts/inspect_board.py` 로 실측.
- [ ] **게시판 페이지네이션 파라미터.** 총 710건, 페이지당 10/20/30/40/50 선택 가능하다는 것만 확인됨. 실제 쿼리스트링(`page=`? `pageNo=`? POST?)은 미확인.
- [ ] **`§14.6` 의 CSS 선택자 전량.** `table tbody tr td:nth-child(n)` 는 추정치다. 실제 DOM 으로 반드시 교체.
### B.2 데이터 소스 (2차)
- [ ] **`https://nedrug.mfds.go.kr/searchDmf` 의 올바른 경로.** WebFetch 가 에러 페이지를 반환했다 [F#63]. DMF 검색 UI 가 실제로 존재하는지, 있다면 URL 은 무엇인지.
- [ ] **식약처 전자민원창구의 "원료의약품 등록 현황(신규등록, 변경등록, 연차보고)"** [S#11] 이 별도 소스로 쓸 만한지. 특히 **"변경등록"이 명시적으로 구분되어 있다면** diff 없이도 변경 사유를 얻을 수 있다.
- [ ] **KHIDI 제약산업정보포털 공고**가 nedrug 게시판보다 빠른지/늦은지. 빠르면 조기 신호 소스로 추가.
- [ ] **`식품의약품안전처_의약품 제품 허가정보` API 의 엔드포인트 URL 과 파라미터.** 페이지에 명시되지 않았다 [F#54]. `CANCEL_DATE`/`CANCEL_NAME`/`CHANGE_DATE` 필드가 있으므로 **취하/변경 판정의 보조 근거**가 될 수 있다.
- [ ] **DMF API 의 갱신 주기.** "Last modified September 19, 2025" 만 확인됨. 일 단위인지 주 단위인지에 따라 06:00 실행의 의미가 달라진다(주 단위면 대부분의 실행이 no-change).
- [ ] **`ENTP_NAME` / `MNFCTR_NAME` 의 표기 흔들림 실태.** `Tanguy9862` 는 회사명 1000종→700종 정규화가 필요했다. 우리도 샘플 1,000건으로 실태 조사 후 `parse/normalize.py` 규칙 작성.
### B.3 Windows 운영
- [ ] **WinSW `samples/minimal.xml` 의 필수 요소 확인.** `§10.1` 의 XML 초안에서 `<id>`/`<name>`/`<description>` 이 필수인지, `<arguments>` 와 `<startarguments>` 를 언제 쓰는지.
- [ ] **WinSW v2.12.0 과 v3.0.0-alpha.11 중 어느 것을 쓸지.** alpha 는 위험하지만 `docs/xml-config-file.md` 는 v3 브랜치 문서다. **v2 에서 동일 요소가 지원되는지 확인 필요** — 특히 `<delayedAutoStart>`, `<priority>`, `<resetfailure>`.
- [ ] **WinSW 실행에 .NET 런타임이 필요한지, 어느 버전인지.** Windows 11 기본 탑재로 충분한지.
- [ ] **`schtasks /create` 의 정확한 옵션.** `/ru`(실행 사용자), `/rp`(비밀번호), `/rl HIGHEST`, `/np`(비밀번호 저장 안 함), `/it`(대화형)의 조합. 특히 **사용자 컨텍스트 토스트 태스크는 `/it` 가 필요한지**.
- [ ] **사용자가 로그오프 상태일 때 06:00 태스크가 도는지.** `/ru SYSTEM` 이면 돌지만 토스트는 못 띄운다 — 그래서 큐 파일 방식을 택했으나, 실제 동작 검증 필요.
- [ ] **`win11toast` 의 `os.chdir()` 함정 재현.** "실행 시 CWD 가 `C:\Windows\system32`" [F#67] 가 Task Scheduler 실행 시에도 발생하는지.
- [ ] **Apprise `windows://` 가 pywin32 를 통해 실제로 토스트를 띄우는지, 250자 제한이 어떻게 잘리는지.**
- [ ] **PC 절전/최대절전 시 Task Scheduler 의 "작업을 실행하기 위해 절전 모드 해제" 옵션**을 켤 것인지. Claude Desktop 문서가 지적한 "컴퓨터가 자면 실행이 스킵된다" 문제의 Windows 네이티브 해법.
### B.4 리포트
- [ ] **XlsxWriter `autofilter()` / `freeze_panes()` 의 정확한 시그니처.** [F#58] 에서 확보 실패. 문서 재확인 필요.
- [ ] **한글 시트명에 `write_url('internal:...')` 가 정상 동작하는지.** 문서는 "Worksheet names with spaces should be single quoted" 만 언급. 한글 + 공백 조합 실측 필요.
- [ ] **엑셀에서 열었을 때 internal 링크가 실제로 클릭되는지** (Excel 버전별, LibreOffice 호환성).
- [ ] **리포트 파일명 규칙 확정.** `YYYY-MM-DD_DMF_리포트.xlsx` 로 할 경우 한글 파일명이 `win11toast` 의 `on_click` 으로 열릴 때 문제가 없는지.
### B.5 diff / 데이터 모델
- [ ] **`DMF_PERMIT_NO` 가 진짜 불변 유일 키인지.** 재발급·번호 변경 사례가 있으면 diff 가 대량 오탐을 낸다. 과거 스냅샷 2개를 확보해 검증.
- [ ] **"취하"가 API 에서 어떻게 표현되는지.** 레코드가 사라지는가(→ `removed`), 아니면 상태 필드가 바뀌는가(→ `changed`)? **전자라면 API 일시 장애 시 전량 `removed` 오탐이 발생하므로, 레코드 수가 전일 대비 N% 이상 감소하면 diff 를 중단하는 안전장치가 필수다.**
- [ ] **`ignore_columns` 에 넣어야 할 노이즈 필드 실태.** 조회수 외에 무엇이 매일 바뀌는지 1주일 관측.
- [ ] **우선순위 규칙(`§14.7` priority_rules)의 타당성**을 실무자에게 확인.
### B.6 검색 예산 소진으로 미완인 조사
WebSearch 예산(200/200)이 소진되어 다음 3개 질의가 **수행되지 않았다**. 필요 시 WebFetch + Bing/DuckDuckGo 로 보완:
- [ ] `github 식약처 공고 크롤링 텔레그램 알림 스케줄러 파이썬 회수 판매중지` [S#31 미수행]
- [ ] `github xlsxwriter multi-sheet report internal hyperlink summary sheet python generator repository` [S#32 미수행]
- [ ] `github Playwright python Windows Task Scheduler headless daily scrape report "pythonw" OR "schtasks" repository` [S#33 미수행]
추가로 확인하지 못한 것:
- [ ] **`urlwatch` 의 `diff_tool` 옵션** — Filters 페이지에 없었다 [F#50]. Configuration 페이지 확인 필요.
- [ ] **Huginn 의 `ChangeDetectorAgent`, `DeDuplicationAgent`, `DigestAgent`, `EmailDigestAgent`, `SchedulerAgent`, `ShellCommandAgent` 설명** — wiki 로딩 실패 [F#65].
- [ ] **`jshchnz/claude-code-scheduler` 의 `src/schedulers/windows.ts` 실제 구현** — schtasks 를 어떻게 호출하는지 [F#66 은 파일명만 확인].
- [ ] **FDA DMF 목록 스프레드시트의 실제 다운로드 URL** — fda.gov 두 페이지가 모두 404 [F#48][F#53].
- [ ] **`FDA/openfda`, `changedetection.io`, `huginn`, `urlwatch` 등의 정확한 최근 커밋 날짜** — GitHub 페이지에서 텍스트로 노출되지 않아 커밋 총수로 대체 기록했다.
### B.7 라이선스·규범
- [ ] **참고한 저장소 중 라이선스 미확인 항목**(`§11.4` 표의 "미확인" 행)을 코드 참고 전에 확인.
- [ ] **식약처 공공데이터 이용약관** — 저장·재배포·사내 공유 범위. data.go.kr 활용 신청 시 "운영" 등급으로 승격하려면 활용 사례 등록이 필요하다 [F#54].
- [ ] **크롤링 빈도 예의(rate limit)** — 개발 등급 10,000 calls 제한 [F#29] 안에서 일일 실행이 몇 콜을 쓰는지 계산하고, 게시판 스크래핑에는 요청 간 지연을 둘 것.