- 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 문서 지도 갱신
536 lines
36 KiB
Markdown
536 lines
36 KiB
Markdown
# 핸드오프 — DMF Crawler
|
||
|
||
> **다음 세션은 이 문서부터 읽어라.** 지금까지 확정된 것, 실측으로 밝혀진 것, 진행 중인 작업, 다음에 할 일이 전부 여기 있다.
|
||
|
||
**작성 시각**: 2026-09-03
|
||
**프로젝트 루트**: `D:\workspace\DMF_Crawler`
|
||
**상태**: 구현 진행 중 / 실제 9,084건 기준선 import 완료 / API 키 선택 정책 복구 / xlsx·Windows GUI 디자인 RED 통과 / 온보딩 스크롤·문장 UX 수정 / AGY headful CCBAC03 xlsx 수집 수동 smoke 성공(9,816건) / 전체 79개 테스트 통과
|
||
|
||
**2026-09-03 추가 사용자 지적 — 스크롤·상단 안내문·AGY headful 수집**
|
||
|
||
사용자 지적:
|
||
|
||
- 온보딩 스크롤창이 실제 항목/텍스트 위에서 휠 스크롤되지 않는다.
|
||
- 상단 노란 안내 박스의 문제가 색/박스 자체라기보다 문장이 `무엇/왜/어떻게/다음` 식으로 AI스럽고 한국어 UX 문장으로 부자연스럽다.
|
||
- AGY가 사람처럼 headful Chrome 으로 수집해야 한다고 여러 번 말했는데, 현재 구현은 공식 API/기존 DB 중심이고 AGY는 요약용 부가 단계처럼만 동작한다.
|
||
|
||
TDD 처리:
|
||
|
||
- 추가 RED:
|
||
- `test_scroll_frame_mousewheel_scrolls_when_pointer_is_over_content`
|
||
- `test_onboarding_status_panel_names_blockers_instead_of_generic_yellow_info_box`
|
||
- `test_headful_browser_prompt_contract_forbids_headless_http_and_secrets`
|
||
- `test_headful_browser_collect_validates_download_and_returns_raw_records`
|
||
- `test_headful_browser_collect_rejects_incomplete_downloads`
|
||
- `test_headful_browser_collect_reports_mcp_permission_denial_from_stderr`
|
||
- `test_headful_browser_collect_reports_execute_url_permission_denial_from_stderr`
|
||
- `test_headful_mcp_allow_rule_is_scoped_and_does_not_use_dangerous_skip`
|
||
- `test_headful_browser_prompt_blocks_wrong_review_result_export`
|
||
- `test_headful_browser_accepts_public_ccbac03_xlsx_headers`
|
||
- `test_source_auto_without_api_key_routes_to_headful_browser`
|
||
- `test_source_browser_mode_routes_to_headful_even_when_api_key_exists`
|
||
- `test_agy_google_login_is_optional_when_ai_disabled_for_api_mode`
|
||
- `test_agy_is_prompted_for_headful_collection_when_auto_has_no_api_key`
|
||
- `test_agy_is_prompted_for_forced_browser_mode_even_with_api_key`
|
||
- RED 실패 확인:
|
||
- 콘텐츠 라벨 위에서 `<MouseWheel>` 발생 시 `canvas.yview()` 가 이동하지 않음.
|
||
- 상단 안내문에 `무엇:`, `왜:`, `어떻게:`, `다음:` 라벨이 실제로 노출됨.
|
||
- `dmf_crawler.agy.browser_collect` 모듈 및 `pipeline._fetch_with_headful_browser` 경로가 없음.
|
||
- 첫 실제 headful smoke 에서 AGY가 `mcp` permission auto-deny 로 빈 응답을 냄.
|
||
- `mcp(chrome-devtools/*)`를 top-level 에만 넣었을 때 실제 AGY shared config 에 매칭되지 않아, `userSettings.globalPermissionGrants.allow` 도 필요함을 확인.
|
||
- 다음 smoke 에서 `execute_url` permission auto-deny 발생.
|
||
- CCBAC03 실제 xlsx 헤더(`신청인`, `제조국가`, `최초등록일자`, `최종변경일자`)를 기존 API/baseline 헤더(`업체명`, `제조국가명`, `발급일자`)만 요구하던 파서가 거부함.
|
||
- 진단 화면이 `agy.enabled=false`만 보고 `auto+API 키 없음` 또는 `browser` 모드에서도 AGY/Google 로그인을 “필요 없음”으로 표시할 수 있었음.
|
||
- AGY가 처음에는 같은 파일명(`의약품등심사결과공개.xlsx`)을 wrong export 로 보냈다고 판단했으나, parser alias 보강 후 실제 CCBAC03 데이터 9,816건으로 확인됨. 파일명은 사이트가 generic 하게 붙이는 이름이다.
|
||
- 수정:
|
||
- `ScrollFrame` 이 canvas 뿐 아니라 내부 텍스트/카드 위의 wheel 이벤트도 처리하도록 `event.widget` 부모 체인을 검사하는 전역 wheel binding 으로 변경.
|
||
- 상단 안내문을 자연스러운 한국어 2~3문장으로 교체. 예: `먼저 자료 보관소, 리포트 저장 폴더를 확인해 주세요.`
|
||
- `src/dmf_crawler/agy/browser_collect.py` 추가. AGY 프롬프트는 visible/headful Chrome + chrome-devtools MCP 를 명시하고 curl/headless/API 직접 호출과 비밀번호/토큰 prompt 입력을 금지한다.
|
||
- `source.mode` 를 `auto | api | browser` 로 확장. `auto` 에서 API 키가 없으면 AGY headful 수집을 시도하고, `browser` 는 키가 있어도 headful 수집을 강제한다.
|
||
- AGY headful 다운로드는 `.crdownload/.tmp` 없음, xlsx zip/XML 구조, 행 수, sheet XML 파싱을 통과해야 `FetchResult` 로 인정한다.
|
||
- headful 권한 준비 함수 `ensure_headful_mcp_permission()` 추가. 실제 AGY CLI가 읽는 top-level `permissions.allow` 와 `userSettings.globalPermissionGrants.allow` 양쪽에 `mcp(chrome-devtools/*)`, `execute_url(nedrug.mfds.go.kr)`만 좁게 추가한다. `mcp(*)`, `execute_url(*)`, `--dangerously-skip-permissions`는 금지.
|
||
- 프롬프트는 ambiguous 버튼 클릭 대신 visible Chrome 에서 `https://nedrug.mfds.go.kr/pbp/CCBAC03/getExcel` 을 열도록 좁혔다.
|
||
- `prototype_xlsx.read_rows()` 는 CCBAC03 웹 xlsx 헤더 alias(`신청인`, `제조국가`, `최초등록일자`, `최종변경일자`)를 API 표준 RawRecord 필드로 매핑한다. 날짜는 `최종변경일자`를 우선하고 없으면 `발급일자`, `최초등록일자` 순으로 사용한다.
|
||
- `checks.py` 는 AGY 필요 여부를 `agy.enabled`만으로 보지 않고 `source.mode`와 API 키 존재까지 함께 본다. API 모드+AI 요약 꺼짐이면 AGY 불필요, `auto+키 없음`/`browser`면 수집용 AGY 준비를 WARN으로 안내한다.
|
||
- 검증:
|
||
- `pytest tests -q` → **79 passed**
|
||
- 실제 수동 smoke: `./.venv/Scripts/python.exe -m dmf_crawler run --trigger manual --force --skip-agy` → **SUCCESS exit=0**
|
||
- 실측 run_id: `run_20260903_130332`
|
||
- 다운로드: `data/headful_downloads/run_20260903_130332/의약품등심사결과공개.xlsx` (사이트 generic 파일명)
|
||
- 수집/정규화: 9,816건 → 9,816건, persist/report/backup 성공
|
||
- 리포트: `reports/DMF_리포트_2026-09-03.xlsx` (9,903행급)
|
||
- 남은 확인:
|
||
- 전용 Chrome profile Preferences, CDP 포트 sentinel 은 RED backlog 에 남아 있다.
|
||
- 06:00 스케줄러가 실제 로그인 데스크톱 세션에서 headful Chrome 을 안정적으로 띄우는지는 별도 수동 smoke 가 필요하다.
|
||
|
||
**2026-09-03 릴리즈 배포본 생성 완료**
|
||
|
||
사용자 요청: 카카오톡으로 전달할 수 있게 원드라이브 바탕화면에 압축 배포본과 가이드를 만들어 둘 것.
|
||
|
||
산출물:
|
||
|
||
- 폴더: `C:\Users\encep\OneDrive\바탕 화면\DMF_Crawler_v0.1.0_20260903`
|
||
- zip: `C:\Users\encep\OneDrive\바탕 화면\DMF_Crawler_v0.1.0_20260903.zip`
|
||
- SHA256: `C:\Users\encep\OneDrive\바탕 화면\DMF_Crawler_v0.1.0_20260903.sha256.txt`
|
||
- 사용자 첫 안내: `00_먼저_읽어주세요.txt`
|
||
- 검증 요약: `릴리즈_검증.txt`
|
||
|
||
포함:
|
||
|
||
- `bootstrap.cmd`, `README.md`, `pyproject.toml`, `src`, `config`, `scripts`, `prompts`, `docs` 일부, fixture 샘플 리포트 1개.
|
||
|
||
제외 감사 통과:
|
||
|
||
- `.venv`, `data`, `logs`, 실제 `reports`, `backup`, `state`, `tests`, `__pycache__`, `*.pyc`, `*.lnk`, `config.local.toml`, `service_key`, `oauth-token` 없음.
|
||
|
||
검증:
|
||
|
||
- `pytest tests -q` → 79 passed
|
||
- `compileall` → exit 0
|
||
- `dmf_crawler --help` → exit 0
|
||
- 최종 zip contents audit → 필수 파일 누락 없음 / 금지 파일 없음
|
||
- 최종 zip 압축 해제본 `compileall` → exit 0
|
||
- 최종 zip 압축 해제본 `dmf_crawler --help` → exit 0
|
||
- 현재 개발 폴더 `doctor --json` 은 exit 2: Excel 이 `DMF_리포트_2026-09-03.xlsx` 를 열고 있어 `report_writable` CRITICAL. 배포본 결함은 아님. Excel 닫으면 기존처럼 WARN 수준으로 내려갈 것.
|
||
|
||
주의:
|
||
|
||
- zip 안에는 실제 DB/리포트/로그가 없다. 새 사용자 PC는 압축 해제 후 `bootstrap.cmd` 를 먼저 실행해야 한다.
|
||
- fresh PC 에서 `bootstrap.cmd` 전 `doctor` 는 `.venv`가 없어 CRITICAL이 정상이다.
|
||
- 인증키 없이 최신 수집을 원하면 AGY 설치/Google 로그인 후 headful Chrome 경로를 사용한다.
|
||
|
||
**2026-09-03 사용자 실측 UI 회귀 — RED 고정·수정 완료**
|
||
|
||
사용자가 새로 제보한 스크린샷:
|
||
|
||
- `C:\Users\encep\AppData\Local\Temp\pi-clipboard-5df73a57-a813-41be-b025-819e48bd73c2.png`
|
||
|
||
관찰된 문제:
|
||
|
||
- 온보딩/진단 창 하단 footer 버튼들이 **글자 없이 빈 버튼처럼 보임**. 이전에 “하단 버튼 최소 크기” RED를 추가했지만 실제 화면 결함을 충분히 잡지 못했다.
|
||
- 일부 버튼이 여전히 **짜부라지거나 clipped** 된다.
|
||
- “새 버전” 화면에서 보여야 할 버튼/액션이 **아예 안 보이는 경우**가 있다.
|
||
- 현재 테스트 `test_onboarding_footer_buttons_have_readable_minimum_size` 는 단순 `width`/요청 크기만 보므로, 실제 렌더링에서 텍스트가 사라지는 문제를 놓쳤다.
|
||
|
||
TDD 처리:
|
||
|
||
- 스크린샷 상태를 재현하는 fixture/check 결과를 주입해, `refresh()` 이후 실제 버튼 `text`, `winfo_ismapped()`, `winfo_width/height()`, `winfo_reqwidth/height()`, 창 밖 clipping, scroll viewport overflow 를 검사하도록 RED를 강화했다.
|
||
- 추가 RED:
|
||
- `test_onboarding_footer_buttons_render_visible_text_after_refresh`
|
||
- `test_onboarding_footer_buttons_keep_text_when_required_items_exist`
|
||
- `test_onboarding_row_action_buttons_are_not_clipped_inside_scroll_view`
|
||
- `test_onboarding_new_grouped_layout_keeps_primary_actions_visible`
|
||
- RED 실패 확인:
|
||
- footer 버튼 `winfo_ismapped() == 0` 으로 실제 화면에 배치되지 않음.
|
||
- 창 요청 높이 `771px` 이 실제 높이 `700px` 을 넘어 footer가 잘림.
|
||
- 긴 한글/경로 제목에서 scroll 내부 요청 폭 `1745px` 이 viewport `867px` 을 넘어 overflow.
|
||
- 수정:
|
||
- `src/dmf_crawler/gui/app.py` 의 shell layout 을 `pack` 누적 방식에서 grid 기반 고정 footer/가변 scroll 구조로 변경.
|
||
- `SCROLL_VIEW_HEIGHT=300` 으로 1366×768 노트북 예산 안에서 footer를 보존.
|
||
- 긴 행 제목/detail/fix_hint 에 `wraplength` 를 적용해 행 액션 버튼이 viewport 밖으로 밀리지 않게 함.
|
||
- CRITICAL 실패가 있으면 footer [지금 실행] 버튼을 disabled 로 유지.
|
||
- 검증:
|
||
- `pytest tests/test_ux_regressions.py tests/test_gui_design_contracts.py -q` → **14 passed**
|
||
- `pytest tests -q` → **65 passed**
|
||
- 남은 확인: 실제 Windows tkinter 창을 열어 육안 확인은 아직 하지 않았다. 스크린샷 기반 결함은 자동화 RED로 고정됐다.
|
||
|
||
**2026-09-03 API 키 선택 정책 복구**
|
||
|
||
- 사용자 지적에 따라 회귀 수정: **공공데이터포털 인증키는 선택**이다.
|
||
- 키가 없으면 `preflight`에서 `BLOCKED` 하지 않는다. 공식 API `fetch`만 `SKIPPED` 처리하고 `ctx.stale=True`로 마지막 성공 스냅샷 리포트 경로를 사용한다.
|
||
- `doctor`/온보딩에서도 `api_key_present`, `api_key_valid`는 `WARN` 선택 기능이다. 실패해도 [지금 실행]을 막지 않는다.
|
||
- GUI 그룹에서 인증키는 `필수 설정`이 아니라 `선택 기능`으로 이동했다.
|
||
- 추가/수정 RED:
|
||
- `test_run_without_service_key_uses_existing_baseline_instead_of_blocking`
|
||
- `test_public_data_api_key_is_optional_in_doctor`
|
||
- `test_public_data_api_key_is_grouped_as_optional_in_onboarding`
|
||
- `test_public_data_api_key_optional_policy_is_documented_in_ssot`
|
||
- 주의: **키 없는 최신 웹/엑셀 수집 소스는 AGY headful Chrome 경로로 구현·수동 smoke 성공했다.** 다만 06:00 스케줄러에서 로그인 데스크톱 세션/Chrome 프로필/9222 CDP 포트가 항상 준비되는지는 아직 별도 RED·수동 smoke 가 필요하다.
|
||
|
||
**2026-09-03 디자인 업그레이드 완료**
|
||
|
||
- `design.md` 생성: 컨셉은 **감사 가능한 조용한 계기판**. Windows 앱은 `tkinter`/한국어/Fluent식 그룹화, xlsx는 규제 원장/출처/접근성 중심.
|
||
- Windows 앱 개선:
|
||
- 상단 요약 strip: `필수 N` / `권장 N` / `정상 N`.
|
||
- 체크 항목을 `필수 설정` / `선택 기능` / `운영 상태`로 그룹화.
|
||
- `공식 API 인증키와 AI 요약은 선택 기능입니다. 없어도 기존 자료 리포트 생성은 막지 않습니다.` 문구를 화면 구조에 포함.
|
||
- 각 행에 `StatusBadge` + 텍스트 상태(`통과`/`필수 조치`/`권장 조치`) + 최소 폭 action button 적용.
|
||
- xlsx 개선:
|
||
- 워크북 문서 속성 추가: title/subject/author/category/keywords/comments/created.
|
||
- `00_대시보드!A1`에 스크린리더용 목적문 추가.
|
||
- dashboard 내부 링크, 각 데이터 시트의 `← 대시보드` 역링크를 RED로 검사.
|
||
- 외부 링크 문구를 `조회`에서 `원문 조회`로 변경.
|
||
- 집계 내부 링크 문구를 `원장 보기`, 워치리스트 링크를 `변경분 보기`로 변경.
|
||
- 데이터 시트 visible column width cap 검사 추가. `01_오늘변경분` 변경내용 열은 52 → 48로 줄여 긴 값은 wrap 처리.
|
||
- 새/강화 테스트:
|
||
- `tests/test_gui_design_contracts.py`
|
||
- `tests/test_report_e2e_design.py`
|
||
- 검증:
|
||
- `python -m compileall -q src tests` PASS
|
||
- `pytest tests -q` → **58 passed**
|
||
- 실제 9,084건 리포트 재생성:
|
||
- `reports/DMF_리포트_2026-09-02.xlsx`
|
||
- 크기: 1,273,793 bytes
|
||
- OOXML 확인: A1 목적문 존재, dashboard internal links 8개, `원문 조회` 포함, core title 포함, visible max column width <= 48.71.
|
||
- 주의: `reports/~$DMF_리포트_최신.xlsx` 잠금 파일이 있어 `reports/DMF_리포트_최신.xlsx`는 갱신 실패. Excel에서 최신본을 닫고 재실행해야 최신본 복사가 된다.
|
||
|
||
---
|
||
|
||
## 1. 프로젝트가 무엇인가
|
||
|
||
Windows 11 PC 에서 **매일 06:00 에 한국 식약처 원료의약품 등록(DMF) 데이터를 수집**하고, 전일 대비 **신규·변경·취하**를 탐지해, **탭별로 연동된 보기 좋은 xlsx 리포트**를 만든다.
|
||
|
||
AI CLI 는 **Google Antigravity CLI (`agy`)** 를 두 역할로 쓴다. 수집용 AGY는 visible/headful Chrome 을 조작하고, 요약용 AGY는 사용자가 AI 요약을 켰을 때만 headless/non-interactive 로 호출한다. Claude Code 가 아니다.
|
||
**쓰는 사람은 개발자가 아니다.** 더블클릭 한 번으로 설치·설정이 끝나야 하고, 문제가 생기면 창이 떠서 고치는 법을 알려줘야 한다.
|
||
|
||
전체 요구사항: `docs/00-REQUIREMENTS.md` (R1~R8, N1~N8)
|
||
|
||
---
|
||
|
||
## 2. 사용자가 확정한 것 (인터뷰 답변)
|
||
|
||
| 항목 | 사용자 결정 |
|
||
|---|---|
|
||
| **PC 상태** | **매일 다르다.** 켜둘 때도 끌 때도 있음 → 트리거 3개(매일 06:00 + 로그온 시 + 놓친 작업 따라잡기), **중복 실행 방지 잠금 필수** |
|
||
| **관리자 승격** | **설치 시 1회 허용** |
|
||
| **API 키** | **아직 없음.** 발급받은 적 없음. **공공데이터포털 인증키는 선택**이며 없어도 기존 자료 리포트 생성은 막지 않는다. |
|
||
| **수집 방식** | `source.mode=auto`: API 키가 있으면 공식 API, 없으면 AGY headful Chrome 으로 CCBAC03 xlsx 다운로드. `source.mode=browser`: 키가 있어도 headful 강제. `source.mode=api`: 공식 API만 사용하고 키 없으면 기존 성공 자료로 폴백. |
|
||
| **크롤링 방식** | **agy 가 headful Chrome 을 몰아 사람처럼 접근.** 실제 수동 smoke에서 `CCBAC03/getExcel` 다운로드 및 9,816건 정규화 성공. |
|
||
| **브라우저 로그인** | 프로필/세션 유지로 처리한다. 비밀번호를 agy 프롬프트에 넣거나 모델에게 입력시키지 않는다. |
|
||
| **리포트 공유** | 본인이 보되 **공유 옵션도 설정 가능해야 함** |
|
||
| **리포트 첫 화면** | **대시보드** |
|
||
| **워치리스트** | **필요함.** 목록은 나중에 등록 |
|
||
| **알림** | **화면 창 + 메신저 둘 다** |
|
||
|
||
### 아직 사용자에게 못 받은 답
|
||
- [ ] 의약품안전나라 계정을 갖고 있는가? 로그인 시 익명보다 더 보이는 정보가 있는가?
|
||
- [ ] 메신저는 어느 것인가 (디스코드/텔레그램/슬랙/이메일)
|
||
- [ ] 워치리스트 초기 목록
|
||
- [ ] `DMF_현황.xlsx` 를 만든 것이 본인인가 타인인가 (카카오톡으로 받음)
|
||
|
||
---
|
||
|
||
## 3. 실측으로 밝혀진 결정적 사실
|
||
|
||
**이것들은 전부 실제로 실행해서 확인한 것이다. 추측이 아니다.**
|
||
|
||
### 3.1 기존 프로토타입이 이미 있었다
|
||
사용자가 준 `C:\Users\encep\OneDrive\문서\카카오톡 받은 파일\DMF_현황.xlsx` 를 전수 분석했다. 정본: `docs/design/00b-baseline-data-analysis.md`
|
||
|
||
- 시트 3개: 전체(9,084건) / 신규(헤더만) / 갱신이력(`2026-09-02, 9084, 0`)
|
||
- 컬럼 8개 = **API 7필드 + 최초수집일(파생)**. 즉 이미 공식 API 로 수집하고 있었다.
|
||
- **서식이 전혀 없다** — 틀 고정·자동필터·조건부서식·차트가 전부 0. "예쁘게" 요구가 겨냥하는 지점.
|
||
|
||
### 3.2 API 7필드로 신규·변경·취하 전부 탐지 가능하다
|
||
| 사실 | 값 |
|
||
|---|---|
|
||
| API 전체 건수 | **9,084건** |
|
||
| 웹 화면 건수 | **9,840건** |
|
||
| 차이 | **756건** → API 는 **정상 건만** 반환 → **취하 = 레코드 소멸로 판정 가능** |
|
||
| 등록번호 중복 | **0건** → 완전한 자연 키 |
|
||
| 등록번호 앞8자리 vs 발급일자 | **44.5% 불일치** → **`발급일자`는 최종 갱신일로 움직인다 = 변경의 직접 신호** |
|
||
|
||
예: `20050831-33-A-81-08(18)` 의 발급일자가 `2026-08-18`. 괄호가 변경 차수.
|
||
|
||
**놓치는 것**: 연차보고, 변경 사유, 취하 vs 취소 구분. `대상의약품`은 등록번호 포맷에서 파생 가능(`수` 접두어 = 신물질).
|
||
|
||
### 3.3 nedrug 은 봇 차단이 없다
|
||
`curl/8.10.1` UA 그대로 **200 OK, HTML 382,506 bytes 수신**. Cloudflare/Akamai/DataDome 없음. Apache + JSESSIONID + Elevisor for J2EE(WAS 모니터링).
|
||
|
||
**즉 차단 회피 기술은 원래부터 불필요했다.** 브라우저 방식의 근거는 "블락 방지"가 아니라 robots.txt 에 대한 태도와 로그인 세션 대비다.
|
||
|
||
### 3.4 엑셀 다운로드 실측 성공 + 함정 2개
|
||
`POST /pbp/CCBAC03/getExcel` 로 **9,841행 / 1,379,451 bytes 1회 수신 성공**. 요청 2회(세션 GET + 엑셀 POST).
|
||
|
||
- ⚠️ **날짜 파라미터를 비우면** 헤더만 든 3,575 bytes 빈 파일이 온다. 조용한 실패.
|
||
- ⚠️ **받은 xlsx 를 openpyxl 로 열면 0행으로 읽힌다.** `<dimension ref="A1"/>` 때문. **sheet1.xml 직접 파싱 필수.**
|
||
|
||
### 3.5 robots.txt 는 자기모순
|
||
`robots.txt` 는 `User-agent: * / Disallow: /` (26 bytes) 인데, `/bbs/117` HTML 에는 `<meta name="robots" content="index,follow"/>` 가 있다.
|
||
|
||
### 3.6 법적 선
|
||
- **형사**: 대법원 2022.5.12. 2021도1533 — 정보통신망법 §48 등 **전부 무죄 확정**(공개정보 + 보호조치 없음 + 약관상 이용제한은 접근제한 아님)
|
||
- **민사**: 야놀자 사건 10억(부정경쟁방지법 성과도용), 잡코리아 v 사람인 2.5억(저작권법 §93)
|
||
- **두 민사 판결의 공통 결정요소는 "재배포·경쟁 이용"**
|
||
- → **지켜야 할 선: 내부 리포트 전용, 재배포 없음.** 이 선 안이면 리스크 Low
|
||
|
||
### 3.7 agy CLI 실측
|
||
정본: `docs/research/05a-agy-cli-ssot.md` (1,079줄)
|
||
|
||
- v1.1.22, 경로 `%LOCALAPPDATA%\agy\bin\agy.exe`
|
||
- **OAuth 토큰이 키링이 아니라 평문 파일**: `~/.gemini/antigravity-cli/antigravity-oauth-token` → 작업 스케줄러를 **동일 사용자 계정**으로 돌리면 인증 통과. SYSTEM 금지.
|
||
- ⚠️ **`--json-schema` 를 신뢰하면 안 된다.** 실측에서 `structured_output` 이 없었고 `response` 에 JSON 이 4회 반복 + 한국어 산문 혼입, 토큰 3배. **자체 파싱·검증 필수.**
|
||
- ⚠️ **첫 호출 오버헤드 input 28,317 토큰 / 33.7초.** 호출을 묶어야 한다.
|
||
- **chrome-devtools MCP 가 이미 등록·활성화됨** → agy 가 Chrome 제어 가능
|
||
- 배치 체크리스트는 SSOT §16
|
||
|
||
### 3.8 Windows 실행 방식 제약
|
||
- **이 계정(Administrators 미소속)은 S4U 태스크 등록이 거부된다.** Interactive 만 성공. 실측 확인.
|
||
- 사용자가 관리자 승격 1회를 허용했으므로 승격하면 S4U 가능한지는 미확인
|
||
- ⚠️ **BOM 없는 UTF-8 `.ps1` 을 Windows PowerShell 5.1 이 CP949 로 오독해 한글이 깨진다.** 실측 재현. **모든 `.ps1` 은 UTF-8 with BOM 필수.**
|
||
- 콘솔 창을 원리적으로 없애려면 GUI 서브시스템 호스트(`wscript.exe`/`pythonw.exe`)를 최상위에 둬야 한다
|
||
|
||
### 3.9 공공데이터 API 제약
|
||
- 트래픽 10,000 = **호출 횟수**(레코드 아님). 하루 수십 호출이면 1% 미만. **운영계정 전환·활용사례 등록 영원히 불필요**
|
||
- ⚠️ **인증키는 포털 계정당 1개. 재발급하면 기존 키 자동 폐기.** 사용자가 무관한 목적으로 재발급하면 배치가 죽는다
|
||
- ⚠️ **인증키에 사용 기한 있음**(오류코드 31). 24개월설은 미확정 → **하드코딩 금지**, 오류 코드 관측으로 감지
|
||
|
||
---
|
||
|
||
## 4. 확정된 아키텍처
|
||
|
||
정본: `docs/design/01-architecture.md` (1,380줄, ADR 18개)
|
||
|
||
**뼈대**: 후보 3안을 독립 생성 → 3개 렌즈(운영 단순성/실패 내성/진화 가능성)로 심사 → SQLite 이벤트 소싱 안을 뼈대로 채택하고 나머지 두 안의 좋은 부분을 이식.
|
||
|
||
핵심 결정:
|
||
- 저장소: **SQLite 단일 파일 + 이벤트 소싱**(append-only 스냅샷 + 이벤트)
|
||
- 의존성 **3개만**: `httpx`, `XlsxWriter`, `jsonschema`. **pandas·openpyxl·PyYAML 기각**
|
||
- 집계는 **SQL GROUP BY**로. pandas 안 씀
|
||
- xlsx 는 **XlsxWriter 100%, 매일 새 파일 통째 생성**
|
||
- 모든 숫자는 **파이썬이 계산해 값으로** 씀(LibreOffice 재계산 안 함)
|
||
- GUI 는 **stdlib tkinter**(외부 모듈 의존 시 "설치 안 되면 알림도 안 됨" 순환 실패)
|
||
- **배치는 UI 를 절대 안 띄운다.** alert 의도만 기록 → 로그온 세션의 Interactive 에이전트가 15분마다 읽어 창을 띄움
|
||
- **온보딩=복구는 단일 컴포넌트.** `checks.py` 진단 엔진을 CLI(`doctor`)와 GUI(`onboard`)가 공유
|
||
- agy 는 **두 계층**이다. 수집 계층은 `fetch` 단계에서 headful Chrome 으로 CCBAC03 xlsx 를 받을 수 있고, 요약 계층은 diff·저장 후 선택적으로만 호출된다. `enrichment` 테이블을 `events` 와 물리 분리해 R4.4(AI 요약 실패해도 리포트 생성)를 스키마 레벨에서 강제
|
||
- 오케스트레이션은 **순차 STAGES 리스트 + 체크포인트**. DAG 아님
|
||
|
||
### 이번 방향 전환으로 이미 반영된 ADR 보정
|
||
| ADR | 현재 정리 |
|
||
|---|---|
|
||
| ADR-01 | 1차 소스는 공식 API지만, API 키가 없거나 `source.mode=browser`이면 AGY headful Chrome CCBAC03 xlsx 수집을 사용한다. |
|
||
| ADR-02 | 별도 거대 소스 추상화 대신 `source.mode` 분기와 `browser_collect.fetch_all()`을 둔다. |
|
||
| ADR-06 | Playwright/Selenium 직접 의존은 쓰지 않고, 브라우저가 필요할 때 AGY + chrome-devtools MCP + visible Chrome 을 쓴다. |
|
||
| ADR-10 | headful 수집은 로그인 데스크톱 세션이 필요하다. 06:00 스케줄 smoke 는 남아 있다. |
|
||
| ADR-15 | AGY 요약은 diff 후 선택 단계, AGY 수집은 fetch 단계의 별도 경로다. 두 AGY를 혼동하지 않는다. |
|
||
|
||
---
|
||
|
||
## 5. 문서 현황 (총 5.6만 줄)
|
||
|
||
| 문서 | 줄 | 역할 |
|
||
|---|---|---|
|
||
| `00-REQUIREMENTS.md` | 244 | **요구사항 SSOT.** 여기서 시작 |
|
||
| `00-PROJECT-OVERVIEW.md` | 1,051 | 프로젝트 개요·로드맵·리스크·용어집 |
|
||
| `README.md` | 97 | 문서 지도 |
|
||
| `design/00-DATA-SOURCE-DECISION.md` | 386 | 데이터 소스 결정 + API 완전 명세 |
|
||
| `design/00b-baseline-data-analysis.md` | 591 | **기존 프로토타입 전수 분석. 실측 근거의 원천** |
|
||
| `design/01-architecture.md` | 1,380 | **아키텍처 확정안. ADR 18개** |
|
||
| `design/02-data-model.md` | 2,894 | 스키마 DDL·정규화·변경탐지 |
|
||
| `design/03-xlsx-report-spec.md` | 3,554 | 시트별 셀 단위 명세 |
|
||
| `design/04-onboarding-wizard.md` | 4,861 | 온보딩=복구 GUI 명세 |
|
||
| `ops/01-scheduling-and-resilience.md` | 2,379 | 스케줄 등록·재부팅 내성·워치독 |
|
||
| `ops/02-failure-alerting.md` | 4,724 | 알림 등급·문구·버튼 액션 |
|
||
| `ops/03-api-usage-policy.md` | 1,796 | API 제약·오류 코드·키 만료 대응 |
|
||
| `ops/04-official-data-request-channels.md` | 401 | 필드 추가 공식 요청 절차 + 문구 초안 |
|
||
| `research/01~10` | 30,018 | 조사 정본 10종 |
|
||
| `research/_raw/*.raw.md` | — | 원본 조사 덤프 8개(1.8MB). WebFetch 428건 + WebSearch 224건 결과 보존 |
|
||
|
||
**⚠️ WebSearch 예산이 이 세션에서 소진됐다(200/200).** 다음 세션에서는 다시 쓸 수 있을 것이나, 안 되면 `https://www.bing.com/search?q=...` 를 WebFetch 하는 우회를 쓴다.
|
||
|
||
---
|
||
|
||
## 6. 진행 중인 작업 / 남은 검증
|
||
|
||
- AGY headful CCBAC03 xlsx 수집은 구현했고, 실제 수동 smoke 에서 9,816건 수집·정규화·저장·리포트 생성까지 성공했다.
|
||
- 남은 핵심은 **스케줄러 환경 검증**이다. 06:00 태스크가 로그인 데스크톱 세션에서 visible Chrome, chrome-devtools MCP, `execute_url(nedrug.mfds.go.kr)` 권한, 다운로드 폴더를 안정적으로 준비하는지 확인해야 한다.
|
||
- 전용 Chrome profile Preferences, CDP 포트 sentinel, headful 실패 시 온보딩 복구 버튼은 backlog 다.
|
||
|
||
---
|
||
|
||
## 7. 코드 현황
|
||
|
||
주요 모듈은 현재 존재하며, 전체 smoke 를 통과했다.
|
||
|
||
- CLI/pipeline/storage/repo/report/gui/notify/agy 모듈 구현됨.
|
||
- `src/dmf_crawler/agy/browser_collect.py`: 수집용 AGY headful Chrome 경로.
|
||
- `src/dmf_crawler/importers/prototype_xlsx.py`: 기존 baseline xlsx + CCBAC03 웹 xlsx 헤더 alias 파싱.
|
||
- `tests/`: 79개 통과.
|
||
|
||
검증된 명령:
|
||
|
||
```powershell
|
||
.\.venv\Scripts\python.exe -m compileall -q src tests
|
||
.\.venv\Scripts\python.exe -m dmf_crawler --help
|
||
.\.venv\Scripts\python.exe -m dmf_crawler doctor --json
|
||
.\.venv\Scripts\python.exe -m pytest tests -q
|
||
.\.venv\Scripts\python.exe -m dmf_crawler run --trigger manual --force --skip-agy
|
||
```
|
||
|
||
---
|
||
|
||
## 8. 다음 세션이 할 일 (우선순위 순)
|
||
|
||
1. 스케줄러에서 headful Chrome 이 실제 로그인 세션에 뜨는지 수동 smoke 한다.
|
||
2. Chrome profile/다운로드 폴더/CDP 9222 sentinel RED를 추가하고 구현한다.
|
||
3. headful 실패 원인별 온보딩 문구와 복구 버튼을 다듬는다. 특히 `mcp`/`execute_url` 권한 부족은 사용자가 바로 고칠 수 있어야 한다.
|
||
4. 실제 리포트를 Excel에서 열어 디자인/내용을 육안 감사하고, 깨지는 부분을 RED로 추가한다.
|
||
5. 사용자에게 미해결 질문 확인: 메신저 종류, 워치리스트 초기 목록, 의약품안전나라 로그인 필요 여부.
|
||
|
||
---
|
||
|
||
## 9. 반드시 지킬 것 (함정 모음)
|
||
|
||
- **`.ps1` 은 UTF-8 with BOM.** 아니면 한글 깨짐
|
||
- **`openpyxl` 로 nedrug xlsx 를 열지 마라.** 0행으로 읽힌다. sheet1.xml 직접 파싱
|
||
- **agy `--json-schema` 결과를 믿지 마라.** 자체 JSON 추출 + jsonschema 검증
|
||
- **agy 는 절대 경로로 호출**, `AGY_CLI_DISABLE_AUTO_UPDATE=true`, `--disable-slash-commands`, stdout/stderr 분리
|
||
- **작업 스케줄러는 사용자 계정으로.** SYSTEM 은 agy 토큰에 접근 못 함
|
||
- **수집 완결성 검증 없이 diff 하지 마라.** 전건 취하 오탐이 최악의 실패 모드. 안전장치 5종 + 취하 비율 임계
|
||
- **비밀번호를 agy 프롬프트에 넣지 마라.** 클라우드 모델로 전송된다. 브라우저 로그인은 프로필 세션 유지 또는 CDP 직접 입력으로
|
||
- **API 키·OAuth 토큰을 로그·문서·저장소에 남기지 마라**
|
||
- **재배포하지 마라.** 법적 선이 거기 있다
|
||
- **여러 에이전트에게 같은 파일을 맡기지 마라.** 이 세션에서 실제로 충돌이 났다
|
||
|
||
---
|
||
|
||
## 10. 이 세션에서 있었던 일 (요약)
|
||
|
||
1. 첫 리서치 워크플로우 8축이 사용자 중간 메시지로 인터럽트됨 → **트랜스크립트에서 전량 복구**해 `docs/research/_raw/` 에 보존
|
||
2. 그 원본을 opus 로 정제해 `research/01~10` 생성
|
||
3. **robots.txt 전면 금지 발견** → 공식 API 로 방향 전환
|
||
4. **공식 Open API 발견** → 우회 불필요 판단
|
||
5. 아키텍처 3안 → 3렌즈 심사 → 확정
|
||
6. **사용자가 `DMF_현황.xlsx` 제공** → 프로토타입 존재 확인, API 7필드 충분성 입증
|
||
7. 온보딩 마법사·API 제약·공식 요청 채널 설계
|
||
8. **사용자가 agy headful 브라우저 방식 요구** → 실현성 실측 착수
|
||
9. 사용자 인터뷰로 운영 조건 확정
|
||
10. **구현 착수** (진행 중)
|
||
|
||
---
|
||
|
||
## 11. 새로 추가된 작업 원칙 — TDD/RED/디자인 감사
|
||
|
||
사용자가 2026-09-03에 확정한 새 원칙:
|
||
|
||
- **TDD 를 최상위 개발 원칙으로 둔다. No RED, No Code.**
|
||
- 기능뿐 아니라 **xlsx 리포트와 tkinter GUI 디자인도 RED 로 감사**한다.
|
||
- 디자인 RED 는 중앙 정렬 남용, UX 붕괴, 시각적 치우침, 안 보이는 폰트, overflow, flex/grid 오류, input padding, 아이콘-텍스트 vertical align, 폼 라벨, 실제 텍스트 입력/버튼 클릭/복합 시나리오까지 잡는다.
|
||
- RED 는 계속 늘리기만 하지 않고, 사용자 유즈케이스 기준으로 통폐합·삭제·강화한다.
|
||
|
||
새 정본:
|
||
|
||
- `docs/research/11-tdd-red-and-design-audit-theory.md` — Kent Beck/GDS/Google/Playwright/Testing Library/WCAG/NN/g/Vercel/LLM-TDD 논문 근거 아카이브
|
||
- `docs/design/07-tdd-red-system.md` — 프로젝트 적용 SSOT
|
||
- `AGENTS.md` — AI agent 실무 지침
|
||
- `CLAUDE.md` — Claude Code 포인터
|
||
|
||
**이번 세션 완료**
|
||
|
||
- R0 계약 RED 를 `tests/test_project_contracts.py` 로 코드화했다.
|
||
- 첫 RED 실패는 기대대로 `dmf_crawler.gui.app`, `dmf_crawler.notify.pump` 누락이었다.
|
||
- 최소 구현으로 `src/dmf_crawler/gui/app.py`, `src/dmf_crawler/notify/pump.py` 를 추가했다.
|
||
- 검증 결과:
|
||
- `python -m compileall -q src tests` 통과
|
||
- 전체 import scan 통과
|
||
- `python -m dmf_crawler --help` 통과
|
||
- `python -m dmf_crawler doctor --json` 은 API 키 없음으로 exit 2 + JSON 출력(정상)
|
||
- `pytest tests -q` → 44 passed
|
||
|
||
**추가 사용자 제보 UX 결함 수정(2026-09-03 04시대)**
|
||
|
||
사용자 실행 로그/스크린샷으로 확인된 문제:
|
||
|
||
- `cp949` 콘솔에서 `—` 문자를 출력하다가 `UnicodeEncodeError` 로 CLI 가 다시 죽음.
|
||
- Google 로그인/agy 가 기본 온보딩에서 필수처럼 보임. 사용자가 “Google 로그인 불가, 애당초 필요하면 물어봐야 한다”고 지적.
|
||
- 온보딩 하단 버튼 텍스트가 세로로 찌그러져 읽기 어려움.
|
||
|
||
TDD 처리:
|
||
|
||
- `tests/test_ux_regressions.py` 추가.
|
||
- `test_cli_result_output_survives_cp949_console`
|
||
- `test_doctor_json_survives_cp949_console`
|
||
- `test_agy_google_login_is_optional_when_ai_disabled`
|
||
- `test_google_login_optional_policy_is_documented_in_ssot`
|
||
- `test_onboarding_footer_buttons_have_readable_minimum_size`
|
||
- RED 실패 확인 후 수정:
|
||
- `src/dmf_crawler/cli.py`: 모든 사용자 출력/JSON 출력에 cp949 안전 `_safe_print` 적용, em dash 구분자 제거.
|
||
- `src/dmf_crawler/checks.py`: `agy.enabled=false` 면 agy 설치/Google 로그인 체크를 OK + `fix_action=None` 으로 처리.
|
||
- `src/dmf_crawler/config.py`, `config/config.toml`: AI 요약 기본값 false. Google 로그인은 사용자가 AI 요약을 켠 뒤에만.
|
||
- `src/dmf_crawler/gui/widgets.py`, `src/dmf_crawler/gui/app.py`: 버튼 padding/최소 width 토큰 추가.
|
||
- `docs/00-REQUIREMENTS.md`, `docs/design/04-onboarding-wizard.md`, `AGENTS.md`: **Google 로그인은 선택** 정책 반영.
|
||
- 검증 결과:
|
||
- `pytest tests/test_ux_regressions.py -q` → 5 passed
|
||
- `pytest tests -q` → 54 passed
|
||
- `compileall` 통과
|
||
- 전체 import scan 통과
|
||
- `doctor --json` cp949 출력에서 `UnicodeEncodeError` 없음(exit 2는 API 키 없음으로 정상)
|
||
|
||
**xlsx E2E 산출물 결함 수정(사용자 지적: “유의미한 xlsx를 단 한번도 배출한 적 없음”)**
|
||
|
||
사용자 지적에 따라 완료 기준을 “파일 존재”가 아니라 **seeded DB → 실제 xlsx 생성 → zip/XML 디자인 감사 통과**로 올렸다.
|
||
|
||
TDD 처리:
|
||
|
||
- `tests/test_report_e2e_design.py` 추가.
|
||
- 기준선 2건 + 둘째 날 신규/변경/취하/워치리스트/AI 코멘트 fixture 를 실제 SQLite DB 에 적재.
|
||
- `build_report()` 로 실제 `.xlsx` 생성.
|
||
- zip/XML 로 검사: 파일 구조, 시트 순서, sharedStrings 내용, freeze panes, filter/table, 조건부서식 수, 탭 색, Excel table 이름.
|
||
- RED 실패 1: `report.data` 가 `v_current_records.ingredient_key` 를 읽지만 DB view 에 컬럼 없음.
|
||
- 수정: `src/dmf_crawler/storage/migrations/0004_report_views.sql` 추가. 기존 0001 checksum 을 깨지 않고 view 재생성.
|
||
- RED 실패 2: `05_워치리스트` 시트에 필터/표 없음.
|
||
- 수정: `src/dmf_crawler/report/sheets/s05_watchlist.py` 에 `T_WATCH` Excel table 추가.
|
||
- 과특정 RED 1건 정정: 출처 문구의 OOXML/문장 순서 정확 일치 대신 `출처:` + `식품의약품안전처` 포함으로 SSOT 정합화.
|
||
- 검증 결과:
|
||
- `pytest tests/test_report_e2e_design.py -q` → 1 passed
|
||
- `pytest tests -q` → 55 passed
|
||
- `compileall` 통과
|
||
- 사용자 확인용 SAMPLE 산출물 생성:
|
||
- `reports/sample/DMF_리포트_SAMPLE_2026-09-04.xlsx`
|
||
- 크기: 약 48 KB
|
||
- 시트: `00_대시보드`, `01_오늘변경분`, `02_전체현황`, `03_성분별`, `04_업체별`, `05_워치리스트`, `06_추이`, `99_메타`
|
||
- **주의: fixture 기반 샘플이며 실제 식약처 데이터가 아니다. 현재 실제 데이터 리포트는 공식 API 키 또는 AGY headful CCBAC03 xlsx 수집으로 생성 가능하다.**
|
||
|
||
**실제 기존 데이터 import 완료(사용자 지적: “기존 데이터는 다 어디 갔어”)**
|
||
|
||
확인 결과:
|
||
|
||
- 기존 원본 파일은 살아 있음:
|
||
- `C:\Users\encep\OneDrive\문서\카카오톡 받은 파일\DMF_현황.xlsx`
|
||
- 크기: 757,331 bytes
|
||
- `전체` 시트: `A1:H9085` = 헤더 1 + 데이터 **9,084건**
|
||
- 이전까지 프로젝트 DB 는 비어 있었음:
|
||
- `records=0`, `snapshots=0`, `events=0`, `fetch_stats=0`
|
||
- API 키 없음으로 `BLOCKED` 실행 기록만 존재
|
||
|
||
TDD 처리:
|
||
|
||
- `tests/test_import_prototype_xlsx.py` 추가.
|
||
- openpyxl 없이 zip/XML 로 기존 프로토타입 xlsx 구조를 읽는 importer RED.
|
||
- 한글 헤더(`등록번호`, `성분명`, `업체명`, `제조소명`, `제조소소재지`, `제조국가명`, `발급일자`)를 API 표준 컬럼으로 매핑.
|
||
- baseline DB 적재 + 실제 새 xlsx 리포트 생성까지 검사.
|
||
- RED 실패: `dmf_crawler.importers` 모듈 없음.
|
||
- 수정:
|
||
- `src/dmf_crawler/importers/__init__.py`
|
||
- `src/dmf_crawler/importers/prototype_xlsx.py`
|
||
|
||
실제 기존 파일 적용 결과:
|
||
|
||
- 실행:
|
||
- 원본: `C:\Users\encep\OneDrive\문서\카카오톡 받은 파일\DMF_현황.xlsx`
|
||
- run_id: `import_prototype_20260902`
|
||
- run_date: `2026-09-02`
|
||
- DB:
|
||
- `schema_version=4`
|
||
- `record_versions=9084`
|
||
- `records=9084`
|
||
- `snapshots=9084`
|
||
- `fetch_stats=1`
|
||
- `quality_checks=7`
|
||
- `events=0` (**기준선 수립일이므로 정상**)
|
||
- 실제 리포트:
|
||
- `reports/DMF_리포트_2026-09-02.xlsx`
|
||
- `reports/DMF_리포트_최신.xlsx`
|
||
- 크기: 1,273,258 bytes
|
||
- 포함 시트: `00_대시보드`, `01_오늘변경분`, `02_전체현황`, `03_성분별`, `04_업체별`, `06_추이`, `99_메타`
|
||
- 원본 문자열 검증: `프레가발린`, `젬시타빈염산염`, `출처:`, `식품의약품안전처`, `기준선` 포함 확인
|
||
- 워치리스트 시트는 현재 watchlist DB 가 0건이라 생성되지 않음(조건부 생성 설계)
|
||
- 검증:
|
||
- `compileall` 통과
|
||
- `pytest tests -q` → 56 passed
|
||
|
||
**다음 구현 작업은 실제 리포트 파일을 사람이 열어본 뒤 디자인 결함을 스크린샷/RED로 추가한다. 그 다음 API 키 입력 후 실제 다음 수집을 수행해 9,084 기준선 대비 신규/변경/취하 이벤트를 만들어야 한다.**
|
||
|
||
---
|
||
|
||
*이 문서는 세션 간 인수인계용이다. 프로젝트 정본은 `docs/00-REQUIREMENTS.md`, `docs/design/01-architecture.md`, `docs/design/07-tdd-red-system.md` 다.*
|