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 문서 지도 갱신
This commit is contained in:
commit
56a6e2da93
159 changed files with 145825 additions and 0 deletions
386
docs/design/00-DATA-SOURCE-DECISION.md
Normal file
386
docs/design/00-DATA-SOURCE-DECISION.md
Normal file
|
|
@ -0,0 +1,386 @@
|
|||
# 데이터 소스 결정 — 크롤링에서 공식 Open API 로
|
||||
|
||||
> **이 문서의 역할**: 이 프로젝트가 데이터를 **어디서 어떻게 가져올지**를 확정한다. 이 결정이 아키텍처·법적 리스크·운영 비용 전부를 바꾸므로, 다른 모든 설계 문서보다 상위에 있다.
|
||||
|
||||
**결정일**: 2026-09-02
|
||||
**결정 상태**: ✅ 확정 — **2026-09-02 실측으로 검증 완료**
|
||||
**영향 범위**: 아키텍처, 봇 차단 대응, 법적 검토, agy 역할 정의, 운영 복잡도 전부
|
||||
|
||||
> ### ✅ 실측 검증 완료 (2026-09-02)
|
||||
>
|
||||
> 이 결정을 내릴 당시 남아 있던 최대 불확실성은 **"API 7필드만으로 신규·변경·취하를 탐지할 수 있는가"** 였다.
|
||||
> 이후 사용자가 이미 운영 중이던 실제 산출물 `DMF_현황.xlsx`(9,084건) 를 전수 분석해 **세 가지 모두 탐지 가능함을 확인**했다.
|
||||
>
|
||||
> | 확인된 사실 | 값 |
|
||||
> |---|---|
|
||||
> | API 전체 건수 | **9,084건** (웹 화면 9,840건과 **756건 차이** → API 는 정상 건만 반환) |
|
||||
> | 등록번호 중복 | **0건** → 완전한 자연 키 |
|
||||
> | 등록번호 앞 8자리 vs `발급일자` 불일치 | **44.5%** → `발급일자`는 **최종 갱신일**로 움직인다 = 변경의 직접 신호 |
|
||||
>
|
||||
> 상세는 **[`00b-baseline-data-analysis.md`](./00b-baseline-data-analysis.md)** 참조. 이 문서의 §7.1 판정 규칙과 부록 B 는 그 결과로 갱신되었다.
|
||||
|
||||
---
|
||||
|
||||
## 0. 한눈에 보기
|
||||
|
||||
- 의약품안전나라(`nedrug.mfds.go.kr`)의 `robots.txt` 는 **`User-agent: * / Disallow: /`** — 전 경로 자동화 접근 금지다.
|
||||
- 그런데 **식약처가 동일한 DMF 데이터를 공공데이터포털에 공식 Open API 로 공개**하고 있다. 무료, 자동승인, 이용허락범위 제한 없음.
|
||||
- 즉 식약처의 메시지는 모순이 아니라 한 쌍이다: **"웹 화면을 긁지 말고 API 를 쓰라."**
|
||||
- 따라서 이 프로젝트는 **HTML 크롤링을 하지 않는다.** 공식 API 를 정본 소스로 삼는다.
|
||||
- 이 전환으로 봇 차단 대응, 셀렉터 유지보수, 법적 리스크, 브라우저 자동화 의존성이 **전부 사라진다.** 우회 기법을 설계하는 것보다 이 길이 모든 축에서 우월하다.
|
||||
- API 는 "현황(스냅샷)"만 준다. 공고 이벤트(신규/변경/취하)는 **매일 전량 스냅샷을 받아 자체 diff 로 계산**한다. 공고문을 파싱하는 것보다 오히려 정확하고 누락이 없다.
|
||||
- `agy` 는 차단 우회 도구가 아니라 **데이터 해석·요약·품질 관리 도구**로 재정의한다. 역할은 5절 참조.
|
||||
|
||||
---
|
||||
|
||||
## 1. 목차
|
||||
|
||||
1. [robots.txt 실측](#2-robotstxt-실측)
|
||||
2. [공식 Open API 발견](#3-공식-open-api-발견)
|
||||
3. [API 완전 명세](#4-api-완전-명세)
|
||||
4. [왜 우회가 아니라 API 인가](#5-왜-우회가-아니라-api-인가)
|
||||
5. [agy 역할 재정의](#6-agy-역할-재정의)
|
||||
6. [API 로 커버되지 않는 것과 대응](#7-api-로-커버되지-않는-것과-대응)
|
||||
7. [보조·확장 소스](#8-보조확장-소스)
|
||||
8. [발급 절차](#9-serviceKey-발급-절차)
|
||||
9. [이 결정이 무효화하는 기존 설계](#10-이-결정이-무효화하는-기존-설계)
|
||||
10. [부록 A. 출처](#부록-a-출처)
|
||||
11. [부록 B. 미해결 / 실측 필요](#부록-b-미해결--실측-필요)
|
||||
|
||||
---
|
||||
|
||||
## 2. robots.txt 실측
|
||||
|
||||
`https://nedrug.mfds.go.kr/robots.txt` 를 직접 열어 확인한 전문:
|
||||
|
||||
```
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
```
|
||||
|
||||
**해석**: 모든 사용자 에이전트에 대해 전 경로 크롤링을 금지한다. 예외 경로도, `Crawl-delay` 도, 사이트맵도 없다. 가장 강한 형태의 거부다.
|
||||
|
||||
**법적 지위**: robots.txt 자체는 법률이 아니라 관례(RFC 9309 는 표준 규격일 뿐 강제력이 아니다). 그러나 실무적으로 다음을 의미한다.
|
||||
|
||||
| 관점 | 의미 |
|
||||
|---|---|
|
||||
| 사이트 운영자의 의사 | 자동화 수집을 원하지 않는다는 **명시적 의사표시** |
|
||||
| 분쟁 발생 시 | "명시적 거부를 알고도 접근했다"는 사실은 불리한 정황이 된다 |
|
||||
| 기술적 현실 | 명시적 거부는 통상 서버 측 차단 로직과 함께 온다. 차단·IP 밴 위험이 상시 존재한다 |
|
||||
| 지속 가능성 | 우회는 상대의 방어 갱신마다 깨진다. 유지보수가 영구히 따라붙는다 |
|
||||
|
||||
또한 `nedrug.mfds.go.kr/pbp/CCBGE01` 등 추정 경로는 **HTTP 404** 를 반환했고, 메인 페이지에서도 DMF 관련 링크의 href 를 확인하지 못했다. 즉 **URL 구조를 확정하는 것부터가 이미 비용**이다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 공식 Open API 발견
|
||||
|
||||
공공데이터포털(`data.go.kr`)에서 "DMF" 로 검색한 결과 **정확히 이 프로젝트가 필요로 하는 데이터셋 2건**이 공개돼 있다.
|
||||
|
||||
| # | 제목 | 유형 | 경로 | 수정일 | 조회수 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | **식품의약품안전처_원료의약품등록(DMF)현황** | 오픈API (XML/JSON) | `/data/15057075/openapi.do` | 2025-09-19 | 31,330 |
|
||||
| 2 | DMF현황 | 연계데이터 (JSON+XML) | `/data/2095/linkedData.do` | 2026-07-08 | 4 |
|
||||
|
||||
2번 연계데이터의 엔드포인트는 `http://data.mfds.go.kr/openapi/MdcDmfInfoService/getMdcDmfList` 로, 1번의 구버전으로 보인다. **1번을 정본으로 채택한다** (HTTPS, 최신 버전, 상세 명세 제공, 활용신청 548건으로 검증됨).
|
||||
|
||||
---
|
||||
|
||||
## 4. API 완전 명세
|
||||
|
||||
### 4.1 기본 정보
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| API 명칭 | 식품의약품안전처_원료의약품등록(DMF)현황 |
|
||||
| 제공기관 | 식품의약품안전처 |
|
||||
| 관리부서 | 데이터혁신기획팀 |
|
||||
| API 유형 | REST |
|
||||
| 기본 데이터 포맷 | XML (`type=json` 으로 JSON 가능) |
|
||||
| 서비스 URL | `https://apis.data.go.kr/1471000/MdcDmfInfoService01` |
|
||||
| **요청 주소** | `https://apis.data.go.kr/1471000/MdcDmfInfoService01/getMdcDmfList01` |
|
||||
| 오퍼레이션 | 1개 — "DMF현황 조회하기" |
|
||||
| 등록일 | 2019-09-27 |
|
||||
| 수정일 | 2025-09-19 |
|
||||
| 활용신청 수 | 548 |
|
||||
| **이용허락범위** | **제한 없음** |
|
||||
| 비용 | 무료 |
|
||||
| 심의 | 개발단계 **자동승인** / 운영단계 **자동승인** |
|
||||
| 트래픽 | 개발계정 **10,000건/일**. 운영계정은 활용사례 등록 후 증량 신청 가능 |
|
||||
| 참고문서 | `IROS_76_원료의약품(DMF)현황_v1.1.docx` (포털에서 다운로드) |
|
||||
|
||||
오퍼레이션 설명 원문:
|
||||
> "등록번호, 발급일자, 업체명, 성분명, 제조소명 등의 원료의약품 현황 정보를 조회"
|
||||
|
||||
### 4.2 요청 파라미터
|
||||
|
||||
| 항목명(국문) | 영문명 | 크기 | 필수 | 샘플 | 설명 |
|
||||
|---|---|---|---|---|---|
|
||||
| 인증키 | `serviceKey` | 100 | **필수** | 인증키 | 공공데이터포털에서 발급받은 인증키. **URL Encode 필요** |
|
||||
| 업체명 | `entp_name` | 200 | 선택 | 업체명 | 검색 조건 |
|
||||
| 성분명 | `ingr_kor_name` | 3000 | 선택 | 성분명 | 검색 조건 |
|
||||
| 페이지 번호 | `pageNo` | 5 | 선택 | 1 | 페이지번호 |
|
||||
| 한 페이지 결과 수 | `numOfRows` | 3 | 선택 | 3 | 한 페이지 결과 수 |
|
||||
| 데이터포맷 | `type` | 4 | 선택 | xml | 응답데이터 형식(xml/json), 기본값 xml |
|
||||
|
||||
> ⚠️ `numOfRows` 의 크기가 **3자리**로 명세돼 있다. 최대 999 일 가능성이 높다. 전량 수집 시 호출 횟수 계산에 영향을 준다. → 부록 B.
|
||||
|
||||
### 4.3 응답 필드
|
||||
|
||||
**공통 헤더**
|
||||
|
||||
| 항목명 | 영문명 | 크기 | 필수 | 샘플 |
|
||||
|---|---|---|---|---|
|
||||
| 결과코드 | `resultCode` | 4 | 필수 | `00` |
|
||||
| 결과메시지 | `resultMsg` | 50 | 필수 | `NORMAL SERVICE.` |
|
||||
| 한 페이지 결과 수 | `numOfRows` | 3 | 선택 | 3 |
|
||||
| 페이지 번호 | `pageNo` | 5 | 선택 | 1 |
|
||||
| 전체 결과 수 | `totalCount` | 7 | 선택 | 1 |
|
||||
|
||||
**데이터 항목** — 이것이 이 프로젝트의 원천 레코드다.
|
||||
|
||||
| 항목명(국문) | 영문명 | 크기 | 샘플 |
|
||||
|---|---|---|---|
|
||||
| 등록번호 | `DMF_PERMIT_NO` | 200 | `20121228-168-I-169-04` |
|
||||
| 성분명 | `INGR_KOR_NAME` | 3000 | `포르모테롤푸마르산염수화물` |
|
||||
| 업체명 | `ENTP_NAME` | 200 | `(주)대웅제약` |
|
||||
| 제조소명 | `MNFCTR_NAME` | 150 | `SICOR SOCIETA'ITALIANA CORTICOSTER OIDO S.R.L.` |
|
||||
| 제조소 소재지 | `MNFCTR_PLACE` | 2000 | `Rho(MI) - Via Terrazzano, 77, Italy` |
|
||||
| 제조국가명 | `MANUF_COUNTRY_CODE_NM` | 1000 | `이탈리아,스위스` |
|
||||
| 발급일자 | `DMF_PERMIT_DATE` | 30 | `2015-02-26` |
|
||||
|
||||
**필드 관찰**:
|
||||
- `DMF_PERMIT_NO` 샘플 `20121228-168-I-169-04` 는 `발급일자8자리-숫자-알파벳-숫자-일련번호` 구조다. 앞 8자리가 최초 수리일로 보인다. 상세 해부는 `docs/research/01-dmf-domain-and-sources.md` 참조.
|
||||
- `MANUF_COUNTRY_CODE_NM` 이 `이탈리아,스위스` 처럼 **콤마 다중값**이다. 정규화가 필요하다.
|
||||
- `MNFCTR_NAME` 샘플에 `CORTICOSTER OIDO` 처럼 **공백이 잘못 들어간 원본 오류**가 보인다. 원본 데이터 품질이 완벽하지 않다는 신호다. 표기 정규화 계층이 필요하다.
|
||||
- 상태 필드(신규/변경/취하)가 **없다.** 따라서 변경 탐지는 우리가 계산해야 한다 (7절).
|
||||
|
||||
### 4.4 호출 예시
|
||||
|
||||
```
|
||||
https://apis.data.go.kr/1471000/MdcDmfInfoService01/getMdcDmfList01
|
||||
?serviceKey=<URL_ENCODED_KEY>
|
||||
&pageNo=1
|
||||
&numOfRows=100
|
||||
&type=json
|
||||
```
|
||||
|
||||
인증 없이 호출하면 **HTTP 403** 을 반환한다 (실측). 즉 엔드포인트는 살아 있고 인증을 강제한다.
|
||||
|
||||
### 4.5 전량 수집 전략
|
||||
|
||||
1. `numOfRows=1`, `pageNo=1` 로 한 번 호출해 `totalCount` 를 얻는다.
|
||||
2. `ceil(totalCount / numOfRows)` 만큼 페이지를 순회한다.
|
||||
3. 페이지 간 지연을 둔다 (0.5~1초). 하루 10,000 호출 한도 안에서 여유롭다.
|
||||
4. 마지막 페이지까지 받은 뒤 **수집 건수 == totalCount** 를 검증한다. 불일치하면 그 실행은 **불완전 수집**으로 표시하고, 취하 판정을 하지 않는다 (7.2 안전장치).
|
||||
|
||||
---
|
||||
|
||||
## 5. 왜 우회가 아니라 API 인가
|
||||
|
||||
원래 계획은 nedrug 화면을 크롤링하고, 차단되면 `agy` 로 에이전틱하게 접근하는 것이었다. 공식 API 를 발견한 지금, 두 경로를 정면으로 비교한다.
|
||||
|
||||
| 축 | HTML 크롤링 (+우회) | 공식 Open API |
|
||||
|---|---|---|
|
||||
| robots.txt | 전면 금지를 무릅씀 | **무관. 애초에 공개용 채널** |
|
||||
| 이용 허락 | 불명확 | **"이용허락범위 제한 없음"** 명시 |
|
||||
| 데이터 구조 | HTML DOM. 사이트 개편마다 파괴 | **고정 스키마 7필드.** 버전 URL 로 관리 |
|
||||
| 차단 위험 | 상시. IP 밴 시 복구 어려움 | 없음. 인증키 기반 정상 트래픽 |
|
||||
| 필요 기술 | 브라우저 자동화, 스텔스, TLS 지문, 프록시 | **HTTP GET 한 줄** |
|
||||
| 의존성 | Playwright + Chromium (수백 MB) | `httpx` 하나 |
|
||||
| 실행 시간 | 브라우저 기동 포함 수십 초~분 | 수 초 |
|
||||
| 유지보수 | 셀렉터 깨짐 상시 대응 | 스키마 변경 시에만 |
|
||||
| 법적 리스크 | 데이터베이스제작자 권리·정보통신망법 논쟁 소지 | **없음** |
|
||||
| 재현성 | 낮음 (렌더링 타이밍 의존) | 높음 (결정론적) |
|
||||
| 무인 운영 적합성 | 낮음 | **높음** |
|
||||
|
||||
**결론**: 우회 기법은 이 프로젝트에서 **더 어렵고, 더 취약하고, 더 위험하고, 결과물도 더 나쁘다.** 공식 API 채택은 타협이 아니라 모든 축에서의 개선이다.
|
||||
|
||||
> 이 프로젝트는 `robots.txt` 를 우회하는 기법을 설계하거나 구현하지 않는다. 봇 차단 관련 조사(`docs/research/04-anti-bot-and-legal.md`)는 **폐기하지 않고 보존**한다. 이유는 두 가지다. 첫째, 향후 API 가 없는 보조 소스를 붙일 때 "정중한 접근" 규칙(요청 빈도, UA 명시, 백오프, 조건부 요청)이 그대로 필요하다. 둘째, 우리가 왜 이 길을 택하지 않았는지의 근거 자료다.
|
||||
|
||||
---
|
||||
|
||||
## 6. agy 역할 재정의
|
||||
|
||||
`agy` 는 원래 "차단을 에이전틱하게 뚫는" 역할로 구상됐다. 그 필요가 사라진 지금, **더 가치 있는 자리로 옮긴다.** 데이터를 가져오는 일은 결정론적 코드가 하고, `agy` 는 **사람이 해야 할 판단을 돕는 일**을 한다.
|
||||
|
||||
| # | 역할 | 입력 | 출력 | 실패 시 |
|
||||
|---|---|---|---|---|
|
||||
| **A1** | 일일 변경 브리핑 | 오늘의 신규/변경/취하 레코드 JSON | 실무자용 한국어 요약 + 중요도 태깅 | 리포트에 요약 없이 표만 |
|
||||
| **A2** | 성분명 정규화·매칭 | 표기가 흔들리는 성분명 목록 | 표준명 매핑 후보 + 신뢰도 | 규칙 기반 정규화만 적용 |
|
||||
| **A3** | 이상 신호 해석 | 건수 급변·0건·중복 급증 등 지표 | 원인 가설과 조치 제안 | 임계값 경보만 발송 |
|
||||
| **A4** | 워치리스트 매칭 보조 | 관심 성분·업체 목록 + 오늘 데이터 | 유사 매칭 후보 | 완전 일치만 |
|
||||
| **A5** | 제조소·국가 표기 정리 | `MANUF_COUNTRY_CODE_NM` 다중값, 오탈자 있는 제조소명 | 정규화된 국가 리스트, 정제된 제조소명 | 원문 그대로 |
|
||||
| **A6** | 주간 트렌드 코멘터리 | 최근 N주 집계 | 서술형 트렌드 요약 | 생략 |
|
||||
| **A7** | API 스키마 변화 대응 | 예상과 다른 응답 구조 | 변경점 진단과 매핑 제안 (**자동 반영 금지, 사람 승인 필수**) | 실행 중단 + 알림 |
|
||||
|
||||
**불변 원칙**: `agy` 가 죽어도, 인증이 만료돼도, 쿼터가 소진돼도 **xlsx 리포트는 반드시 생성된다.** AI 산출물은 리포트의 부가 가치이지 전제 조건이 아니다.
|
||||
|
||||
**프롬프트 인젝션 방어**: API 응답 문자열(성분명, 제조소명 등)이 `agy` 프롬프트에 들어간다. 원본 데이터에 악의적 지시문이 섞일 가능성은 낮지만 0은 아니다. 따라서 `--dangerously-skip-permissions` 를 쓰지 않고, `--disable-slash-commands` 를 붙이며, 데이터는 명확한 구분자로 감싸고, 출력은 스키마로 검증한다. 상세는 `docs/research/10-agy-agent-integration-patterns.md`.
|
||||
|
||||
---
|
||||
|
||||
## 7. API 로 커버되지 않는 것과 대응
|
||||
|
||||
### 7.1 상태 필드(신규/변경/취하)가 없다
|
||||
|
||||
API 는 **현재 시점의 등록 현황 스냅샷**만 준다. "오늘 무엇이 새로 등록됐고 무엇이 취하됐는가"는 없다.
|
||||
|
||||
**대응**: 매일 전량 스냅샷을 저장하고 **전일 스냅샷과 diff** 한다.
|
||||
|
||||
| 판정 | 규칙 | 실측 근거 |
|
||||
|---|---|---|
|
||||
| **신규** | 오늘 키가 있고 어제 없음 | 등록번호 중복 0건 → 유일 키로 안전 |
|
||||
| **변경(1차)** | 양쪽에 키가 있고 **`DMF_PERMIT_DATE` 가 어제보다 최신** | 발급일자가 최종 갱신일로 이동 (불일치 44.5%) |
|
||||
| **변경(2차)** | 양쪽에 키가 있고, 나머지 6필드 중 하나 이상이 다름 | 내용 변경 포착 |
|
||||
| **취하** | 어제 키가 있고 오늘 없음 | API 가 정상 건만 반환 (웹과 756건 차이) |
|
||||
| 동일 | 그 외 | |
|
||||
|
||||
키는 `DMF_PERMIT_NO` 를 1순위로 한다. 비교 대상 필드와 정규화 규칙은 `docs/design/02-data-model.md` 에서 확정한다.
|
||||
|
||||
**`DMF_PERMIT_DATE` 가 변경의 직접 신호라는 것이 실측의 가장 큰 수확이다.** 등록번호 앞 8자리는 최초 등록일로 고정되고, 발급일자는 갱신마다 움직인다. 예를 들어 `20050831-33-A-81-08(18)` 은 2005년 등록 건이지만 발급일자가 `2026-08-18` 이다. 괄호 안의 값이 변경 차수로 보인다. 상세는 [`00b-baseline-data-analysis.md`](./00b-baseline-data-analysis.md) §6.
|
||||
|
||||
이 방식은 공고문 파싱보다 **오히려 우월하다.** 공고에 실리지 않는 조용한 변경까지 잡아내고, 공고문 형식 변경에 영향받지 않는다.
|
||||
|
||||
**여전히 놓치는 것**(정직하게 기록): 연차보고 이벤트(`최종연차보고년도` 가 API 에 없음), 변경 사유·유형, 취하와 취소의 구분, 정확한 취하 일자. 각각의 영향과 대응은 [`00b-baseline-data-analysis.md`](./00b-baseline-data-analysis.md) §6.4 참조. `대상의약품`(별표1/신물질) 은 등록번호 포맷에서 파생 가능하므로 실질 손실이 아니다.
|
||||
|
||||
### 7.2 오탐 안전장치 (필수)
|
||||
|
||||
전량 수집이 불완전하면 **멀쩡한 레코드가 전부 "취하"로 오판**된다. 이건 가장 위험한 실패 모드다. 다음 조건을 **전부** 통과하지 못하면 그 실행은 diff 를 수행하지 않고 경보만 낸다.
|
||||
|
||||
- [ ] 모든 페이지 요청이 HTTP 200 이고 `resultCode == "00"`
|
||||
- [ ] 수집 레코드 수 == 응답의 `totalCount`
|
||||
- [ ] `totalCount` 가 전일 대비 **-5% 이상 급감하지 않음** (임계값은 설정 가능)
|
||||
- [ ] 수집 레코드의 필수 필드 널 비율이 임계값 이하
|
||||
- [ ] 중복 `DMF_PERMIT_NO` 비율이 임계값 이하
|
||||
|
||||
### 7.3 공고 원문·변경 사유가 없다
|
||||
|
||||
변경이 감지돼도 "왜 변경됐는지"는 API 에 없다. **대응**: 리포트의 각 변경 행에 **의약품안전나라 검색 링크를 붙여** 사람이 클릭해 확인하게 한다. 자동으로 화면을 긁지 않는다. 이것이 robots.txt 를 존중하면서 사람의 필요를 충족하는 방법이다.
|
||||
|
||||
### 7.4 갱신 주기가 명시되지 않았다
|
||||
|
||||
포털에 데이터 갱신주기가 표기돼 있지 않다 (연계데이터 쪽은 "수시"). **대응**: 매일 06:00 에 받되, `totalCount` 와 데이터 해시를 기록해 **실제 갱신 빈도를 실측**한다. 2~4주 관측 후 스케줄을 조정한다.
|
||||
|
||||
---
|
||||
|
||||
## 8. 보조·확장 소스
|
||||
|
||||
같은 제공기관의 인접 데이터셋. 지금 붙이지 않되, 확장 지점으로 기록한다.
|
||||
|
||||
| 데이터셋 | 유형 | 경로 | 수정일 | 이 프로젝트에서의 가치 |
|
||||
|---|---|---|---|---|
|
||||
| 의약품 제품 허가정보 | 오픈API | `/data/15095677/openapi.do` | 2025-10-31 | 완제의약품 허가와 DMF 원료 연결. 성분→제품 역추적 |
|
||||
| 의약품 낱알식별 정보 | 오픈API | `/data/15057639/openapi.do` | 2025-11-10 | 낮음 |
|
||||
| 의약품개요정보(e약은요) | 오픈API | `/data/15075057/openapi.do` | 2025-09-19 | 성분 설명 보강 |
|
||||
| 의약품 생산·수입실적현황 | 오픈API | `/data/15056880/openapi.do` | 2026-03-20 | 원료 수입 실적과 DMF 등록 대조 |
|
||||
| 약가마스터_의약품표준코드 | 파일데이터 | `/data/15067462/fileData.do` | 2025-12-01 | 표준코드 매핑 |
|
||||
| 식품의약품안전처 의약품 관련 정보 | 파일데이터 (CSV) | `/data/15020627/fileData.do` | 2025-09-10 | 통계 보강 |
|
||||
|
||||
해외 소스(FDA DMF list, EDQM CEP, PMDA MF)는 `docs/research/01-dmf-domain-and-sources.md` 참조. 이들은 별도 배포 채널(다운로드 파일)이 있어 크롤링 없이 접근 가능한지 확인이 필요하다.
|
||||
|
||||
---
|
||||
|
||||
## 9. serviceKey 발급 절차
|
||||
|
||||
사용자가 직접 해야 하는 유일한 수동 단계다.
|
||||
|
||||
1. https://www.data.go.kr 회원가입·로그인
|
||||
2. https://www.data.go.kr/data/15057075/openapi.do 접속
|
||||
3. **활용신청** 클릭 → 활용 목적 기재 → 신청
|
||||
4. **자동승인**이므로 즉시 승인된다 (개발계정, 일 10,000건)
|
||||
5. 마이페이지 → 오픈API → 개발계정에서 **일반 인증키(Encoding)** 와 **일반 인증키(Decoding)** 를 확인
|
||||
6. 프로젝트 루트의 `.env` 에 저장:
|
||||
|
||||
```dotenv
|
||||
# 공공데이터포털 인증키 (Decoding 키를 넣고 코드에서 URL 인코딩한다)
|
||||
DATA_GO_KR_SERVICE_KEY=여기에_디코딩_인증키
|
||||
```
|
||||
|
||||
7. `.env` 는 반드시 `.gitignore` 에 포함한다. **인증키를 문서·로그·저장소에 남기지 않는다.**
|
||||
|
||||
> ⚠️ 인코딩/디코딩 키 혼동은 공공데이터포털 API 의 1위 실패 원인이다. 라이브러리가 자동 인코딩하는 경우 **Decoding 키**를 쓰고, 직접 URL 문자열을 조립하는 경우 **Encoding 키**를 쓴다. 우리는 `httpx` 의 `params=` 로 넘겨 자동 인코딩되게 하므로 **Decoding 키**를 사용한다.
|
||||
|
||||
**최초 검증 명령** (키 발급 후 즉시 실행):
|
||||
|
||||
```powershell
|
||||
$key = (Get-Content .env | Select-String '^DATA_GO_KR_SERVICE_KEY=').ToString().Split('=',2)[1]
|
||||
$url = "https://apis.data.go.kr/1471000/MdcDmfInfoService01/getMdcDmfList01"
|
||||
$resp = Invoke-RestMethod -Uri $url -Body @{
|
||||
serviceKey = $key
|
||||
pageNo = 1
|
||||
numOfRows = 1
|
||||
type = 'json'
|
||||
}
|
||||
$resp | ConvertTo-Json -Depth 6
|
||||
```
|
||||
|
||||
기대: `resultCode` 가 `00`, `totalCount` 에 전체 DMF 등록 건수가 나온다. **이 숫자를 문서 부록 B 에 기록하라.**
|
||||
|
||||
---
|
||||
|
||||
## 10. 이 결정이 무효화하는 기존 설계
|
||||
|
||||
이 결정 이전에 작성된 설계·조사 문서 중 다음 부분은 **더 이상 이 프로젝트의 구현 대상이 아니다.** 삭제하지 않고 "채택하지 않음" 표시를 달아 근거로 보존한다.
|
||||
|
||||
| 문서 | 무효화되는 부분 | 처리 |
|
||||
|---|---|---|
|
||||
| `research/04-anti-bot-and-legal.md` | 스텔스 도구, TLS 지문 위장, 헤드리스 지문 회피 | **참고 자료로 보존.** 구현하지 않음 |
|
||||
| `research/03-crawling-theory-and-papers.md` | wrapper induction, 셀렉터 안정성, DOM 추출 | 보존. **증분·변경 감지 이론은 그대로 유효**하며 오히려 핵심이 된다 |
|
||||
| `design/01-architecture.md` | `fetch` 계층의 브라우저 자동화 전제, Playwright 의존성 | **개정 필요.** HTTP 클라이언트 단일 경로로 축소 |
|
||||
| `research/10-agy-agent-integration-patterns.md` | UC2 "셀렉터 자가 복구" | **API 스키마 변화 대응(A7)** 으로 대체 |
|
||||
| `research/01-dmf-domain-and-sources.md` | nedrug 화면 파라미터·컬럼 조사 | 보존. **API 필드와 화면 컬럼의 대응 관계** 파악에 여전히 유용 |
|
||||
|
||||
> 아키텍처 문서가 작성 완료되면 이 절을 근거로 **개정 작업**을 수행한다. 개정 내용은 `design/01-architecture.md` 의 ADR 표에 "데이터 소스: 공식 Open API" 항목으로 기록한다.
|
||||
|
||||
**이 결정이 살려낸 것**: 데이터 흐름 뒷단(정규화 → diff → 저장 → xlsx 리포트 → 알림 → 스케줄링)은 **전혀 영향받지 않는다.** 이 프로젝트 가치의 대부분은 그쪽에 있고, 앞단이 단순해진 만큼 그쪽에 더 투자할 수 있다.
|
||||
|
||||
---
|
||||
|
||||
## 부록 A. 출처
|
||||
|
||||
| 제목 | URL | 확인 |
|
||||
|---|---|---|
|
||||
| nedrug robots.txt | https://nedrug.mfds.go.kr/robots.txt | ✅ 전문 확인 (`User-agent: * / Disallow: /`) |
|
||||
| 의약품안전나라 메인 | https://nedrug.mfds.go.kr/index | ✅ 열람 (DMF 링크 href 확인 실패) |
|
||||
| nedrug 추정 경로 | https://nedrug.mfds.go.kr/pbp/CCBGE01 | ✅ HTTP 404 확인 |
|
||||
| **DMF 오픈API 상세** | https://www.data.go.kr/data/15057075/openapi.do | ✅ 전체 명세 확인 (핵심 출처) |
|
||||
| DMF 연계데이터 | https://www.data.go.kr/data/2095/linkedData.do | ✅ 열람 |
|
||||
| 공공데이터포털 DMF 검색 | https://www.data.go.kr/tcs/dss/selectDataSetList.do?keyword=DMF | ✅ 열람 |
|
||||
| 공공데이터포털 원료의약품 등록 검색 | https://www.data.go.kr/tcs/dss/selectDataSetList.do?keyword=원료의약품+등록 | ✅ 열람 |
|
||||
| 식약처 의약품 데이터셋 목록 | https://www.data.go.kr/tcs/dss/selectDataSetList.do?keyword=의약품&org=식품의약품안전처 | ✅ 열람 |
|
||||
| API 엔드포인트 생존 확인 | https://apis.data.go.kr/1471000/MdcDmfInfoService01/getMdcDmfList01 | ✅ HTTP 403 (인증 강제, 엔드포인트 정상) |
|
||||
| 연계데이터 구버전 엔드포인트 | http://data.mfds.go.kr/openapi/MdcDmfInfoService/getMdcDmfList | 미호출 |
|
||||
| API 참고문서 | `IROS_76_원료의약품(DMF)현황_v1.1.docx` | ⚠️ 미다운로드 |
|
||||
|
||||
---
|
||||
|
||||
## 부록 B. 미해결 / 실측 필요
|
||||
|
||||
### ✅ 해소된 항목 (2026-09-02 `DMF_현황.xlsx` 전수 분석)
|
||||
|
||||
- [x] ~~전체 DMF 등록 건수(`totalCount`)는 몇 건인가?~~ → **9,084건**
|
||||
- [x] ~~응답에 `DMF_PERMIT_NO` 중복이 존재하는가?~~ → **중복 0건. 완전한 자연 키**
|
||||
- [x] ~~API 응답에 취하·말소된 등록번호가 남아 있는가?~~ → **남지 않을 가능성 매우 높음.** 웹 9,840 vs API 9,084 = 756건 차이. 연속 관측으로 최종 확정 예정
|
||||
- [x] ~~`type=json` 이 실제로 동작하는가?~~ → 프로토타입이 수집에 성공했으므로 API 자체는 정상 동작 확인. 포맷 파라미터는 별도 확인
|
||||
- [x] ~~변경 탐지가 7필드로 가능한가?~~ → **가능. `DMF_PERMIT_DATE` 가 최종 갱신일로 이동** (앞 8자리와 44.5% 불일치)
|
||||
|
||||
### 남은 항목
|
||||
|
||||
- [ ] `numOfRows` 의 실제 최대값은? 명세 크기가 3자리이므로 999 로 추정되나 실측 필요.
|
||||
- [ ] 데이터 실제 갱신 주기는? 2~4주 관측 필요.
|
||||
- [ ] **연차보고 시 `DMF_PERMIT_DATE` 가 갱신되는가?** 연차보고는 매년 1~2월에 몰리므로 그때 관측. 갱신된다면 변경으로 잡히고, 아니면 영구히 놓친다.
|
||||
- [ ] 변경 시 등록번호의 괄호 부분이 바뀌는가, 번호는 그대로이고 발급일자만 바뀌는가? 전자면 신규로 오탐할 위험이 있다.
|
||||
- [ ] 웹 9,840 − API 9,084 = 756건이 정말 취하·취소 건인지 표본 대조.
|
||||
- [ ] 제조국가명 결측 5건의 등록번호와 원인.
|
||||
- [ ] 참고문서 `IROS_76_원료의약품(DMF)현황_v1.1.docx` 에 명세 외 추가 정보가 있는가?
|
||||
- [ ] `entp_name` / `ingr_kor_name` 검색 파라미터가 부분 일치인가 완전 일치인가?
|
||||
- [ ] API 응답에 취하·말소된 등록번호가 남아 있는가, 아니면 사라지는가? (취하 판정 로직의 전제)
|
||||
- [ ] 일 10,000건 한도의 카운트 단위가 "호출 수"인가 "레코드 수"인가?
|
||||
- [ ] 운영계정 전환 조건과 증량 한도는?
|
||||
- [ ] 해외 소스(FDA/EDQM/PMDA)의 robots.txt 와 공식 배포 채널은?
|
||||
- [ ] 인접 데이터셋 "의약품 제품 허가정보" 로 DMF 원료↔완제품 연결이 실제로 가능한가?
|
||||
|
||||
---
|
||||
|
||||
*이 문서는 데이터 소스에 관한 SSOT 다. 소스 관련 새 사실은 여기를 갱신한다.*
|
||||
591
docs/design/00b-baseline-data-analysis.md
Normal file
591
docs/design/00b-baseline-data-analysis.md
Normal file
|
|
@ -0,0 +1,591 @@
|
|||
# 기준선 데이터 분석 — 기존 `DMF_현황.xlsx` 실측
|
||||
|
||||
> **이 문서의 역할**: 사용자가 이미 운영 중이던 `DMF_현황.xlsx` 를 전수 분석한 결과. **이 프로젝트는 스크래치가 아니라 기존 프로토타입의 개선**이며, 이 문서가 그 출발점의 사실 기록이다. 데이터 모델·변경 탐지·정규화 규칙의 근거가 전부 여기서 나온다.
|
||||
|
||||
**분석일**: 2026-09-02
|
||||
**대상 파일**: `C:\Users\encep\OneDrive\문서\카카오톡 받은 파일\DMF_현황.xlsx` (757,331 bytes, 수정 2026-09-02 21:38)
|
||||
**분석 도구**: openpyxl 3.1.5, pandas 2.2.3
|
||||
|
||||
---
|
||||
|
||||
## 0. 한눈에 보기
|
||||
|
||||
- **이 프로젝트는 스크래치가 아니다.** 이미 공식 Open API 로 수집해 xlsx 를 만드는 프로토타입이 돌고 있었고, 그 산출물이 이 파일이다. 컬럼이 API 7필드와 정확히 일치한다.
|
||||
- **API 전체 건수는 9,084건이다.** 웹 화면 실측 9,840건과 **756건 차이**가 난다. 이 차이가 **API 는 취하·취소 건을 제외하고 정상 건만 반환한다**는 강력한 증거다.
|
||||
- **따라서 취하 탐지는 diff 로 가능하다.** 레코드가 API 응답에서 사라지면 취하다. "API 7필드로는 취하를 탐지할 수 없다"던 우려는 해소 방향이다.
|
||||
- **변경 탐지도 가능하다.** 등록번호 앞 8자리(최초 등록일)와 `발급일자`가 **44.5% 불일치**하며, 불일치하는 건은 전부 괄호가 붙은 갱신 건이다. 즉 **`발급일자`는 최종 갱신일로 움직이는 필드**다. 이것이 변경의 직접 신호다.
|
||||
- **등록번호는 완전한 자연 키다.** 9,084건 중 중복 0건. 별도 대체 키가 필요 없다.
|
||||
- **데이터 품질 문제 4종을 확인했다.** 국가 중복 표기 496건, 성분명 표기 흔들림 21그룹, 업체명 표기 흔들림 2그룹, 제조소명 공백 오류 42건. 정규화 계층이 반드시 필요하다.
|
||||
- **기존 xlsx 에는 서식이 전혀 없다.** 틀 고정, 자동 필터, 조건부 서식, 차트가 모두 0이다. "디자인 예쁘게" 요구가 겨냥하는 지점이 정확히 여기다.
|
||||
- 데이터는 **2003-04-18부터 2026-09-01까지 23년치**다. 성분 1,539종, 업체 438개, 제조소 3,051개, 국가 49개.
|
||||
|
||||
---
|
||||
|
||||
## 1. 목차
|
||||
|
||||
1. [파일 구조](#2-파일-구조)
|
||||
2. [컬럼과 API 필드 대응](#3-컬럼과-api-필드-대응)
|
||||
3. [건수 검증 — 취하 탐지 가능성의 결정적 근거](#4-건수-검증--취하-탐지-가능성의-결정적-근거)
|
||||
4. [등록번호 분석](#5-등록번호-분석)
|
||||
5. [변경 탐지 가능성 — 발급일자의 의미](#6-변경-탐지-가능성--발급일자의-의미)
|
||||
6. [데이터 규모와 분포](#7-데이터-규모와-분포)
|
||||
7. [데이터 품질 문제](#8-데이터-품질-문제)
|
||||
8. [기존 서식 상태와 개선 여지](#9-기존-서식-상태와-개선-여지)
|
||||
9. [확정되는 설계 결정](#10-확정되는-설계-결정)
|
||||
10. [부록 A. 재현 스크립트](#부록-a-재현-스크립트)
|
||||
11. [부록 B. 미해결 / 확인 필요](#부록-b-미해결--확인-필요)
|
||||
|
||||
---
|
||||
|
||||
## 2. 파일 구조
|
||||
|
||||
| 시트 | 범위 | 행 | 열 | 내용 |
|
||||
|---|---|---|---|---|
|
||||
| `전체` | A1:H9085 | 9,085 (헤더 1 + 데이터 9,084) | 8 | 누적 DMF 등록 목록 |
|
||||
| `신규` | A1:H1 | 1 (헤더만) | 8 | 오늘의 신규 건. 첫 실행이라 비어 있음 |
|
||||
| `갱신이력` | A1:C2 | 2 (헤더 1 + 1행) | 3 | 실행 로그 |
|
||||
|
||||
`갱신이력` 시트의 유일한 데이터 행:
|
||||
|
||||
| 실행일 | 누적건수 | 신규건수 |
|
||||
|---|---|---|
|
||||
| 2026-09-02 | 9084 | 0 |
|
||||
|
||||
**해석**: 2026-09-02 에 최초 실행되어 9,084건을 기준선으로 적재했고, 비교 대상이 없어 신규는 0으로 기록됐다. 설계 의도가 이미 "스냅샷 + 신규 + 실행 이력" 3층 구조였다는 뜻이다. **이 구조를 버리지 않고 확장한다.**
|
||||
|
||||
---
|
||||
|
||||
## 3. 컬럼과 API 필드 대응
|
||||
|
||||
| # | xlsx 컬럼 | API 응답 필드 | 비고 |
|
||||
|---|---|---|---|
|
||||
| 1 | 등록번호 | `DMF_PERMIT_NO` | 자연 키 |
|
||||
| 2 | 성분명 | `INGR_KOR_NAME` | |
|
||||
| 3 | 업체명 | `ENTP_NAME` | 화면에서는 "신청인" |
|
||||
| 4 | 제조소명 | `MNFCTR_NAME` | |
|
||||
| 5 | 제조소소재지 | `MNFCTR_PLACE` | |
|
||||
| 6 | 제조국가명 | `MANUF_COUNTRY_CODE_NM` | 콤마 다중값 |
|
||||
| 7 | 발급일자 | `DMF_PERMIT_DATE` | **최종 갱신일로 동작** (6절) |
|
||||
| 8 | 최초수집일 | *(파생)* | 이 시스템이 처음 이 레코드를 본 날 |
|
||||
|
||||
**확인 사항**: 8개 컬럼 중 7개가 API 필드와 1:1로 정확히 대응한다. 8번째 `최초수집일`은 프로토타입이 스스로 만든 파생 컬럼이다. 좋은 설계이므로 **유지하고, 여기에 `최종변경감지일`·`상태`·`변경차수`를 추가**한다.
|
||||
|
||||
**웹 화면에만 있는 컬럼**(API 미제공): `대상의약품`, `최종변경일자`, `최종연차보고년도`, `취소/취하구분`, `취소/취하일자`, `문서번호`. 이 중 실질적 손실과 대체 수단은 10절 참조.
|
||||
|
||||
---
|
||||
|
||||
## 4. 건수 검증 — 취하 탐지 가능성의 결정적 근거
|
||||
|
||||
| 소스 | 건수 | 출처 |
|
||||
|---|---|---|
|
||||
| 웹 화면 (`/pbp/CCBAC03`) | **9,840** | 조사 문서 `research/01` 실측 (2026-09-02) |
|
||||
| 공식 Open API | **9,084** | 이 파일 실측 (2026-09-02) |
|
||||
| **차이** | **756** | |
|
||||
|
||||
두 수치는 같은 날 측정됐다. 756건의 차이를 설명하는 가설은 둘뿐이다.
|
||||
|
||||
| 가설 | 내용 | 판정 |
|
||||
|---|---|---|
|
||||
| **A** | API 가 취하·취소된 건을 제외하고 **정상 건만** 반환한다 | **유력** |
|
||||
| B | API 데이터가 웹보다 오래됐다 | **기각** |
|
||||
|
||||
가설 B가 기각되는 근거: 이 파일의 최신 `발급일자`가 **2026-09-01** 이고, 조사 문서가 기록한 웹 화면의 최신 `최초등록일자`도 **2026-09-01** 로 동일하다. API 가 뒤처져 있지 않다. 하루치 지연으로는 756건을 설명할 수 없다.
|
||||
|
||||
또한 웹 화면의 `취소/취하구분` 컬럼 실측값이 `정상`이었다는 사실은, 웹 테이블이 취하 건을 **삭제하지 않고 상태 컬럼으로 표시**한다는 뜻이다. 두 사실을 합치면 756건은 취하·취소 건일 가능성이 높다.
|
||||
|
||||
### 이것이 의미하는 것
|
||||
|
||||
**API 응답에서 등록번호가 사라지면 취하로 판정할 수 있다.** 이는 이 프로젝트 변경 탐지 설계의 핵심 전제이며, 이 분석으로 강하게 뒷받침된다.
|
||||
|
||||
> ⚠️ **아직 증명은 아니다.** 이틀 이상 연속 수집해 실제로 사라지는 레코드를 관찰해야 확정된다. 그때까지는 `design/00-DATA-SOURCE-DECISION.md` §7.2 의 안전장치(건수 급감 시 diff 중단)를 반드시 유지한다.
|
||||
|
||||
---
|
||||
|
||||
## 5. 등록번호 분석
|
||||
|
||||
### 5.1 유일성
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| 전체 행 | 9,084 |
|
||||
| 고유 등록번호 | **9,084** |
|
||||
| 중복 | **0** |
|
||||
|
||||
**결론: 등록번호는 완전한 자연 키다.** 복합 키나 해시 대체 키가 필요 없다. SQLite 의 `PRIMARY KEY` 로 그대로 쓴다.
|
||||
|
||||
### 5.2 포맷 분포
|
||||
|
||||
| 포맷 | 건수 | 비율 | 예시 |
|
||||
|---|---|---|---|
|
||||
| 표준 `YYYYMMDD-n-A-n-n` | 3,774 | 41.5% | `20260901-86-D-173-26`, `20260831-209-J-2250` |
|
||||
| 표준 + 괄호 | 2,973 | 32.7% | `20230116-200-I-647-07(A)`, `20250219-32-C-423-28(1)` |
|
||||
| 신물질 `수nnnn-n-ND` | 1,882 | 20.7% | `수6580-16-ND(20)`, `수6256-1-ND`, `수582-34-ND(A)` |
|
||||
| 기타 | 455 | 5.0% | `1962-17-ND`, `20100616-122-G-60-22(1)-A(1)`, `20150918-135-H-305-43(2)-A(A)` |
|
||||
|
||||
**설계 시사점**: 파서는 **4종 이상의 변형을 관대하게 처리**해야 한다. 특히 "기타" 455건에는 `수` 접두어 없는 `1962-17-ND` 형태와 이중 괄호 `(1)-A(1)` 형태가 섞여 있다.
|
||||
|
||||
**파싱 실패는 치명적이지 않게 설계한다.** 등록번호는 문자열 그대로가 키이므로, 파싱은 파생 정보(최초 등록일, 변경 차수)를 얻기 위한 부가 작업이다. 실패해도 레코드는 정상 처리하고 파생 필드만 null 로 둔다.
|
||||
|
||||
### 5.3 괄호의 의미
|
||||
|
||||
괄호가 붙은 건은 **표준+괄호 2,973건 + 신물질 일부 + 기타 일부**로 전체의 약 3분의 1이다. 다음 절의 분석이 괄호의 의미를 밝힌다.
|
||||
|
||||
---
|
||||
|
||||
## 6. 변경 탐지 가능성 — 발급일자의 의미
|
||||
|
||||
### 6.1 실측
|
||||
|
||||
표준 포맷 등록번호 6,768건에 대해, 앞 8자리(`YYYYMMDD`)와 `발급일자`를 비교했다.
|
||||
|
||||
| 항목 | 건수 | 비율 |
|
||||
|---|---|---|
|
||||
| 앞 8자리 == 발급일자 | 3,753 | 55.5% |
|
||||
| **앞 8자리 ≠ 발급일자** | **3,015** | **44.5%** |
|
||||
|
||||
불일치 사례:
|
||||
|
||||
| 등록번호 | 앞 8자리(최초 등록) | 발급일자(최종) | 간격 |
|
||||
|---|---|---|---|
|
||||
| `20230116-200-I-647-07(A)` | 2023-01-16 | 2026-08-28 | 3년 7개월 |
|
||||
| `20180814-209-J-127(A)` | 2018-08-14 | 2026-08-25 | 8년 |
|
||||
| `20131031-84-D-126-07(A)` | 2013-10-31 | 2026-08-25 | 12년 10개월 |
|
||||
| `20050831-33-A-81-08(18)` | 2005-08-31 | 2026-08-18 | 21년 |
|
||||
| `20250219-32-C-423-28(1)` | 2025-02-19 | 2026-08-21 | 1년 6개월 |
|
||||
| `20210721-209-J-1073(6)` | 2021-07-21 | 2026-08-18 | 5년 1개월 |
|
||||
|
||||
### 6.2 해석
|
||||
|
||||
불일치 건은 **전부 괄호가 붙어 있다.** 따라서 다음 구조가 성립한다.
|
||||
|
||||
| 요소 | 의미 |
|
||||
|---|---|
|
||||
| 등록번호 앞 8자리 | **최초 등록일** (고정) |
|
||||
| 괄호 안의 값 `(A)`, `(1)`, `(18)` | **변경/갱신 표식** |
|
||||
| `발급일자` (`DMF_PERMIT_DATE`) | **최종 갱신일** (움직인다) |
|
||||
|
||||
**즉 `발급일자`는 정적인 최초 등록일이 아니라 변경 때마다 갱신되는 동적 필드다.**
|
||||
|
||||
### 6.3 이것이 의미하는 것
|
||||
|
||||
**API 7필드만으로 신규·변경·취하 세 가지를 모두 탐지할 수 있다.**
|
||||
|
||||
| 판정 | 규칙 | 근거 |
|
||||
|---|---|---|
|
||||
| **신규** | 오늘 등록번호가 있고 어제 없음 | 등록번호가 유일 키 (5.1) |
|
||||
| **변경** | 등록번호 동일 + `발급일자`가 어제보다 최신 | 발급일자가 최종 갱신일로 동작 (6.2) |
|
||||
| **변경(보조)** | 등록번호 동일 + 성분명·업체명·제조소명·소재지·국가 중 하나가 다름 | 내용 변경 |
|
||||
| **취하** | 어제 등록번호가 있고 오늘 없음 | API 가 정상 건만 반환 (4절) |
|
||||
|
||||
이로써 조사 문서 `research/01` 이 제기한 "API 단독으로는 변경·취하 탐지 요구를 충족할 수 없다"는 주장은 **반증된다.** 그 주장은 웹 화면의 `최종변경일자`·`취소/취하구분` 컬럼이 있어야만 탐지가 가능하다는 전제에 서 있었으나, 실제로는 `발급일자`의 이동과 레코드 소멸이 같은 정보를 담고 있다.
|
||||
|
||||
### 6.4 여전히 놓치는 것
|
||||
|
||||
정직하게 기록한다. API 로 탐지되지 않는 이벤트가 있다.
|
||||
|
||||
| 놓치는 것 | 이유 | 영향 | 대응 |
|
||||
|---|---|---|---|
|
||||
| **연차보고** | `최종연차보고년도` 컬럼이 API 에 없고, 연차보고 시 `발급일자`가 갱신되는지 불명 | 중간. 연차보고는 매년 1월 말에 몰린다 | 리포트에서 별도 이벤트로 다루지 않음. 확인 필요 |
|
||||
| **변경 사유·유형** | 어떤 항목이 왜 바뀌었는지 API 에 없음 | 낮음 | 리포트 각 행에 웹 화면 검색 링크를 붙여 사람이 확인 |
|
||||
| **취하 vs 취소 구분** | 소멸했다는 사실만 알고 사유를 모름 | 낮음 | "목록에서 사라짐"으로 표기 |
|
||||
| **취하 일자** | 소멸을 감지한 날짜만 앎 | 낮음 | 감지일로 기록 |
|
||||
| **대상의약품 구분** (`별표1`/`신물질`) | API 에 없음 | 낮음 | **등록번호 포맷으로 추론 가능** (`수` 접두어 = 신물질) |
|
||||
|
||||
`대상의약품`은 등록번호 포맷에서 파생할 수 있으므로 실질 손실이 아니다. 실질적으로 아쉬운 것은 연차보고 하나다.
|
||||
|
||||
---
|
||||
|
||||
## 7. 데이터 규모와 분포
|
||||
|
||||
### 7.1 기본 통계
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| 전체 레코드 | 9,084 |
|
||||
| 고유 성분명 | 1,539 |
|
||||
| 고유 업체명 | 438 |
|
||||
| 고유 제조소명 | 3,051 |
|
||||
| 고유 제조소소재지 | 4,270 |
|
||||
| 고유 제조국가명(원문) | 225 |
|
||||
| 고유 국가(콤마 분해 후) | **49** |
|
||||
| 고유 발급일자 | 2,397 |
|
||||
| 발급일자 범위 | 2003-04-18 ~ 2026-09-01 |
|
||||
| 결측치 | 제조국가명 5건. 나머지 컬럼 0건 |
|
||||
|
||||
### 7.2 연도별 등록 건수 (최근 12년)
|
||||
|
||||
| 연도 | 건수 |
|
||||
|---|---|
|
||||
| 2015 | 278 |
|
||||
| 2016 | 246 |
|
||||
| 2017 | 284 |
|
||||
| 2018 | 473 |
|
||||
| 2019 | 502 |
|
||||
| 2020 | 668 |
|
||||
| 2021 | 939 |
|
||||
| 2022 | 633 |
|
||||
| 2023 | 463 |
|
||||
| 2024 | 534 |
|
||||
| 2025 | **1,185** |
|
||||
| 2026 (9월 2일까지) | 610 |
|
||||
|
||||
> 주의: 이 집계는 `발급일자` 기준이므로 **최초 등록이 아니라 최종 갱신 연도**다. 2025년이 급증한 것은 최근 갱신된 건이 많다는 뜻이지 신규 등록이 폭증했다는 뜻이 아니다. **리포트에서 이 구분을 명확히 표기해야 사용자가 오해하지 않는다.**
|
||||
|
||||
### 7.3 제조국가 분포 (원문 기준 상위 15)
|
||||
|
||||
| 국가 | 건수 |
|
||||
|---|---|
|
||||
| 인도 | 3,351 |
|
||||
| 중국 | 2,231 |
|
||||
| 대한민국 | 845 |
|
||||
| 이탈리아 | 313 |
|
||||
| 스페인 | 210 |
|
||||
| 대한민국,중국 | 156 |
|
||||
| 일본 | 149 |
|
||||
| 대만 | 121 |
|
||||
| 독일 | 117 |
|
||||
| 프랑스 | 88 |
|
||||
| 미국 | 83 |
|
||||
| 스위스 | 81 |
|
||||
| 아일랜드 | 77 |
|
||||
| 중국,중국 | 76 |
|
||||
| 인도,인도 | 74 |
|
||||
|
||||
인도와 중국이 전체의 **61.5%** 를 차지한다. 원료의약품 공급망이 이 두 나라에 집중돼 있다는 사실이 데이터로 확인된다. **리포트 대시보드의 핵심 지표가 될 만하다.**
|
||||
|
||||
### 7.4 업체 분포 (상위 15)
|
||||
|
||||
| 업체명 | 건수 |
|
||||
|---|---|
|
||||
| (주)삼오제약 | 392 |
|
||||
| (주)파마피아 | 378 |
|
||||
| 에이징생명과학(주) | 277 |
|
||||
| (주)국전 | 271 |
|
||||
| 에이스바이오팜주식회사 | 248 |
|
||||
| 대신무약(주) | 215 |
|
||||
| 화일약품(주) | 188 |
|
||||
| (주)마성엘에스 | 176 |
|
||||
| 성우화학(주) | 155 |
|
||||
| (주)성진엑심 | 153 |
|
||||
| ㈜하이플 | 147 |
|
||||
| (주)휴시드 | 124 |
|
||||
| 이성인터내쇼날(주) | 117 |
|
||||
| 성이바이오(주) | 117 |
|
||||
| 주식회사토루 | 116 |
|
||||
|
||||
### 7.5 성분 분포 (상위 15)
|
||||
|
||||
| 성분명 | 건수 |
|
||||
|---|---|
|
||||
| 히알루론산나트륨 | 104 |
|
||||
| 메트포르민염산염 | 97 |
|
||||
| 로수바스타틴칼슘 | 95 |
|
||||
| 아세트아미노펜 | 92 |
|
||||
| 세레콕시브 | 73 |
|
||||
| 발사르탄 | 73 |
|
||||
| 암로디핀베실산염 | 72 |
|
||||
| 레바미피드 | 69 |
|
||||
| 덱시부프로펜 | 67 |
|
||||
| 에제티미브 | 67 |
|
||||
| 트라마돌염산염 | 66 |
|
||||
| 프레가발린 | 61 |
|
||||
| 엠파글리플로진 | 61 |
|
||||
| 세파클러수화물 | 58 |
|
||||
| 아토르바스타틴칼슘 | 55 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 데이터 품질 문제
|
||||
|
||||
정규화 계층에서 반드시 처리해야 할 실제 문제들이다. 모두 실측으로 확인했다.
|
||||
|
||||
### 8.1 제조국가명 — 같은 국가 반복 표기 (496건)
|
||||
|
||||
원본에 같은 국가가 콤마로 반복된다.
|
||||
|
||||
| 원문 | 건수 |
|
||||
|---|---|
|
||||
| `중국,중국` | 76 |
|
||||
| `인도,인도` | 74 |
|
||||
| `대한민국,대한민국` | 56 |
|
||||
| `이탈리아,이탈리아` | 32 |
|
||||
| `대한민국,중국,중국` | 30 |
|
||||
| `중국,중국,중국` | 23 |
|
||||
| `인도,인도,인도` | 22 |
|
||||
| `독일,독일` | 19 |
|
||||
| `스위스,스위스` | 12 |
|
||||
| `일본,일본` | 10 |
|
||||
| `스페인,스페인` | 10 |
|
||||
| `이탈리아,중국,중국,중국` | 8 |
|
||||
|
||||
**총 496건.** 제조소가 여럿이고 같은 국가에 있을 때 국가명이 그만큼 반복되는 것으로 보인다.
|
||||
|
||||
**정규화 규칙**: 콤마로 분해 → 공백 제거 → 중복 제거 → 정렬 → 재결합. 원문은 별도 컬럼에 보존한다.
|
||||
|
||||
정규화 후 고유 국가는 **49개**다.
|
||||
|
||||
```
|
||||
남아프리카 공화국, 네덜란드, 노르웨이, 뉴질랜드, 대만, 대한민국, 덴마크, 독일, 라트비아,
|
||||
루마니아, 말레이지아, 멕시코, 몰타, 미국, 바하마, 벨기에, 불가리아, 브라질, 스웨덴,
|
||||
스위스, 스페인, 슬로바키아, 슬로베니아, 싱가포르, 아르헨티나, 아일랜드, 영국, 오만,
|
||||
오스트리아, 우크라이나, 이란, 이스라엘, 이탈리아, 인도, 인도네시아, 일본, 중국,
|
||||
체코공화국, 캐나다, 크로아티아, 태국, 튀르키예, 포르투갈, 폴란드, 푸에르토리코,
|
||||
프랑스, 핀란드, 헝가리, 호주
|
||||
```
|
||||
|
||||
> 표기 특이점: `말레이지아`(표준 표기는 말레이시아), `체코공화국`(체코) 처럼 비표준 표기가 섞여 있다. 국가 코드 매핑 테이블을 두면 지도 시각화나 그룹화에 유리하다.
|
||||
|
||||
### 8.2 성분명 표기 흔들림 (21그룹)
|
||||
|
||||
공백·구분자만 다른 같은 성분이 별개 값으로 존재한다.
|
||||
|
||||
| 흔들리는 표기 |
|
||||
|---|
|
||||
| `다비가트란 에텍실레이트 메실산염` / `다비가트란에텍실레이트메실산염` |
|
||||
| `DL-메틸에페드린염산염` / `dl-메틸에페드린염산염` |
|
||||
| `L-아스파르트산-L-오르니틴` / `L-아스파르트산·L-오르니틴` |
|
||||
| `로베글리타존 황산염` / `로베글리타존황산염` |
|
||||
| `항독성간장 엑스` / `항독성간장엑스` |
|
||||
| `오메가-3-산에틸에스테르90` / `오메가3산에틸에스테르90` |
|
||||
| `싸이모신 알파 1` / `싸이모신-알파1` / `싸이모신알파1` |
|
||||
| `은행엽 건조엑스` / `은행엽건조엑스` |
|
||||
| `덱스클로르페니라민 말레산염` / `덱스클로르페니라민말레산염` |
|
||||
| `톨밥탄 분무건조분말` / `톨밥탄분무건조분말` |
|
||||
|
||||
**총 21그룹.** 1,539개 성분 중 21그룹이므로 비율은 낮지만, **성분별 집계 시트에서 같은 성분이 두 줄로 갈라져 보이는 문제**를 만든다.
|
||||
|
||||
**정규화 규칙**: 공백·중점(`·`)·하이픈·괄호를 제거하고 소문자화한 값을 그룹 키로 삼는다. 표시는 원문 중 최빈값을 대표로 쓴다. **원문은 반드시 보존한다.**
|
||||
|
||||
### 8.3 업체명 표기 흔들림 (2그룹)
|
||||
|
||||
| 흔들리는 표기 |
|
||||
|---|
|
||||
| `삼진제약(주)` / `삼진제약주식회사` |
|
||||
| `(주)유일팜테크` / `주식회사 유일팜테크` |
|
||||
|
||||
438개 업체 중 2그룹으로 매우 적다. `㈜` → `(주)`, `주식회사` → `(주)`, 공백 제거로 해소된다.
|
||||
|
||||
### 8.4 제조소명 형식 오류 (42건)
|
||||
|
||||
원본에 연속 공백이나 마침표 중복이 들어 있다.
|
||||
|
||||
| 예시 |
|
||||
|---|
|
||||
| `BDR LIFESCIENCES PVT. LTD..` (마침표 2개) |
|
||||
| `Nanjing King-Friend Biochemical Pharmaceutical Co., Ltd.` (연속 공백) |
|
||||
| `North China Pharmaceutical Group Semisyntech Co., Ltd` (연속 공백) |
|
||||
|
||||
또한 제조소가 여러 곳인 경우 콤마로 이어붙어 있고, `[출발물질제조소]` 같은 **역할 표시가 문자열 안에 섞여** 있다.
|
||||
|
||||
```
|
||||
Sichuan Renan Pharmaceutical Co, Ltd,[출발물질제조소]North China Pharmaceutical Group Semisyntech Co., Ltd
|
||||
```
|
||||
|
||||
**주의**: 제조소명 자체에 콤마가 포함되므로(`Co., Ltd.`) **콤마로 단순 분할하면 안 된다.** 다중 제조소 분해는 신뢰할 수 없으니, 정규화는 연속 공백 압축과 양끝 정리에 그친다.
|
||||
|
||||
### 8.5 발급일자 형식
|
||||
|
||||
**9,084건 전부 `YYYY-MM-DD` 형식으로 일관**된다. 파싱 실패 0건. 날짜 처리는 안전하다.
|
||||
|
||||
### 8.6 제조소소재지 길이
|
||||
|
||||
| 통계 | 값 |
|
||||
|---|---|
|
||||
| 평균 | 81자 |
|
||||
| 중앙값 | 78자 |
|
||||
| 75분위 | 107자 |
|
||||
| **최대** | **473자** |
|
||||
|
||||
**xlsx 설계 시사점**: 이 컬럼을 그대로 표시하면 열 너비가 파괴된다(기존 파일의 D열 너비가 255.6으로 최대치에 붙어 있는 이유다). **줄바꿈 + 행 높이 고정 + 열 너비 상한**을 적용하거나, 목록 시트에서는 앞 60자만 보이고 전체는 셀 메모나 상세 시트에서 보게 해야 한다.
|
||||
|
||||
---
|
||||
|
||||
## 9. 기존 서식 상태와 개선 여지
|
||||
|
||||
`전체` 시트의 서식 실측 결과다.
|
||||
|
||||
| 항목 | 현재 상태 |
|
||||
|---|---|
|
||||
| 틀 고정 (freeze_panes) | **없음** |
|
||||
| 자동 필터 | **없음** |
|
||||
| 조건부 서식 | **0개 규칙** |
|
||||
| 병합 셀 | 0 |
|
||||
| 차트 | **0** |
|
||||
| 이미지 | 0 |
|
||||
| Excel 표(ListObject) | **없음** |
|
||||
| 열 너비 지정 | A 30.6 / B 85.6 / C 33.1 / **D 255.6** / E~H 미지정 |
|
||||
| 시트 탭 색상 | **없음** |
|
||||
|
||||
**진단**: 데이터는 정확한데 **읽기 도구로서의 설계가 전혀 없다.** 9,084행을 헤더 고정 없이 스크롤해야 하고, 필터가 없어 특정 성분·업체를 찾을 수 없으며, 무엇이 새 건인지 색으로 구분되지 않는다. D열 너비 255.6은 소재지 473자를 담으려다 최대치에 닿은 것으로, 화면이 가로로 파괴된다.
|
||||
|
||||
**"디자인 예쁘게, 한눈에" 요구가 겨냥하는 지점이 정확히 여기다.** 개선 방향은 `design/03-xlsx-report-spec.md` 에서 확정하되, 이 분석에서 나오는 필수 항목은 다음과 같다.
|
||||
|
||||
- 헤더 행 고정과 자동 필터 (9,084행을 다루려면 필수)
|
||||
- 열 너비 상한과 소재지 줄바꿈 처리
|
||||
- 신규·변경·취하 상태에 따른 행 색상 구분
|
||||
- 대시보드 시트 신설: 오늘 요약, 국가 분포(인도·중국 61.5% 집중), 상위 성분·업체, 최근 추이
|
||||
- 성분별·업체별·국가별 집계 시트 (정규화된 값 기준)
|
||||
- 시트 탭 색상과 목차 하이퍼링크
|
||||
|
||||
---
|
||||
|
||||
## 10. 확정되는 설계 결정
|
||||
|
||||
이 분석으로 확정 또는 강하게 뒷받침되는 결정들이다.
|
||||
|
||||
| # | 결정 | 근거 |
|
||||
|---|---|---|
|
||||
| D1 | **등록번호를 기본 키로 쓴다.** 대체 키 불필요 | 9,084건 중복 0 (5.1) |
|
||||
| D2 | **취하는 "API 응답에서 소멸"로 판정한다** | 웹 9,840 vs API 9,084, 756건 차이 (4절) |
|
||||
| D3 | **변경은 `발급일자` 이동 + 6개 필드 diff 로 판정한다** | 발급일자가 최종 갱신일로 동작, 44.5% 불일치 (6절) |
|
||||
| D4 | **`대상의약품`(별표1/신물질)은 등록번호 포맷에서 파생한다** | `수` 접두어 1,882건 식별 (5.2) |
|
||||
| D5 | **정규화 계층을 반드시 둔다.** 국가·성분명·업체명·제조소명 4종 | 품질 문제 실측 (8절) |
|
||||
| D6 | **원문을 반드시 보존한다.** 정규화 값은 별도 컬럼 | 되돌릴 수 없는 손실 방지 |
|
||||
| D7 | **등록번호 파싱 실패를 허용한다.** 파생 필드만 null | 포맷 4종 이상, 기타 455건 (5.2) |
|
||||
| D8 | **기존 3시트 구조(전체/신규/갱신이력)를 계승·확장한다** | 프로토타입 설계 의도 존중 (2절) |
|
||||
| D9 | **`최초수집일` 파생 컬럼을 유지하고 형제 컬럼을 추가한다** | 기존 설계가 이미 옳았음 (3절) |
|
||||
| D10 | **소재지 컬럼은 표시 폭을 제한한다** | 최대 473자 (8.6) |
|
||||
| D11 | **연도별 집계는 "갱신 연도"임을 명시한다** | 발급일자가 최종 갱신일 (7.2) |
|
||||
| D12 | **국가 코드 매핑 테이블을 둔다** | 비표준 표기 존재, 49개국 (8.1) |
|
||||
|
||||
### 이 분석이 해소한 기존 미해결 항목
|
||||
|
||||
`design/00-DATA-SOURCE-DECISION.md` 부록 B 의 항목들이 다음과 같이 해소됐다.
|
||||
|
||||
| 기존 미해결 항목 | 해소 상태 |
|
||||
|---|---|
|
||||
| 전체 DMF 등록 건수(`totalCount`)는? | ✅ **9,084건** (2026-09-02) |
|
||||
| 응답에 `DMF_PERMIT_NO` 중복이 존재하는가? | ✅ **중복 0** |
|
||||
| API 에 취하·말소된 등록번호가 남아 있는가? | 🟡 **남지 않을 가능성 높음** (756건 차이). 연속 관측으로 확정 필요 |
|
||||
| `numOfRows` 실제 최대값 | ❌ 미해소 |
|
||||
| `type=json` 실동작 여부 | 🟡 프로토타입이 수집에 성공했으므로 API 자체는 동작 확인 |
|
||||
|
||||
---
|
||||
|
||||
## 부록 A. 재현 스크립트
|
||||
|
||||
이 분석을 재현하는 스크립트다. 데이터가 갱신되면 다시 돌려 비교한다.
|
||||
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""DMF_현황.xlsx 기준선 분석 재현 스크립트"""
|
||||
import re
|
||||
import collections
|
||||
import pandas as pd
|
||||
import openpyxl
|
||||
|
||||
XLSX = r"C:\Users\encep\OneDrive\문서\카카오톡 받은 파일\DMF_현황.xlsx"
|
||||
|
||||
|
||||
def inspect_structure(path: str) -> None:
|
||||
"""시트 구조와 서식 상태를 출력한다."""
|
||||
wb = openpyxl.load_workbook(path, data_only=True)
|
||||
for ws in wb.worksheets:
|
||||
rules = sum(len(r.rules) for r in ws.conditional_formatting)
|
||||
print(f"[{ws.title}] rows={ws.max_row} cols={ws.max_column} "
|
||||
f"freeze={ws.freeze_panes} filter={ws.auto_filter.ref} "
|
||||
f"cf_rules={rules} charts={len(ws._charts)}")
|
||||
|
||||
|
||||
def normalize_countries(value: str) -> str:
|
||||
"""콤마 다중값에서 중복을 제거하고 정렬해 재결합한다."""
|
||||
if not isinstance(value, str) or not value.strip():
|
||||
return ""
|
||||
parts = [p.strip() for p in value.split(",") if p.strip()]
|
||||
return ",".join(sorted(set(parts)))
|
||||
|
||||
|
||||
def normalize_ingredient(value: str) -> str:
|
||||
"""성분명 그룹 키. 공백·중점·하이픈·괄호를 제거한다."""
|
||||
return re.sub(r"[\s\u00a0()()·・\-–—,]", "", str(value)).lower()
|
||||
|
||||
|
||||
def normalize_entity(value: str) -> str:
|
||||
"""업체명 그룹 키. 법인 표기를 통일한다."""
|
||||
s = str(value).replace("㈜", "(주)")
|
||||
s = re.sub(r"주식회사", "(주)", s)
|
||||
return re.sub(r"[\s\u00a0().]", "", s).lower()
|
||||
|
||||
|
||||
def classify_permit_no(value: str) -> str:
|
||||
"""등록번호 포맷을 4종으로 분류한다."""
|
||||
if re.match(r"^\d{8}-\d+-[A-Z]+-\d+(-\d+)?$", value):
|
||||
return "std"
|
||||
if re.match(r"^\d{8}-\d+-[A-Z]+-\d+(-\d+)?\([^)]*\)$", value):
|
||||
return "std_paren"
|
||||
if re.match(r"^수\d+-\d+-[A-Z]+", value):
|
||||
return "new_substance"
|
||||
return "other"
|
||||
|
||||
|
||||
def analyze(path: str) -> None:
|
||||
df = pd.read_excel(path, sheet_name="전체", dtype=str)
|
||||
print(f"행수={len(df)} 고유등록번호={df['등록번호'].nunique()}")
|
||||
|
||||
issued = pd.to_datetime(df["발급일자"], errors="coerce")
|
||||
print(f"발급일자 {issued.min().date()} ~ {issued.max().date()}")
|
||||
|
||||
# 등록번호 앞 8자리 vs 발급일자
|
||||
std = df[df["등록번호"].str.match(r"^\d{8}-")]
|
||||
head = pd.to_datetime(std["등록번호"].str[:8], format="%Y%m%d", errors="coerce")
|
||||
same = (head == pd.to_datetime(std["발급일자"], errors="coerce")).sum()
|
||||
print(f"std {len(std)}건 중 앞8자리==발급일자 {same}건 ({same / len(std) * 100:.1f}%)")
|
||||
|
||||
# 국가 중복 표기
|
||||
redundant = df["제조국가명"].fillna("").apply(
|
||||
lambda v: "," in v and len({p.strip() for p in v.split(",")}) != len(v.split(","))
|
||||
)
|
||||
print(f"국가 중복 표기 {redundant.sum()}건")
|
||||
|
||||
# 표기 흔들림
|
||||
for name, fn, col in (
|
||||
("성분명", normalize_ingredient, "성분명"),
|
||||
("업체명", normalize_entity, "업체명"),
|
||||
):
|
||||
groups = collections.defaultdict(set)
|
||||
for v in df[col].fillna(""):
|
||||
groups[fn(v)].add(v)
|
||||
collisions = {k: v for k, v in groups.items() if len(v) > 1}
|
||||
print(f"{name} 표기 흔들림 그룹 {len(collisions)}개")
|
||||
|
||||
# 포맷 분포
|
||||
counts = collections.Counter(classify_permit_no(v) for v in df["등록번호"].fillna(""))
|
||||
print("등록번호 포맷:", dict(counts))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
inspect_structure(XLSX)
|
||||
analyze(XLSX)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 부록 B. 미해결 / 확인 필요
|
||||
|
||||
**사용자에게 확인해야 할 것**
|
||||
|
||||
- [ ] 이 xlsx 를 만든 **수집 스크립트가 어디에 있는가?** 있다면 재사용·개선의 출발점이 된다.
|
||||
- [ ] **`serviceKey` 를 이미 발급받았는가?** 프로토타입이 API 수집에 성공했으므로 키가 존재할 것이다. 어디에 저장돼 있는가.
|
||||
- [ ] 이 파일이 카카오톡으로 전달된 경위 — 본인이 만든 것인가, 다른 사람이 만든 것인가.
|
||||
- [ ] `신규` 시트가 비어 있는데, 기대하는 동작이 "당일 신규만" 인가 "최근 N일" 인가.
|
||||
- [ ] `갱신이력` 시트에 추가로 기록하고 싶은 항목이 있는가 (변경건수, 취하건수, 소요시간, 오류).
|
||||
|
||||
**데이터로 확인해야 할 것**
|
||||
|
||||
- [ ] **API 응답 건수 9,084 가 실제로 정상 건만인지** 연속 2일 이상 수집해 소멸 레코드를 관찰
|
||||
- [ ] 웹 9,840 − API 9,084 = 756건이 정말 취하·취소 건인지 표본 대조
|
||||
- [ ] 연차보고가 일어날 때 `발급일자`가 갱신되는가 (1~2월에 관측)
|
||||
- [ ] 변경 시 등록번호의 괄호 부분이 실제로 바뀌는가, 아니면 번호는 그대로이고 발급일자만 바뀌는가
|
||||
- [ ] `numOfRows` 최대값과 전량 수집에 필요한 호출 횟수
|
||||
- [ ] 제조국가명 결측 5건의 등록번호와 원인
|
||||
- [ ] "기타" 포맷 455건의 하위 패턴 분류
|
||||
|
||||
**설계에 반영해야 할 것**
|
||||
|
||||
- [ ] `research/01` 의 "API 로는 변경·취하 탐지 불가" 결론을 이 분석 결과로 정정
|
||||
- [ ] `research/07` 의 시트 컬럼 스펙을 API 7필드 + 파생 필드 기준으로 재작성
|
||||
- [ ] 국가 코드 매핑 테이블 작성 (49개국, 비표준 표기 포함)
|
||||
|
||||
---
|
||||
|
||||
*이 문서는 기준선 사실의 정본이다. 데이터가 갱신되면 부록 A 스크립트로 재분석하고 수치를 갱신한다.*
|
||||
1384
docs/design/01-architecture.md
Normal file
1384
docs/design/01-architecture.md
Normal file
File diff suppressed because it is too large
Load diff
2894
docs/design/02-data-model.md
Normal file
2894
docs/design/02-data-model.md
Normal file
File diff suppressed because it is too large
Load diff
3554
docs/design/03-xlsx-report-spec.md
Normal file
3554
docs/design/03-xlsx-report-spec.md
Normal file
File diff suppressed because it is too large
Load diff
4862
docs/design/04-onboarding-wizard.md
Normal file
4862
docs/design/04-onboarding-wizard.md
Normal file
File diff suppressed because it is too large
Load diff
433
docs/design/07-tdd-red-system.md
Normal file
433
docs/design/07-tdd-red-system.md
Normal file
|
|
@ -0,0 +1,433 @@
|
|||
# DMF Crawler TDD · E2E RED · 디자인 감사 운영 체계
|
||||
|
||||
> 이 문서는 DMF Crawler 의 **TDD/RED 운영 SSOT** 다.
|
||||
> 이론 근거는 `docs/research/11-tdd-red-and-design-audit-theory.md` 를 따른다.
|
||||
|
||||
**상태**: v1 확정
|
||||
**적용 시점**: 이 문서가 생성된 뒤의 모든 코드·GUI·리포트·에이전트 작업
|
||||
**핵심 명령**: **테스트 없이 새 코드를 쓰지 않는다. RED 실패를 먼저 본다.**
|
||||
|
||||
---
|
||||
|
||||
## 1. 법칙
|
||||
|
||||
### 1.1 신성한 루프
|
||||
|
||||
모든 구현 작업은 다음 순서를 따른다.
|
||||
|
||||
1. **RED** — 실패하는 자동화 테스트를 먼저 만든다.
|
||||
2. **RED 확인** — 그 테스트가 실제로 실패하며, 실패 이유가 기대한 요구 위반인지 확인한다.
|
||||
3. **GREEN** — 통과에 필요한 최소 구현만 한다.
|
||||
4. **REFACTOR** — 전체 관련 테스트가 Green 인 상태에서만 구조·중복·이름·디자인을 정리한다.
|
||||
5. **REGRESSION** — 영향 테스트 + 전체 smoke 를 돌리고 결과를 기록한다.
|
||||
|
||||
### 1.2 금지
|
||||
|
||||
- 테스트가 없는 `src/` 기능 구현 금지.
|
||||
- 실패를 보지 않은 테스트를 “RED” 라고 부르기 금지.
|
||||
- 테스트를 약하게 바꿔 Green 만들기 금지.
|
||||
- `pytest.skip`, `xfail`, 느슨한 `except Exception: pass`, `assert True` 류로 실패 숨기기 금지.
|
||||
- 실패 중에 리팩터링 금지.
|
||||
- 의미 없는 테스트를 무한히 추가하기 금지.
|
||||
- 디자인 감사를 “눈으로 대충 봄”으로 대체 금지.
|
||||
|
||||
### 1.3 예외
|
||||
|
||||
다음은 테스트 선행이 어려울 수 있다. 그래도 문서화가 필요하다.
|
||||
|
||||
| 예외 | 허용 조건 | 대체 증거 |
|
||||
|---|---|---|
|
||||
| 문서만 수정 | 코드 동작이 변하지 않음 | 링크/목차/SSOT 일관성 검사 또는 변경 요약 |
|
||||
| 설치 스크립트/작업 스케줄러 | 실제 Windows 권한이 필요 | dry-run/PowerShell parser/ASCII/BOM 검사 + 수동 프로브 로그 |
|
||||
| 실제 공공 사이트 접근 | 사용자 승인 필요 | fixture/mock 서버 E2E + 수동 실행 체크리스트 |
|
||||
| 순수 스타일 문구 | 자동화 어려움 | 디자인 RED 체크리스트와 스크린샷/수기 판정 기록 |
|
||||
|
||||
예외를 남길 때는 “왜 자동화 못 했는가 / 어떤 수동 증거로 대체했는가 / 나중에 자동화할 조건”을 적는다.
|
||||
|
||||
---
|
||||
|
||||
## 2. RED 계층 구조
|
||||
|
||||
### R0 — 계약/부팅 RED
|
||||
|
||||
목표: 프로젝트가 import/CLI/패키지 수준에서 깨지지 않음을 보장한다.
|
||||
|
||||
필수 검사:
|
||||
|
||||
```powershell
|
||||
.\.venv\Scripts\python.exe -m compileall -q src tests
|
||||
.\.venv\Scripts\python.exe - <<'PY'
|
||||
import importlib, pathlib
|
||||
for p in pathlib.Path('src/dmf_crawler').rglob('*.py'):
|
||||
if p.name == '__init__.py':
|
||||
continue
|
||||
importlib.import_module('.'.join(p.with_suffix('').relative_to('src').parts))
|
||||
PY
|
||||
.\.venv\Scripts\python.exe -m dmf_crawler --help
|
||||
.\.venv\Scripts\python.exe -m dmf_crawler doctor --json
|
||||
```
|
||||
|
||||
RED 예시:
|
||||
|
||||
- `cli.py` 가 없으면 `python -m dmf_crawler --help` 실패.
|
||||
- `storage/repo.py` 가 없으면 `pipeline.run_once()` 런타임 실패.
|
||||
- `.ps1` 에 BOM 이 없으면 PowerShell 5.1 한글 깨짐 위험.
|
||||
|
||||
### R1 — 도메인 단위 RED
|
||||
|
||||
대상:
|
||||
|
||||
- `normalize.py`
|
||||
- `diff.py`
|
||||
- `integrity.py`
|
||||
- `source_mfds.py` 파서
|
||||
- `agy/extract.py` JSON 추출
|
||||
|
||||
원칙:
|
||||
|
||||
- 네트워크 없음.
|
||||
- DB 없음.
|
||||
- clock/random 고정.
|
||||
- fixture 로 실패 재현.
|
||||
|
||||
대표 RED:
|
||||
|
||||
| 위험 | 테스트 |
|
||||
|---|---|
|
||||
| 등록번호 파서 회귀 | 표준/신물질/허여서/깨진 입력 10종 |
|
||||
| openpyxl 함정 재발 | nedrug xlsx 는 sheet XML 직접 파싱 요구 문서/테스트 |
|
||||
| API 오류 봉투 2종 | JSON 정상 봉투 + XML/JSON Gateway 오류 봉투 |
|
||||
| 조용한 빈 파일/빈 응답 | 200 OK + 헤더만/0건은 무결성 게이트 차단 |
|
||||
| 발급일자 변경 신호 | permit_date 변경 시 CHANGED |
|
||||
| 취하 오탐 | totalCount 급감/전건 취하율 임계 초과 시 diff 폐기 |
|
||||
|
||||
### R2 — 저장소/통합 RED
|
||||
|
||||
대상:
|
||||
|
||||
- `storage/db.py`
|
||||
- `storage/repo.py`
|
||||
- migrations
|
||||
- `pipeline.py`
|
||||
|
||||
필수 RED:
|
||||
|
||||
- 새 DB 에 migrations 0001~0003 적용.
|
||||
- 중복 적용 시 checksum 검증 후 무변화.
|
||||
- `start_run` → checkpoint → snapshot → events → report data 조회.
|
||||
- 같은 날짜 SUCCESS 중복 방지.
|
||||
- 기준선 첫 실행은 이벤트 0건.
|
||||
- 스냅샷 저장 전 무결성 실패면 DB 오염 없음.
|
||||
|
||||
### R3 — 파이프라인 E2E RED
|
||||
|
||||
목표: 사람이 실제로 쓰는 흐름을 fixture 로 끝까지 검증한다.
|
||||
|
||||
시나리오:
|
||||
|
||||
1. **첫 설치/키 없음**
|
||||
- `doctor` 가 API 키 없음 WARN 을 낸다. 인증키는 선택 기능이다.
|
||||
- `source.mode=auto` 는 API 키가 없으면 AGY headful Chrome CCBAC03 xlsx 수집을 시도한다.
|
||||
- `source.mode=api` 에서 키가 없거나 headful 수집이 실패하면 BLOCKED 가 아니라 마지막 성공 자료 PARTIAL 리포트로 폴백한다.
|
||||
2. **첫 수집 기준선**
|
||||
- fixture API 3건 → DB snapshot 3건.
|
||||
- diff 이벤트 0건.
|
||||
- 대시보드 배너 “기준선 수립일”.
|
||||
3. **둘째 날 변경**
|
||||
- 신규 1 / 변경 1 / 취하 1 fixture.
|
||||
- events 3건, report changes 3행.
|
||||
4. **무결성 차단**
|
||||
- 200 OK 이지만 0건.
|
||||
- diff/persist 스킵.
|
||||
- 마지막 성공 자료로 PARTIAL 리포트.
|
||||
5. **리포트 파일 잠김**
|
||||
- 대상 xlsx 잠김 또는 `~$` 파일 존재.
|
||||
- `_HHMMSS` 폴백 이름 사용.
|
||||
- 최신본 갱신 실패는 WARN 으로 기록.
|
||||
6. **AI 실패 독립성**
|
||||
- agy timeout/schema fail.
|
||||
- 리포트는 생성되고 AI 없음 배너만 표시.
|
||||
|
||||
### R4 — xlsx 디자인 감사 RED
|
||||
|
||||
DMF Crawler 의 가장 중요한 사용자 화면은 Excel 리포트다. xlsx 는 zip/XML 이므로 stdlib 로 검사한다.
|
||||
|
||||
필수 감사:
|
||||
|
||||
| 항목 | RED 조건 |
|
||||
|---|---|
|
||||
| 파일 구조 | `[Content_Types].xml`, `xl/workbook.xml`, worksheets, styles 존재 |
|
||||
| 시트 순서 | 대시보드가 첫 시트, 메타가 마지막, 워치리스트는 조건부 |
|
||||
| 시트명 | `00_대시보드` 등 SSOT 명칭과 1:1 |
|
||||
| 탭 색 | `report/theme.py` 의 `TAB_COLORS` 반영 |
|
||||
| 대시보드 viewport | zoom 90, 행/열 폭 합이 1366×768 무스크롤 목표 초과 금지 |
|
||||
| 틀 고정 | 변경분/전체현황/집계 시트에 freeze panes |
|
||||
| 자동필터/표 | 데이터 표가 ListObject 또는 autofilter 를 가짐 |
|
||||
| 내부 링크 | 대시보드 내비, 변경분→전체현황 링크 존재 |
|
||||
| 조건부서식 | 신규/변경/취하/워치/게이트 실패 서식 존재 |
|
||||
| 대비 | 팔레트 상태 배경/글자 contrast 최소 AA, 핵심 상태 AAA 목표 |
|
||||
| 색 단독 금지 | 신규/변경/취하가 라벨+기호+색으로 표현 |
|
||||
| 긴 텍스트 | 한글 긴 성분명/제조소/소재지 fixture 가 열폭 255 같은 폭주를 만들지 않음 |
|
||||
| 빈 상태 | 변경 0건, 워치리스트 없음, AI 없음이 깨진 표/빈 차트로 보이지 않음 |
|
||||
| 출처 | 식약처/공공데이터포털 출처 문구가 메타/푸터에 존재 |
|
||||
|
||||
### R5 — GUI 디자인·UX 감사 RED
|
||||
|
||||
대상: `tkinter` 온보딩/복구 GUI.
|
||||
|
||||
자동화 전략:
|
||||
|
||||
- 실제 창을 띄우되 가능하면 withdraw/offscreen 으로 둔다.
|
||||
- 위젯 트리(`winfo_children`)를 검사한다.
|
||||
- `Entry` 에 실제 텍스트를 넣고 버튼 command 를 호출한다.
|
||||
- 텍스트 길이를 늘려 레이아웃 clipping 을 검사한다.
|
||||
- geometry propagation 이후 `winfo_reqwidth/height` 와 screen/컨테이너 크기를 비교한다.
|
||||
|
||||
필수 감사 항목:
|
||||
|
||||
| 범주 | 실패 조건 |
|
||||
|---|---|
|
||||
| 끔찍한 중앙 정렬 | 폼 라벨/입력/버튼이 의미 없이 전체 중앙에 뭉쳐 스캔 불가 |
|
||||
| 시각적 치우침 | 주요 column/버튼 그룹의 x/y 정렬선이 크게 어긋남 |
|
||||
| 폰트 | 본문 10pt 미만, 대비 낮은 muted text, 제목/본문 hierarchy 없음 |
|
||||
| overflow | 긴 한글/영문/API 키/경로가 컨테이너 밖으로 나가거나 잘림 |
|
||||
| input label | 모든 입력에 visible label 또는 설명 텍스트 없음 |
|
||||
| input padding | 입력 내부 텍스트와 경계/버튼 간격이 너무 좁음 |
|
||||
| 실제 입력 | API 키/웹훅/워치리스트 텍스트를 넣어도 깨지지 않음 |
|
||||
| 버튼 클릭 | [키 입력], [다시 검사], [지금 실행], [로그 열기] command 가 연결됨 |
|
||||
| 복합 시나리오 | 키 없음→입력→검증 실패→오류 표시→재입력 흐름이 끊기지 않음 |
|
||||
| 아이콘/텍스트 | 아이콘만 있는 버튼에 텍스트/툴팁 없음, vertical align 어긋남 |
|
||||
| focus | Tab 순서가 논리적이고 기본 포커스가 위험 버튼에 가지 않음 |
|
||||
| 모달 | 닫기/나중에/복구 행동이 명확하고 ESC/창닫기 처리됨 |
|
||||
| 자연스러운 한국어 안내 | 온보딩 상단 안내가 `무엇:/왜:/어떻게:/다음:` 같은 AI식 라벨을 노출하지 않고, 현재 막힌 항목과 누를 행동을 1~3개의 자연스러운 문장으로 설명 |
|
||||
|
||||
### R6 — 브라우저 수집 E2E RED
|
||||
|
||||
브라우저 수집은 수집용 AGY가 visible/headful Chrome 을 조작하는 경로다. 요약용 AGY와 혼동하지 않는다.
|
||||
|
||||
- [ ] 전용 Chrome 프로필 Preferences 에 다운로드 경로가 고정된다.
|
||||
- [ ] CDP 포트 소유권 sentinel 을 확인하지 못하면 중단한다.
|
||||
- [x] `.tmp`/`.crdownload` 가 사라진 뒤 최종 xlsx 만 집는다. (`test_headful_browser_collect_rejects_incomplete_downloads`)
|
||||
- [x] 다운로드 파일이 최소 크기/zip 구조/행 수 임계 검증을 통과한다. (`test_headful_browser_collect_validates_download_and_returns_raw_records`)
|
||||
- [x] CCBAC03 웹 xlsx 헤더 alias(`신청인`, `제조국가`, `최초등록일자`, `최종변경일자`)를 RawRecord 표준 필드로 매핑한다. (`test_headful_browser_accepts_public_ccbac03_xlsx_headers`)
|
||||
- [x] 로그인 상태가 아니면 “세션 만료”로 감지하고 비밀번호를 agy prompt 로 보내지 않는다. (`test_headful_browser_prompt_contract_forbids_headless_http_and_secrets`)
|
||||
- [x] `mcp`/`execute_url` permission auto-deny 를 빈 JSON 오류로 뭉개지 않고 원인별로 보고한다. (`test_headful_browser_collect_reports_mcp_permission_denial_from_stderr`, `test_headful_browser_collect_reports_execute_url_permission_denial_from_stderr`)
|
||||
- [x] AGY 권한은 `mcp(chrome-devtools/*)`, `execute_url(nedrug.mfds.go.kr)`만 좁게 추가한다. `mcp(*)`, `execute_url(*)`, `--dangerously-skip-permissions` 금지. (`test_headful_mcp_allow_rule_is_scoped_and_does_not_use_dangerous_skip`)
|
||||
- [x] 프롬프트는 ambiguous 버튼 클릭 대신 visible Chrome 에서 `/pbp/CCBAC03/getExcel` 을 열고 wrong export 를 guard 한다. (`test_headful_browser_prompt_blocks_wrong_review_result_export`)
|
||||
- [x] `source.mode=auto` + API 키 없음은 AGY headful browser 로 라우팅한다. (`test_source_auto_without_api_key_routes_to_headful_browser`)
|
||||
- [x] `source.mode=browser` 는 API 키가 있어도 AGY headful browser 를 강제한다. (`test_source_browser_mode_routes_to_headful_even_when_api_key_exists`)
|
||||
- [x] 실제 nedrug 접근은 기본 테스트가 아니라 사용 승인 수동 smoke 로 분리한다. 2026-09-03 smoke `run_20260903_130332`: fetch/normalize/persist/report SUCCESS, 9,816건.
|
||||
|
||||
### R7 — 에이전트 지침/정책 RED
|
||||
|
||||
`AGENTS.md`, `CLAUDE.md`, 이 문서가 서로 어긋나면 agent 가 나쁜 습관으로 돌아간다.
|
||||
|
||||
필수 검사 후보:
|
||||
|
||||
- `AGENTS.md` 가 이 문서를 링크한다.
|
||||
- “No code without RED” 규칙이 존재한다.
|
||||
- “테스트 약화 금지”가 존재한다.
|
||||
- “디자인 감사 RED” 체크리스트가 존재한다.
|
||||
- “실패를 숨기지 말 것”이 존재한다.
|
||||
- `CLAUDE.md` 는 중복 SSOT 가 아니라 `AGENTS.md`/이 문서 포인터다.
|
||||
|
||||
---
|
||||
|
||||
## 3. RED 작성 품질 기준
|
||||
|
||||
새 RED 는 아래 8문항을 통과해야 한다.
|
||||
|
||||
1. 어떤 사용자/운영 위험을 막는가?
|
||||
2. 어느 요구사항/설계 문서를 근거로 하는가?
|
||||
3. 실패했을 때 메시지가 원인을 좁혀 주는가?
|
||||
4. 현재 코드에서 실제로 실패하는가?
|
||||
5. 실패 이유가 기대한 이유인가?
|
||||
6. network/clock/random/shared DB 에 의존하지 않는가?
|
||||
7. 같은 위험을 이미 더 강한 테스트가 덮고 있지 않은가?
|
||||
8. Green 후에도 리팩터링을 방해하지 않는 행동 중심 테스트인가?
|
||||
|
||||
통과하지 못하면 RED 가 아니라 메모다.
|
||||
|
||||
---
|
||||
|
||||
## 4. RED 통폐합·삭제 기준
|
||||
|
||||
TDD 는 계속 RED 를 늘리는 행위가 아니다. 테스트 스위트도 제품이다.
|
||||
|
||||
### 4.1 통합해야 하는 경우
|
||||
|
||||
- 같은 fixture 로 같은 실패를 여러 테스트가 반복한다.
|
||||
- 단위 테스트와 E2E 가 같은 단순 분기만 검사한다.
|
||||
- 실패하면 원인이 항상 같은 내부 구현 세부다.
|
||||
|
||||
처리:
|
||||
|
||||
- 가장 사용자 의미가 강한 테스트 하나를 남긴다.
|
||||
- 나머지는 helper/fixture 로 합친다.
|
||||
- 커밋/작업 로그에 “어떤 위험이 어디로 이동했는지” 기록한다.
|
||||
|
||||
### 4.2 삭제해야 하는 경우
|
||||
|
||||
- 구현 세부를 강제해 좋은 리팩터링을 막는다.
|
||||
- flaky 해서 신뢰를 깎는다.
|
||||
- 요구사항이 사라졌다.
|
||||
- 더 강한 상위 테스트가 같은 결함을 잡는다.
|
||||
|
||||
삭제 금지:
|
||||
|
||||
- 단지 Green 을 만들기 어렵다는 이유.
|
||||
- 실행 시간이 길다는 이유만으로 삭제. 먼저 계층 이동/fixture 축소/impact run 을 시도한다.
|
||||
|
||||
### 4.3 완화해야 하는 경우
|
||||
|
||||
디자인 임계값(예: 폭/대비/간격)은 완화 가능하지만, 다음이 있어야 한다.
|
||||
|
||||
- 실패 스크린샷 또는 xlsx XML 증거.
|
||||
- 사용자 유즈케이스에서 문제가 되지 않는 이유.
|
||||
- 완화 후에도 잡히는 결함 목록.
|
||||
|
||||
---
|
||||
|
||||
## 5. 작업 절차 — 에이전트 필수 프로토콜
|
||||
|
||||
### 5.1 시작 전
|
||||
|
||||
1. `docs/HANDOFF.md` 를 읽는다.
|
||||
2. 이 문서와 관련 설계 문서를 읽는다.
|
||||
3. 변경할 파일 목록을 정한다.
|
||||
4. 영향 테스트 목록을 쓴다.
|
||||
5. 아직 테스트가 없으면 먼저 RED 를 작성한다.
|
||||
|
||||
### 5.2 구현 중
|
||||
|
||||
1. RED 를 실행해 실패 로그를 확인한다.
|
||||
2. 최소 구현으로 Green 을 만든다.
|
||||
3. 관련 테스트를 다시 실행한다.
|
||||
4. Green 상태에서만 리팩터링한다.
|
||||
5. 전체 smoke 를 돌린다.
|
||||
|
||||
### 5.3 보고
|
||||
|
||||
보고에는 최소 다음을 포함한다.
|
||||
|
||||
- 추가/수정한 RED.
|
||||
- RED 실패가 기대한 이유였는지.
|
||||
- Green 을 위해 바꾼 파일.
|
||||
- 실행한 명령과 결과.
|
||||
- 남은 실패/미검증 항목.
|
||||
- 디자인 감사 결과(해당 시).
|
||||
|
||||
---
|
||||
|
||||
## 6. 영향 테스트 지도 v1
|
||||
|
||||
| 변경 파일 | 먼저 돌릴 테스트 | 그 다음 |
|
||||
|---|---|---|
|
||||
| `models.py` | 전체 import, dataclass contract tests | 전체 pytest |
|
||||
| `normalize.py` | `tests/test_normalize.py` | diff/integrity/report data |
|
||||
| `diff.py` | diff unit tests | pipeline scenario |
|
||||
| `integrity.py` | integrity gate tests | pipeline stale fallback |
|
||||
| `source_mfds.py` | API fixture parser tests | fetch E2E mock |
|
||||
| `storage/db.py` | migration tests | repo/pipeline/report data |
|
||||
| `storage/repo.py` | repo roundtrip tests | pipeline/report-only |
|
||||
| `pipeline.py` | pipeline fixture E2E | alerts/watchdog/report |
|
||||
| `agy/*` | polluted JSON/schema/budget tests | pipeline AI failure independence |
|
||||
| `report/data.py` | SQL fixture report data tests | xlsx design audit |
|
||||
| `report/*sheets*` | xlsx XML/visual audit tests | report-only smoke |
|
||||
| `gui/*` | tkinter structure/input/button tests | onboard smoke |
|
||||
| `notify/*`, `alerts.py` | template 4요소/DB state tests | notify-pump once |
|
||||
| `scripts/*.ps1` | BOM/parser tests | 수동 Windows registration probe |
|
||||
| `AGENTS.md`, `CLAUDE.md` | agent policy contract tests | docs link check |
|
||||
|
||||
---
|
||||
|
||||
## 7. 현재 프로젝트에 즉시 필요한 RED backlog
|
||||
|
||||
이전 구현 워크플로우가 중간에 끊겨 있으므로, 아래 RED 부터 만든다.
|
||||
|
||||
### 7.1 R0 계약 RED
|
||||
|
||||
- [x] `test_all_modules_import` — 현재 이름: `test_all_expected_runtime_entry_modules_import` (`tests/test_project_contracts.py`)
|
||||
- [x] `test_cli_help_exits_zero` (`tests/test_project_contracts.py`)
|
||||
- [x] `test_doctor_json_never_crashes_without_key` (`tests/test_project_contracts.py`)
|
||||
- [x] `test_powershell_scripts_have_bom` — 현재 이름: `test_powershell_scripts_have_utf8_bom` (`tests/test_project_contracts.py`)
|
||||
- [x] `test_cmd_files_are_ascii_only` (`tests/test_project_contracts.py`)
|
||||
|
||||
### 7.2 R2 저장소 RED
|
||||
|
||||
- [x] `test_migrations_apply_to_empty_db` — 현재 이름: `test_migrations_apply_to_empty_db_and_are_checksum_idempotent` (`tests/test_storage_repo.py`)
|
||||
- [x] `test_repo_baseline_roundtrip` — 현재 이름: `test_repo_baseline_roundtrip_creates_snapshot_without_events` (`tests/test_storage_repo.py`)
|
||||
- [x] `test_repo_changed_withdrawn_events_roundtrip` — 현재 이름: `test_repo_changed_new_withdrawn_events_roundtrip` (`tests/test_storage_repo.py`)
|
||||
- [x] `test_same_day_success_is_idempotent` — 현재 이름: `test_same_day_success_is_enforced_by_database_guard` (`tests/test_storage_repo.py`)
|
||||
|
||||
### 7.3 R3 파이프라인 RED
|
||||
|
||||
- [x] `test_run_without_service_key_uses_existing_baseline_instead_of_blocking`
|
||||
- [ ] `test_first_success_run_creates_baseline_report`
|
||||
- [ ] `test_empty_success_body_blocks_diff_and_preserves_previous_snapshot`
|
||||
- [ ] `test_agy_failure_does_not_block_report`
|
||||
|
||||
### 7.4 R4 xlsx 디자인 RED
|
||||
|
||||
- [x] `test_report_sheet_order_and_names` — `tests/test_report_e2e_design.py` 의 seeded DB → 실제 xlsx XML 감사가 검사한다.
|
||||
- [ ] `test_dashboard_has_no_scroll_viewport_budget`
|
||||
- [x] `test_report_has_freeze_panes_autofilter_internal_links` — `tests/test_report_e2e_design.py`
|
||||
- [x] `test_report_status_styles_are_not_color_only` — `tests/test_report_e2e_design.py` 의 상태 라벨/기호/조건부서식 감사가 검사한다.
|
||||
- [ ] `test_report_palette_contrast_ratios`
|
||||
- [x] `test_long_korean_values_do_not_create_extreme_column_widths` — `tests/test_report_e2e_design.py`
|
||||
|
||||
### 7.5 R5 GUI 디자인 RED
|
||||
|
||||
- [ ] `test_onboard_all_inputs_have_visible_labels`
|
||||
- [ ] `test_onboard_can_type_api_key_and_trigger_save_action`
|
||||
- [ ] `test_recover_message_explains_problem_and_action_in_natural_korean`
|
||||
- [ ] `test_main_actions_have_specific_button_labels`
|
||||
- [ ] `test_long_korean_error_text_does_not_exceed_window_budget`
|
||||
- [ ] `test_tab_order_reaches_primary_actions_before_secondary_actions`
|
||||
- [x] `test_onboarding_footer_buttons_render_visible_text_after_refresh` — 사용자 스크린샷 `C:\Users\encep\AppData\Local\Temp\pi-clipboard-5df73a57-a813-41be-b025-819e48bd73c2.png` 회귀. 하단 버튼이 빈 버튼처럼 보이면 실패해야 한다. (`tests/test_ux_regressions.py`)
|
||||
- [x] `test_onboarding_footer_buttons_keep_text_when_required_items_exist` — 필수/권장 조치가 있는 상태에서도 footer 버튼 텍스트가 짜부라지거나 사라지면 실패해야 한다. (`tests/test_ux_regressions.py`)
|
||||
- [x] `test_onboarding_row_action_buttons_are_not_clipped_inside_scroll_view` — 행별 액션 버튼이 스크롤 영역/창 하단에 잘리면 실패해야 한다. (`tests/test_ux_regressions.py`)
|
||||
- [x] `test_onboarding_new_grouped_layout_keeps_primary_actions_visible` — 새 그룹 레이아웃에서 주요 버튼이 아예 안 보이면 실패해야 한다. (`tests/test_ux_regressions.py`)
|
||||
- [x] `test_scroll_frame_mousewheel_scrolls_when_pointer_is_over_content` — scroll canvas 안의 실제 텍스트/카드 위에서 휠을 굴려도 스크롤되어야 한다. (`tests/test_ux_regressions.py`)
|
||||
- [x] `test_onboarding_status_panel_names_blockers_instead_of_generic_yellow_info_box` — 상단 안내문은 `무엇:/왜:/어떻게:/다음:` 같은 AI식 라벨이 아니라 자연스러운 한국어 문장이어야 한다. (`tests/test_ux_regressions.py`)
|
||||
- [x] `test_agy_google_login_is_optional_when_ai_disabled_for_api_mode` — 공식 API만 쓰고 AI 요약이 꺼져 있으면 AGY/Google 로그인을 요구하지 않는다. (`tests/test_ux_regressions.py`)
|
||||
- [x] `test_agy_is_prompted_for_headful_collection_when_auto_has_no_api_key` — auto+API 키 없음이면 AI 요약이 꺼져 있어도 최신 수집용 headful AGY 준비를 안내한다. (`tests/test_ux_regressions.py`)
|
||||
- [x] `test_agy_is_prompted_for_forced_browser_mode_even_with_api_key` — browser 모드는 API 키가 있어도 수집용 headful AGY 준비를 안내한다. (`tests/test_ux_regressions.py`)
|
||||
|
||||
### 7.6 R6 브라우저 수집 RED
|
||||
|
||||
- [x] `test_headful_browser_prompt_contract_forbids_headless_http_and_secrets`
|
||||
- [x] `test_headful_browser_collect_validates_download_and_returns_raw_records`
|
||||
- [x] `test_headful_browser_collect_rejects_incomplete_downloads`
|
||||
- [x] `test_headful_browser_collect_reports_mcp_permission_denial_from_stderr`
|
||||
- [x] `test_headful_browser_collect_reports_execute_url_permission_denial_from_stderr`
|
||||
- [x] `test_headful_mcp_allow_rule_is_scoped_and_does_not_use_dangerous_skip`
|
||||
- [x] `test_headful_browser_prompt_blocks_wrong_review_result_export`
|
||||
- [x] `test_headful_browser_accepts_public_ccbac03_xlsx_headers`
|
||||
- [x] `test_source_auto_without_api_key_routes_to_headful_browser`
|
||||
- [x] `test_source_browser_mode_routes_to_headful_even_when_api_key_exists`
|
||||
- [ ] 전용 Chrome profile Preferences / CDP 포트 sentinel / 스케줄러 headful smoke RED
|
||||
|
||||
---
|
||||
|
||||
## 8. 이 문서와 다른 문서의 관계
|
||||
|
||||
| 문서 | 관계 |
|
||||
|---|---|
|
||||
| `docs/00-REQUIREMENTS.md` | 사용자 요구 SSOT. 이 문서는 테스트 운영 방법 SSOT. |
|
||||
| `docs/design/01-architecture.md` | 아키텍처 SSOT. 이 문서는 검증 게이트를 추가한다. |
|
||||
| `docs/design/03-xlsx-report-spec.md` | xlsx 디자인 명세 SSOT. R4 RED 는 이 문서를 실행 가능하게 만든다. |
|
||||
| `docs/design/04-onboarding-wizard.md` | GUI 명세 SSOT. R5 RED 는 이 문서를 실행 가능하게 만든다. |
|
||||
| `docs/research/11-tdd-red-and-design-audit-theory.md` | 이론/근거 아카이브. |
|
||||
| `AGENTS.md` | AI agent 실무 지침. 이 문서를 압축해 적용한다. |
|
||||
| `CLAUDE.md` | Claude Code 진입 포인터. 중복 정본이 아니다. |
|
||||
|
||||
---
|
||||
|
||||
## 9. 미해결 항목
|
||||
|
||||
- [x] `tests/test_project_contracts.py` 를 만들어 R0 계약 RED 를 실제 코드화한다.
|
||||
- [x] xlsx XML 감사 RED 를 실제 코드화한다. 현재 파일: `tests/test_report_e2e_design.py`.
|
||||
- [x] tkinter 구조/입력/버튼 감사 RED 를 실제 코드화한다. 현재 파일: `tests/test_gui_design_contracts.py`, `tests/test_ux_regressions.py`.
|
||||
- [x] 브라우저 수집 경로 확정 후 R6 RED 를 코드화한다. 현재 파일: `tests/test_agy_headful_collection.py`.
|
||||
- [ ] 영향 테스트 지도를 자동 생성/검증할지 결정한다(TDAD 방식의 단순 텍스트 맵부터 시작).
|
||||
Loading…
Add table
Add a link
Reference in a new issue