# DMF 도메인 지식과 데이터 소스 정본 > ### ⚠️ 이 문서의 현재 지위: §0 핵심 결론 일부가 실측으로 반증됨 (부분 정정) > > 이 문서는 `design/00b-baseline-data-analysis.md` 의 실측(사용자가 이미 운영 중이던 `DMF_현황.xlsx` 9,084건 전수 분석, 2026-09-02) 이전에 작성되었다. > > **반증된 것**: §0 의 "공공데이터포털 OpenAPI 는 보조·검증용으로만 쓴다. API 단독으로는 변경·취하 탐지를 충족할 수 없다"는 결론은 실측으로 반증되었다. > - API 전체 건수 **9,084건** vs 웹 화면 **9,840건** — **756건 차이**. 같은 날 측정이고 양쪽 최신 날짜가 2026-09-01로 동일해 시차로는 설명되지 않는다. → API 는 취하·취소 건을 제외하고 정상 건만 반환한다는 강력한 증거. > - 등록번호 앞 8자리(최초 등록일)와 `발급일자`가 **44.5% 불일치**하며, 불일치 건은 전부 괄호가 붙은 갱신 건이다. → `발급일자`는 고정된 최초 등록일이 아니라 변경 때마다 갱신되는 **최종 갱신일**이다. > - 등록번호는 9,084건 전부 유일(**중복 0건**) — 완전한 자연 키다. > > 근거: `design/00b-baseline-data-analysis.md` §4~§6. > > **이 문서에서 여전히 유효하고 가치가 큰 부분**: DMF 제도 개요(§2), 법적 근거 약사법·규칙·고시(§2.3), 등록번호 체계 해부(§3), 화면 컬럼과 API 필드의 대응 관계(§4.5~4.6), RA 실무자 관점(§10), 해외 소스 조사(§8), robots.txt·저작권 사실(§9). > > **채택하지 않는 부분**: nedrug `/pbp/CCBAC03` 화면을 주 수집원으로 삼는 결론, "절제된 수집" 크롤링 실행 정책, 화면 엔드포인트(엑셀 POST·HTML 페이지네이션) 기반 수집 설계. 이 프로젝트는 HTML 을 크롤링하지 않고 공식 Open API 만 쓴다. 근거: `design/00-DATA-SOURCE-DECISION.md`. > > 절별 채택 여부는 문서 끝의 [이 문서와 확정 설계의 관계](#이-문서와-확정-설계의-관계) 표를 보라. --- > **이 문서의 역할**: DMF_Crawler 프로젝트가 "무엇을(어떤 제도의, 어떤 필드를), 어디서(어떤 URL·API·파일을), 어떤 규칙으로" 수집하는지를 확정하는 단일 정본(SSOT)이며, 이 문서만 읽고 수집기·데이터 모델·리포트 스키마를 구현할 수 있어야 한다. > > ⚠️ 위 상태 배너 참조 — 이 문서 작성 이후 확보된 실측 증거로 §0 의 일부 결론이 정정되었다. 구현 시 `design/00-DATA-SOURCE-DECISION.md` 와 `design/00b-baseline-data-analysis.md` 를 우선 확인하라. --- ## 0. 한눈에 보기 이 문서에서 내린 결론(구현팀은 이것만 먼저 읽어도 된다): - **주 데이터 소스는 의약품안전나라(nedrug.mfds.go.kr)의 `/pbp/CCBAC03` "원료의약품등록(DMF) 공고" 화면 1개로 확정한다.** JS 렌더링 불필요(Thymeleaf 서버사이드 렌더링), 순수 `GET /pbp/CCBAC03/getList` 로 목록 HTML을 받을 수 있고, `POST /pbp/CCBAC03/getExcel` 로 **전체 9,840건을 xlsx 한 방에** 받을 수 있음이 실측으로 확인되었다(2026-09-02 기준). > ⚠️ **채택하지 않음** — nedrug 화면을 주 수집원으로 삼는 결론은 이후 확정 설계와 어긋난다. 이 프로젝트는 HTML 크롤링을 하지 않고 공식 Open API 만 쓴다. 근거: `design/00-DATA-SOURCE-DECISION.md` - **공공데이터포털 OpenAPI(`MdcDmfInfoService01/getMdcDmfList01`)는 보조·검증용으로만 쓴다.** 응답 필드가 7개(등록번호·성분명·업체명·제조소명·제조소소재지·제조국가명·발급일자)뿐이라 **신규/변경/취하 탐지에 필수인 `최종변경일자`·`최종연차보고년도`·`취소/취하구분`·`취소/취하일자`·`문서번호`·`연계심사문서번호`가 없다.** 즉 API 단독으로는 이 프로젝트의 요구사항(변경·취하 탐지)을 충족할 수 없다. > ❌ **반증됨** — 실측으로 API 7필드만으로 신규·변경·취하 전부 탐지 가능함이 확인됐다. API 전체 건수 9,084건 대 웹 화면 9,840건(756건 차이, 같은 날 측정)은 API 가 취하·취소를 제외하고 정상 건만 반환한다는 증거이며, 등록번호 앞 8자리 대비 발급일자가 44.5% 불일치(불일치 건은 전부 괄호 붙은 갱신 건)한 것은 발급일자가 최종 갱신일로 동작함을 보여준다. 등록번호 중복도 0건(9,084건 전부 유일)이라 자연 키도 확보된다. 근거: `design/00b-baseline-data-analysis.md` §4, §5, §6 - **변경(diff) 탐지의 1차 키는 `등록번호(dmfPermitNo)`이며, 이 번호는 단순 문자열이 아니라 "등록수리일자-별표1 성분 일련번호-시행일 알파벳군-접수순번-동일성분 일련번호(허여서 괄호)"로 구성된 복합 의미 키다.** 파싱 규칙과 정규식을 §3에 확정했다. 단, `수6580-16-ND(20)` 같은 **신물질(신약) 계열 별도 포맷**이 공존하므로 파서는 2개 포맷을 모두 처리해야 한다. > ✅ **유효** — 등록번호가 자연 키라는 점과 파싱 규칙은 API 의 `dmf_permit_no` 필드에도 그대로 적용된다. 9,084건 중복 0건으로 재확인됨(`design/00b-baseline-data-analysis.md` §5.1). - **주간 공고 게시판(`/bbs/117`)은 2021-02-19(등록번호 bbscttNo=714, "2021년 2월 1,2주차")를 마지막으로 사실상 갱신이 끊겼다.** 따라서 "주간 공고 게시판 크롤링"이 아니라 **"공고 현황 테이블 일일 스냅샷 diff"**가 이 프로젝트의 올바른 아키텍처다. > ⚠️ **부분 채택하지 않음** — 게시판 갱신 중단 사실은 유효하다. 다만 "공고 현황 테이블(CCBAC03 화면)" 을 일일 스냅샷 diff 대상으로 삼는다는 결론은 채택하지 않는다. 이 프로젝트는 화면이 아니라 **API 응답을 일일 스냅샷으로 diff** 한다. 스냅샷 diff 라는 아키텍처 원리 자체는 API 로 재확인됐다. 근거: `design/00b-baseline-data-analysis.md` §6.3, `design/00-DATA-SOURCE-DECISION.md` - **`https://nedrug.mfds.go.kr/robots.txt` 는 `User-agent: * / Disallow: /` 로 전면 차단이다.** 반면 식약처 저작권정책은 저작권법 제24조의2에 따라 공표 저작물의 자유 이용을 허용한다. → **법적 이용은 가능하되 robots.txt를 존중하는 절제된 수집(1일 1회, 저빈도, User-Agent 명시, 엑셀 1회 다운로드)** 정책을 택한다. §9 참조. > ⚠️ **채택하지 않음** — robots.txt 준수를 전제로 한 HTML 크롤링 절제 정책이다. 이 프로젝트는 HTML 을 크롤링하지 않고 공식 API 만 쓰므로 이 정책은 적용 대상이 없다. robots.txt 실측 사실과 법적 검토(저작권법 제24조의2 등, §9)는 그대로 유효하며 `research/04-anti-bot-and-legal.md` 에도 보존되어 있다. 근거: `design/00-DATA-SOURCE-DECISION.md` - **해외 소스는 난이도 격차가 크다.** PMDA MF는 `.xlsx` 직접 다운로드가 200 OK로 성공(5,023행), EDQM CEP는 `EXPORT_WEB_CEP.txt` 전량 다운로드가 200 OK로 성공(1,459,534 bytes, 11컬럼 TSV). **FDA는 WAF(abuse-detection)로 봇 차단되어 404/403** — 브라우저 자동화 없이는 불가. > ✅ **유효** - **RA 실무 관점의 알림 축은 4개다: ① 성분(내가 다루는 API), ② 제조원(제조소명/소재지/국가), ③ 경쟁사(신청인), ④ 등록번호의 상태 변화(변경일자·연차보고·취하/취소).** 리포트 탭 설계는 이 4축을 그대로 시트로 매핑한다. > ✅ **유효** - **최종 수집 필드는 14개 원본 컬럼 + 파생 필드 9개로 확정**했다(§12). > ⚠️ **채택하지 않음** — 이 필드 집합은 CCBAC03 화면/엑셀 14개 컬럼 기준이다. 이 프로젝트는 API 7필드 + 파생 필드 9개를 수집 대상으로 채택하며, §12 를 전면 재작성했다. 근거: `design/00b-baseline-data-analysis.md` §3, §10 - 법적 근거는 **「약사법」 제31조의2 + 「의약품 등의 안전에 관한 규칙」 제15·16·17조 + 「원료의약품 등록에 관한 규정」(식약처 고시 제2024-89호, 2024.12.30. 시행)** 3단 구조이며, **공고 의무는 규칙 제16조 후단·제17조제3항 후단의 "인터넷 등으로 공고하여야 한다"** 에서 나온다. > ✅ **유효** - 처리기한은 **등록 20일(신약 원료 및 규칙 제15조제1항제1호가목 단서 해당 시 90일), 변경등록 20일(동 단서 자료 제출 시 90일)** 이다. 수수료는 등록 802,000원(전자민원)/887,000원(방문·우편), 변경등록 401,000원/443,000원. > ✅ **유효** --- ## 1. 목차 1. [목차](#1-목차) 2. [DMF(원료의약품 등록) 제도 개요](#2-dmf원료의약품-등록-제도-개요) - 2.1 [제도 정의와 도입 배경](#21-제도-정의와-도입-배경) - 2.2 [연혁 타임라인 (원문 그대로)](#22-연혁-타임라인-원문-그대로) - 2.3 [법적 근거 3단 구조](#23-법적-근거-3단-구조) - 2.4 [등록대상 원료의약품](#24-등록대상-원료의약품) - 2.5 [등록 절차·처리기한·수수료](#25-등록-절차처리기한수수료) - 2.6 [변경등록 vs 변경보고(연차보고)](#26-변경등록-vs-변경보고연차보고) - 2.7 [취하·취소·반려](#27-취하취소반려) - 2.8 [공고 제도 — 이 프로젝트의 법적 원천](#28-공고-제도--이-프로젝트의-법적-원천) - 2.9 [등록 공고 건수 통계 (연도별)](#29-등록-공고-건수-통계-연도별) 3. [등록번호 체계 완전 해부](#3-등록번호-체계-완전-해부) 4. [의약품안전나라 `/pbp/CCBAC03` — 화면 구조 조사 (수집 대상 아님)](#4-의약품안전나라-pbpccbac03--화면-구조-조사-수집-대상-아님) 5. [의약품안전나라 `/bbs/117` — 주간 공고 게시판(레거시)](#5-의약품안전나라-bbs117--주간-공고-게시판레거시) 6. [공공데이터포털 OpenAPI](#6-공공데이터포털-openapi) 7. [식의약 데이터 포털 / 기타 국내 채널](#7-식의약-데이터-포털--기타-국내-채널) 8. [해외 DMF 소스 (FDA / EDQM / PMDA)](#8-해외-dmf-소스-fda--edqm--pmda) 9. [robots.txt·저작권·이용약관·크롤링 정책](#9-robotstxt저작권이용약관크롤링-정책) 10. [제약사 RA 실무자의 모니터링 관점](#10-제약사-ra-실무자의-모니터링-관점) 11. [언론·유사 서비스가 DMF를 다루는 방식](#11-언론유사-서비스가-dmf를-다루는-방식) 12. [수집 대상 필드 확정안](#12-수집-대상-필드-확정안) 13. [부록 A. 출처 목록](#부록-a-출처-목록) 14. [부록 B. 미해결 질문 / 실측 필요 항목](#부록-b-미해결-질문--실측-필요-항목) --- ## 2. DMF(원료의약품 등록) 제도 개요 ### 2.1 제도 정의와 도입 배경 DMF(Drug Master File, 국내 명칭 **원료의약품 등록제도**, 영문 약칭 **KDMF**)는 완제의약품을 제조할 때 **등록된 주성분(원료의약품)만을 사용하도록 의무화**한 제도다. 원료의약품 제조원의 영업비밀(제조방법·노하우)을 완제 허가신청자에게 노출하지 않으면서도 규제기관이 품질을 심사할 수 있게 하는 구조다. 「원료의약품 등록 제도(DMF) 해설서 제5개정판(민원인안내서)」(2020.12., 허가총괄담당관, 안내서-0225-04) Ⅰ장 원문: > □ 원료의약품 등록 제도 도입 > ○ 원료의약품 품질개선 등을 통해 유통 의약품의 안전 및 품질을 확보할 수 있도록 **2002년 7월 원료의약품 등록제도(Drug Master File)를 도입** > ○ 미국, 유럽 등과 같이 전체 유효성분을 그 대상으로 하여야 하나, 제약산업 환경, 관련 업계의 준비기간 등을 감안하여 **매년 국민 다소비 성분을 우선하여 단계적으로 대상 성분을 확대중**임 해설서 문의처: 허가총괄담당관, 전화 **043-719-2312, 2325**, 팩스 **043-719-2300**. 해설서 제·개정 이력(원문 표): | 연번 | 제·개정번호 | 승인일자 | 주요내용 | |---|---|---|---| | 1 | C0-2007-2-001 | 2007.11.28 | 제정 | | 2 | C0-2012-2-006 | 2012.10.30 | 관련 규정 개정사항 현행화, 변경등록/연차보고 대상 구분 및 제출자료 명확화 | | 3 | 안내서-0225-01 | - | 「식약처 지침서등의 관리에 관한 규정」 개정에 따른 등록번호 일괄 정비 (규제개혁담당관실-3761호, 2017.5.16) | | 4 | 안내서-0225-02 | 2018.12. | 관련 규정 개정사항 현행화 | | 5 | 안내서-0225-03 | 2019.12 | 관련 규정 개정사항 현행화 / 변경등록·변경보고 대상 구분 및 제출자료 명확화 / 허여서 품목 변경관리 개선 / 미분화 원료 등록 관리 방안 | | 6 | 안내서-0225-04 | 2020.12 | 원료-완제의약품 연계심사 절차 및 질의·응답 반영 / 「의약품동등성 확보 필요 대상 의약품 지정」(‘20.7.8.) 개정에 따른 추가된 등록대상 원료의약품 포함 / 심사시 잦은 ‘보완사항’ 관련 내용 구체 기술 / 적제개편 사항 업무처리 절차 모식도 반영 | 해설서 목차(구현 시 참조 지점): ``` I. 원료의약품 등록제도 개요 ............ 1 II. 관련 법령 ........................... 4 III.「원료의약품 등록에 관한 규정」해설 ... 24 IV. 관련 질의·응답 정리 ................. 64 ← 등록번호 체계(Q51)가 여기 있음 V. 신청양식 및 자가점검표 등 ............ 89 첨부1 원료의약품 (변경)등록 신청서 첨부2 원료의약품 등록증 첨부3 자가 점검표 첨부4 신청인 체크리스트 첨부5 자료공유허여서 양식 첨부6 원료의약품 등록 업무처리흐름도 붙임1 변경등록 및 변경보고 사항 비교표 붙임2 원료-완제 연계심사를 위한 세부 운영절차 ``` ### 2.2 연혁 타임라인 (원문 그대로) 해설서 Ⅰ장의 연혁을 **날짜·성분 수치 그대로** 보존한다. 이 타임라인은 §3의 등록번호 알파벳군(A~K) 해석과 1:1로 대응하므로 절대 요약하지 말 것. | 시점 | 내용 | |---|---| | '02. 07. 01 | 신규 신청되는 **신약 성분** | | '05. 09. 01 | 글리클라짓(당뇨병약) 등 **77개 성분** 추가 | | '06. 07. 01 | **인태반 함유** 원료의약품 추가 | | '06. 11. 14 | 돔페리돈(위장관조절제) 등 **22개 성분** 추가 (2008. 1. 1.부터 시행) | | '07. 12. 17 | 노르플록사신 등 **14개 성분** 추가 (2009. 1. 1.부터 시행) | | '08. 12. 31 | 세프메타졸 등 **10개 성분** 추가 (2010. 1. 1.부터 시행) | | '10. 04. 20 | 동일 유효성분으로 **염류 및 수화물이 다른 경우에도** 등록 대상으로 하고, 설피리드 등 **18개 성분** 추가 (2011. 1. 1.부터 시행) | | '11. 02. 10 | 나프록센 등 **67개 성분** 추가 (2013. 1. 1.부터 시행) | | '12. 03. 30 | **원료의약품 신고제에서 등록제로 변경** | | '12. 05. 17 | 원료의약품 신고지침 → **「원료의약품 등록에 관한 규정」으로 개정** | | '15. 12. 28 | 한약(생약)성분 **별표1의2 추가** (2018. 1. 1.부터 시행) | | '16. 06. 30 | 의약품동등성 확보가 필요한 의약품, 주사제의 원료의약품 등 **별표1 추가** (2017. 12. 25.부터 시행) — 고시 제2016-59호 | | '18. 08. 23 | 주사제 원료의약품 중 **퇴장방지의약품 해당 성분 및 영양소 보급 목적 제제의 원료의약품 제외**. 항생물질제제(약효분류번호 **610**) 주사제 제조(수입)판매품목허가(신고)자는 **2020년 8월 31일까지** 별표1 개정규정에 적합하여야 함 | | '19. 10. 02 | [별표 1] **209.** ‘의약품 동등성 확보 필요 대상의약품 지정’ 제2조 해당 원료의약품을 주성분으로 하는 의약품 추가 (퇴장방지의약품 및 동 지정 제2조 복합성분 중 별표1~별표4 비해당 비타민·무기질 제외) | | '20. 06. 04 | 제네릭의약품 품질강화 방안의 일환으로 **완제 품질심사 시 원료-완제 연계심사**를 위한 등록대상 원료의약품 심사 절차 개선 (제네릭의약품 품질심사 절차 개선 정책 '20.5.13. 발표 연계) | | '20. 07. 24 | 원료·완제 품질심사 연계를 위한 **세부 운영절차 마련** | 경과조치(해설서 §제2조 해설 원문): > ‘원료의약품 등록에 관한 규정’(식약처 고시, 제2016-59호, 2016.6.30.) 부칙 제5조에 따라 ‘17.12.25. 이전 「의약품 등의 안전에 관한 규칙」제4조제1항제3호나목에 따른 의약품동등성 확보가 필요한 의약품의 제조판매 또는 수입품목허가(신고)를 받은 자는 다음 각 호에 따른 기한까지 별표 1의 개정규정에 적합하도록 하여야 함. > 1. 「의약품동등성 확보 필요 대상 의약품 지정」(식약처 고시)[별표 1] ‘상용의약품’ : **2021년 12월 31일까지** > 2. 동 [별표 2] ‘고가의약품’ : **2022년 12월 31일까지** > 3. 동 [별표 3] ‘그 밖의 의약품 동등성 확보가 필요한 의약품’ 및 [별표 4] ‘생체를 이용하지 아니한 시험이 필요한 의약품’ : **2023년 6월 30일까지** > > 주1) [별표3] 그 밖의 의약품동등성 확보가 필요한 의약품 성분 추가(65. abiraterone acetate ~ 100. travoprost) — 식약처 고시 제2020-57호, 2020.7.8. 일부개정, 시행일: 신약지정 해제된 날부터 시행 **최근 규제완화 흐름 (2024~2025)** - 2024.05.09 히트뉴스 「수입 원료의약품 등록, 이제 'GMP 증명서'만 있으면 OK」: - 수입 원료의약품 등록 시 제조소 시설 자료·**GMP 11종 자료**·현장조사를 **GMP 증명서로 대체**. 증명서는 생산국 정부기관 또는 **PIC/S 가입 기관** 발급, 제조소 정보·유효기간·GMP 적합 여부 포함 필요. - 원료의약품 등록 기간 **기존 120일 → 20일(신약 90일)** 로 단축 예상. - 허가·적합판정 신청 시 제출 GMP 자료 **11종 → 4종** 간소화, 나머지는 **제조소 총람(SMF)** 으로 통합. - 총리령·고시 개정 소요 약 4~6개월, 2024년 5월 입법예고 시작 계획. - 「원료의약품 등록에 관한 규정」 일부개정: **식약처 고시 제2024-89호, 2024.12.30.** (한국제약바이오협회 공지 2025-01-02, 식품의약품안전처 의약품정책과-12825, 담당 의약품정책과 이지은 043-719-2609). 첨부 3종(개정고시/전문/요약 안내), 전문 파일명 `「원료의약품 등록에 관한 규정」 (제2024-89호, 2024.12.30) 전문.hwpx`. - 2025.06.26 데일리팜 「규제완화 여파...원료약 등록 1년새 237→653건」: **2025년 상반기 653건** vs **2024년 상반기 256건**(2.8배), 2024년 전체 545건, 2021년 상반기 537건(당시 역대 최고). 기사 서술 원문: "올해부턴 현장 실사가 폐지됐다. 또한 생산국 정부기관 또는 PIC/S 가입국이 발급한 GMP 증명서로 기존 자료를 대체했다." - 연도별 DMF 건수 추이(약업신문/데일리팜 계열 보도 기준, ⚠️ 원 기사 표기가 "2019년"을 두 번 쓰는 등 부정확 — **미검증**): 2017년 347건, 2019년 516건, 2019년 571건, 2020년 714건, 2021년 967건, 2022년 671건, 2023년 487건, 2024년 545건. - 2023.01.12 데일리팜 「필수약·공급중단 보고대상 의약품 'DMF 등록' 유예」: 국가필수의약품 및 생산·수입·공급중단 보고 대상 의약품에 대해 근거자료 마련 시 **DMF 등록 유예 절차** 가능. > **크롤러 설계 시사점**: 2021년(등록 의무화 완료 압박)·2025년(규제완화)처럼 **연간 건수가 2~3배 튀는 해**가 존재한다. 일일 신규 건수 이상탐지 임계값을 고정값으로 두지 말고 이동평균 대비 배수로 잡아야 한다. ### 2.3 법적 근거 3단 구조 #### (1) 「약사법」 제31조의2 (원료의약품의 등록 등) — 전문 > ① 신약의 원료의약품 또는 식품의약품안전처장이 정하여 고시하는 원료의약품을 제조하여 판매하려는 자는 총리령으로 정하는 바에 따라 그 성분ㆍ명칭과 제조방법 등 총리령으로 정하는 사항을 식품의약품안전처장에게 등록할 수 있다. > ② 식품의약품안전처장은 제1항에 따른 등록사항이 총리령으로 정하는 기준에 적합한지 여부를 검토하여 그 결과를 신청인에게 알리고, 그 내용을 원료의약품 등록대장에 기록하고 보관하여야 한다. **이 경우 해당 원료의약품의 성분 및 제조원 등 총리령으로 정하는 사항을 공고하여야 한다.** > ③ 제1항 및 제2항에 따라 등록된 사항 중 총리령으로 정하는 중요한 사항을 변경하려는 자는 식품의약품안전처장에게 변경등록을 하여야 한다. 다만, 그 밖의 사항을 변경하려는 자는 보고하여야 한다. > ④ 제1항부터 제3항까지의 규정에 따라 등록된 원료의약품은 제31조제2항에 따른 품목허가를 받거나 품목신고를 한 것으로 본다. > ⑤ 제1항부터 제3항까지에서 규정한 사항 외에 원료의약품의 등록ㆍ변경등록 또는 변경보고, 등록된 원료의약품의 공고 등에 필요한 사항은 총리령으로 정한다. 관련: **「약사법」 제42조(의약품등의 수입허가 등)** — 수입 원료의약품의 등록 근거(제42조제4항). **「약사법」 제2조제8호(신약 정의)**: > 8. "신약"이란 화학구조나 본질 조성이 전혀 새로운 신물질의약품 또는 신물질을 유효성분으로 함유한 복합제제 의약품으로서 식품의약품안전처장이 지정하는 의약품을 말한다. 정부24 민원안내(「원료의약품(등록신청, 등록사항 변경등록신청)」, 민원코드 `CappBizCD=14700000458`) 기준 근거 법령: **약사법 제31조 제2항·제42조**, **의약품 등의 안전에 관한 규칙 제15조 제1항**, **원료의약품 등록에 관한 규정**. 처리기간 표기: **20일 / 대체절차 90일**. 신청 방법: 인터넷·방문·우편. 문의: 식품의약품안전처 **043-719-2304**. #### (2) 「의약품 등의 안전에 관한 규칙」 제15·16·17조 — 전문 **제15조(원료의약품의 등록)** > ① 법 제31조의2제1항 또는 제42조제4항에 따라 등록대상 원료의약품을 등록하려는 자는 별지 제16호서식의 원료의약품 등록신청서(전자문서로 된 등록신청서를 포함한다)에 다음 각 호의 자료(전자문서를 포함한다) 등을 첨부하여 식품의약품안전처장에게 제출하여야 한다. 이 경우 특히 자료의 보호가 필요한 경우에는 다음 각 호의 자료를 원료의약품 공급자가 직접 식품의약품안전처장에게 제출할 수 있다. >  1. 원료의약품의 제조소에 관한 다음 각 목의 자료 >   가. 법 제31조제1항에 따른 시설에 관한 자료 >   나. 품목별로 실시상황이 별표 1의2의 원료의약품 제조 및 품질관리기준에 맞거나 이와 같은 수준 이상임을 증명하는 자료 또는 제4조제1항제4호가목에 따른 제조증명서 >  2. 원료의약품의 성분ㆍ명칭과 제조방법에 관한 다음 각 목의 자료 >   가. 물리화학적 특성과 안정성에 관한 자료 >   나. 제조방법, 포장, 용기 및 취급상의 주의사항 등에 관한 자료 >   다. 원료의약품의 시험성적서, 분석방법 및 사용된 용매 등에 관한 자료 >   라. 시험용 원료의약품(식품의약품안전처장이 품질검사를 위하여 특별히 필요하다고 인정하는 경우에만 해당한다) >  3.~6. 삭제 <2016. 10. 28.> > ② 제1항에도 불구하고 **제16조제1항에 따라 공고된 제조소에서 동일한 시설을 이용하여 제조된 원료의약품을 등록하려는 경우** 등 식품의약품안전처장이 정하는 경우에는 제1항제1호의 자료를 제출하지 아니할 수 있다. <신설 2016. 10. 28.> > ③ 식품의약품안전처장 또는 지방청장은 제1항에 따른 등록 신청이 식품의약품안전처장이 정하여 고시하는 등록기준에 적합한지를 판정하기 위하여 품질검사 또는 현장 조사를 할 수 있다. <개정 2014. 8. 21., 2016. 10. 28.> > ④ 제1항에 따른 원료의약품을 등록하려는 자는 식품의약품안전처장이 정하여 고시하는 수수료(외국에서 현지실사를 할 필요가 있는 경우에는 이에 드는 경비를 포함한다)를 내야 한다. <개정 2016. 10. 28.> > ⑤ 제1항 각 호에 따른 자료의 작성요령, 자료의 요건 및 면제 범위 등에 관한 세부 사항은 식품의약품안전처장이 정하여 고시한다. **제16조(원료의약품 등록대장과 등록증 등)** — **이 프로젝트의 데이터가 존재하는 이유** > 법 제31조의2제2항 및 이 규칙 제15조에 따라 식품의약품안전처장은 원료의약품 등록을 한 경우에는 원료의약품 등록대장(전자문서로 된 대장을 포함한다)에 다음 각 호의 사항을 적고, 신청인에게 별지 제17호서식의 원료의약품 등록증을 발급하여야 한다. **이 경우 식품의약품안전처장은 다음 각 호의 사항을 인터넷 등으로 공고하여야 한다.** >  1. 원료의약품의 성분ㆍ명칭 >  2. 등록자의 성명 >  3. 등록번호 및 등록 연월일 >  4. 제조소의 명칭 및 소재지 >  5. 제15조제2항에 따라 자료를 첨부하지 않은 경우 그 사실 > **해석**: 화면 컬럼 `성분명`, `신청인`, `등록번호`/`최초등록일자`, `제조소명`/`제조소소재지`가 각각 제16조 제1~4호에 직결된다. 즉 **법정 공고 항목 = 우리가 수집해야 할 최소 필드**다. **제17조(원료의약품 등록사항의 변경등록 신청 등)** > ① 법 제31조의2제3항 본문에 따라 원료의약품의 등록사항 중 다음 각 호의 어느 하나에 해당하는 **중요한 사항**을 변경하려는 자는 별지 제16호서식의 원료의약품 등록사항 변경등록 신청서(전자문서로 된 신청서를 포함한다)에 원료의약품 등록증, 변경사유서(전자문서로 된 사유서를 포함한다) 및 변경 사유를 증명할 수 있는 서류(전자문서를 포함한다)를 첨부하여 식품의약품안전처장에게 제출하여야 한다. >  1. **제조소 소재지의 변경.** 다만, 행정구역개편에 따라 소재지가 변경되는 경우는 제외한다. >  2. 원료의약품의 제조방법 중 별표 1 제1호파목에 따른 **원료약품**(촉매 및 유기용매 등을 포함한다), **제조공정 또는 제조단위 규모의 변경.** 다만, 제조단위 규모의 변경은 **10배 이상**으로 변경하는 경우만 해당한다. >  3. 원료의약품을 직접 담는 **용기나 포장의 재질, 저장 방법 또는 사용기간의 변경** >  4. 원료의약품 **분석방법의 변경**(대한민국약전, 법 제52조에 따른 의약품등의 기준 또는 제4조제1항제1호나목에 따른 공정서에 실려 있지 아니한 방법으로 변경하는 경우만 해당한다) >  5. 그 밖에 변경등록이 필요한 경우로서 식품의약품안전처장이 정하여 고시하는 등록사항의 변경 > ② 법 제31조의2제3항 단서에 따라 원료의약품의 등록사항 중 제1항 각 호에서 규정한 사항 외의 사항을 변경하려는 자는 식품의약품안전처장이 정하는 바에 따라 원료의약품 등록증을 첨부하여 **매년 1월 31일까지** 식품의약품안전처장에게 변경사항을 보고하여야 한다. > ③ 식품의약품안전처장은 제1항 또는 제2항에 따라 변경등록 신청 또는 변경보고를 받은 경우에는 제16조에 따른 원료의약품 등록대장 및 원료의약품 등록증에 변경사항을 적고, 원료의약품 등록증을 내주어야 한다. **이 경우 식품의약품안전처장은 변경사항을 인터넷 등에 공고하여야 한다.** > ④ 식품의약품안전처장 또는 지방청장은 제1항에 따른 변경등록 신청이 제15조제2항에 따른 등록기준에 적합한지를 판정하기 위하여 품질검사 또는 현장 조사를 할 수 있다. <개정 2014. 8. 21.> > ⑤ 제1항에 따라 변경등록을 신청하는 자는 식품의약품안전처장이 정하여 고시하는 수수료(외국에서 현지실사를 할 필요가 있는 경우에는 이에 드는 경비를 포함한다)를 내야 한다. 관련 규칙 조문(연계): **제48조(제조업자 등의 준수사항) 제5호 나목** — 원료의약품은 제조방법(합성, 발효, 추출, 그 밖의 방법)별로 **별표 1의2 원료의약품 제조 및 품질관리기준(원료 GMP)** 적합 판정 후 제조한 것을 판매. **제4조(제조판매ㆍ수입품목의 허가신청)**, **제8조(허가사항 등의 변경허가 신청 등)**, **제3조(의약품 등 제조소의 시설 기준 등)**, **제48조의2제5항(제조 및 품질관리기준 적합판정서)**. #### (3) 「원료의약품 등록에 관한 규정」 (식약처 고시) | 항목 | 값 | |---|---| | 정확한 명칭 | **원료의약품 등록에 관한 규정** | | 현행 고시번호 | **식품의약품안전처고시 제2024-89호** | | 시행일 | **2024. 12. 30.** | | 소관 부서 | 의약품정책과 (담당 이지은, 043-719-2609) | | 국가법령정보센터 | `https://law.go.kr/LSW/admRulLsInfoP.do?admRulSeq=2100000252602` (본문 미노출), `https://www.law.go.kr/LSW/admRulLsInfoP.do?admRulSeq=2100000150569`, `https://law.go.kr/LSW/admRulLsInfoP.do?admRulSeq=2100000198136` | | 이전 개정 | 제2021-8호 (시행 2021.2.15.) — 첨부파일 `원료의약품+등록에+관한+규정(제2021-8호,+2021.2.15).hwp` (43KB) / 제2016-59호 (2016.6.30.) / 제2020-57호(관련 고시 「의약품 동등성 확보 필요 대상 의약품 지정」, 2020.7.8.) | | 고시 전문 게시글 | `https://www.mfds.go.kr/brd/m_211/view.do?seq=14870&...` — 첨부 `「원료의약품 등록에 관한 규정」 (제2024-89호, 2024.12.30) 전문.hwpx` | > ⚠️ **미검증**: law.go.kr 및 mfds.go.kr 게시글 페이지는 WebFetch로 **본문(조문 텍스트)이 렌더링되지 않았다.** 조문 전문은 첨부 `.hwp/.hwpx` 안에 있다. 아래 §2.4·§2.5의 조문 인용은 **해설서 제5개정판(2020.12.)에 재수록된 조문 텍스트**를 출처로 하며, 2024-89호 개정으로 달라졌을 가능성이 있다 → 부록 B 체크리스트. ### 2.4 등록대상 원료의약품 「원료의약품 등록에 관한 규정」 **제2조(등록대상 원료의약품의 지정)** — 해설서 재수록 전문: > 「의약품 등의 안전에 관한 규칙」 제15조에 따른 등록대상 원료의약품은 다음 각 호와 같다. **다만, 희귀의약품, 유전자재조합의약품·세포배양의약품·생물학적제제·세포치료제·유전자치료제, 방사성의약품, 수출용의약품 및 약리활성이 없는 성분(부형제, 첨가제 등)은 제외한다.** >  1. 「약사법」 제2조제8호에 따른 신약 중 **2002년 7월 1일 이후** 식품의약품안전처에 의약품 제조판매·수입품목 허가가 **신청된** 신약의 유효성분으로 사용하는 **신물질 원료의약품** >  2. **별표 1**의 원료의약품과 그 **염류 및 수화물** >  3. **인태반 유래 원료의약품**(최종원액 과정 의약품 포함) >  4. **별표 1의2**의 등록 대상 **한약(생약)제제 원료의약품**과 그 혼합물(용매의 농도가 다른 의약품 포함) 해설(원문 요지 보존): - 신약 중 등록 대상은 2002.7.1. 이후 **허가일자가 아닌 ‘신청일자’ 기준**. - 최초 신물질 원료의약품은 **신약 품목허가 신청 이전에 원료 단독 등록 신청 불가**하며 신약 품목허가 신청과 **병행** 진행. - [별표 1] **209.**: 「의약품 등의 안전에 관한 규칙」제4조제1항제3호나목에 따른 **의약품동등성 확보가 필요한 의약품**이거나 「의약품 동등성 확보 필요 대상 의약품 지정」제2조에 해당하는 원료의약품을 주성분으로 하는 의약품. 단, 퇴장방지의약품 및 동 지정 제2조 복합성분 중 별표1~별표4 해당 비타민·무기질 제외. - 구체 범위: "‘89년 1월 1일 이후 신약을 제외한 **전문의약품**으로서 이미 제조판매(수입) 품목허가 받은 것과 성분이 동일한 **정제, 캡슐, 좌제, 산제, 과립, 점안제, 점이제, 폐에 적용하는 흡입제 또는 외용제제**에 사용하는 주성분(원료의약품)으로서 「의약품동등성확보 필요 대상 의약품지정」에 해당하는 성분(**염류, 수화물, 이성체 포함**)" - [별표 1] **210.** 주사제 원료의약품(퇴장방지의약품 및 영양소 보급 목적 제제 원료 제외): 고시 제2016-59호(2016.6.30.) 시행일 **‘17.12.25. 이후 적용**, 고시개정일 이전 품목허가(신고)된 품목은 적용대상 아님(변경 포함). 단 항생물질제제(분류번호 **610**) 주사제는 **’20.8.31.까지** 적합. - [별표 1] **211.** 신청사가 제조(수입)하여 등록하고자 하는 원료의약품 (**2018.1.1.부터 시행**) — §3의 J군 설명에 등장. - 완제품 ‘원료약품 및 그 분량’에 **‘○○과립’처럼 반제품 형태**로 표기된 경우 해당 반제품은 등록 대상. - 혼합물(주성분+주성분): 각각의 등록대상 원료의약품이 등록·공고되었다면 혼합물은 **반드시 등록대상은 아님**. 다만 혼합물로 판매하려면 혼합물로서 등록 필요. - 혼합물(주성분+첨가제): 혼합물로서 등록 **가능**. - 제조공정에 **미분화 공정**이 포함되고 미분화 원료로 관리하는 경우 **미분화 원료의약품으로 별도 등록 필요**(미분화 여부 **비고란 기재**). 미분화 등록 시 **입자도 규격 설정** 및 미분화 품질기준 적합 **안정성시험자료** 제출 필요. 용어 정의(규정 제1조의2 계열, 해설서 원문): - **“중요공정”**: 주요 핵심물질 생성 공정 및 그 이후 공정. - **“주요 핵심물질”**: 화학반응을 통해 화학적인 기본구조의 변화가 생긴 단계의 생성물질로서 **고체 상태로 분리 가능한 중간체**(용액 상태로 반응이 연속 진행되는 경우 제외). - **“제조단위” 또는 “로트”**: 동일제조공정으로 제조되어 균질성을 가지는 의약품의 일정한 분량. - **“제조번호” 또는 “로트번호”**: 일정한 제조단위분에 대하여 제조관리 및 출하에 관한 모든 사항을 확인할 수 있도록 표시된 번호(숫자·문자 또는 조합). > **크롤러 시사점**: 화면 컬럼 `대상의약품`의 실제 값은 **`별표1`, `신물질`** 두 가지가 실측 확인되었다. 규정 제2조 4호(별표1의2 한약(생약))와 3호(인태반)에 해당하는 값이 별도로 존재할 가능성이 높다 → 부록 B. ### 2.5 등록 절차·처리기한·수수료 #### (1) 「원료의약품 등록에 관한 규정」 제5조(등록처리기준 등) — 해설서 재수록 전문 > ① 식품의약품안전처장은 제3조제1항에 따른 원료의약품 등록신청서를 다음 각 호에 따라 처리하여 「의약품 등의 안전에 관한 규칙」 별지 제17호 서식에 따른 원료의약품 등록증(이하 “등록증”이라 한다)을 교부하고 **식품의약품안전처 홈페이지 등에 공고하여야 하며**, 제조소(수입품목의 경우 생산국의 제조소를 말한다) 실태조사를 실시하는 때에는 **실시 20일 전까지** 이를 원료의약품 등록신청인에게 통지하여야 한다. >  1. 「의약품 등의 안전에 관한 규칙」 제15조에 따라 원료의약품 등록신청서가 제출된 경우에는 **17주 이내(실태조사 불필요 시 13주)** 에 제출자료의 적합성을 검토하여 처리한다. >  2. 「의약품 등의 안전에 관한 규칙」 제4조제1항제7호에 따라 의약품 제조판매·수입 품목허가를 신청한 경우에는 **17주 이내(실태조사 불필요 시 13주)** 에 처리한다. >  2의1. 제1호 및 제2호에도 불구하고 **양도·양수로 인한 변경신청**일 경우에는 신청일로부터 **25일 이내** 처리한다. >  3. 제1호 및 제2호의 규정에도 불구하고 **국제공인기관 등록 등** 안전성 및 품질 등이 이미 확보되었다고 식품의약품안전처장이 인정할 수 있는 경우에는 제4조의 첨부자료 구비 여부만을 검토하여 처리할 수 있다. > ② 식품의약품안전처장은 **의약품 등의 수급상 불균형 등의 우려**가 있는 경우, 그 수급 문제 해소, 고시 시행일 등을 고려하여 **등록증 교부 및 인터넷 공고 등을 유보할 수 있다.** > ③ 제1항에 따른 제조소 실태조사는 「의약품 등의 안전에 관한 규칙」 별표 1에 따라 실시한다. > ④ 제조소 실태조사 등에 필요한 제반 소요비용은 「수익자부담 해외출장여비에 관한 규정」(식약처 예규)에 따라 **원료의약품 등록신청인이 부담**한다. > ⑤ 「의약품 등의 안전에 관한 규칙」제15조에 따른 등록기준: >  1. **제조소 관련 등록사항**: 규칙 별표 1의2 원료의약품 제조 및 품질관리기준의 적합성 (이 경우 **제조소의 명칭과 소재지 공고 시 제조소의 원료의약품 GMP 적합 평가 실시 여부를 추가하여 기재할 수 있다**) >  2. **성분·명칭과 제조방법 관련 등록사항**: 제4조에서 정한 자료요건 및 면제범위의 적합성 > ⑥ 「의약품 등의 안전에 관한 규칙」 제17조제1항제5호에 따른 그 밖의 변경등록이 필요한 경우는 제조방법 중 **“병원미생물의 불활화 또는 제거방법”** 을 말한다. > ⚠️ 제5조①의 **17주/13주**는 해설서(2020.12.) 시점 값이고, 2024~2025년 규제완화로 **20일/90일**로 바뀐 민원 처리기간과 병존한다(§(3) 참조). **크롤러가 "언제 공고에 뜨는가"를 예측할 때는 20일/90일 기준을 쓴다.** 제5조 관련 실태조사 운영(해설서 발췌): - 현장 조사를 **원칙**으로 하되, 이미 적합 인정된 제조소와 **동일 제조국 동일 회사**인 경우 현장 조사 면제 가능. - 실태조사 생략기간(의약품, 생물학적제제등·한약(생약)제제 제외): **무균제제(원료의약품) 3년 / 비무균제제(원료의약품) 5년**. 비무균은 **PIC/S 가입국 또는 EU 화이트리스트 국가 제조소** 중 요건 적합 PIC/S 국가 실사보고서 제출 시 최초 평가·생략기간 경과 모두 인정. - 생략기간 기산: 무균·비무균별로, 원료·완제별로 각각 **의약품의 실사 최종일부터 품목허가(신고) 또는 등록 신청일(접수일자 기준)까지**. - 실태조사 대상 예시: "**부적합, 취하(부적합 예상)**, 실사이력 대비 시설 등에 큰 변경이 있는 경우, 품목 허가심사 단계에서 제조원에 대한 제조 및 품질관리 실태 확인이 필요하다고 판단되는 경우, 신청인의 요청에 따라 실태조사의 필요성이 인정되는 경우 등" - 제조공정이 2개 이상 제조소로 나뉜 품목: **중요공정을 실시하는 1개 대표 제조소**에 대해 현장 실태조사 실시 가능. - 관련 지침: 「의약품등 품목별 사전 GMP 평가 운영 지침」(의약품품질과, 2016.12.), 「한약(생약)제제 품목별 사전 GMP평가 및 식물성 한약(생약) 원료의약품 등록 처리 지침」(한약정책과, 2017.04. → 2020.11.). #### (2) 업무처리 절차 (MFDS MaPP 지침서) 문서: **`MFDS/MaPP : GRP-MaPP-허가업무-04(지침서-0943-03)` 「원료의약품 등록 및 변경등록 업무」 (Drug Master File(DMF) registration and post-approval registration)** — 승인일 2015, **개정일 2025. 2. 20.(6개정)**, 작성 의약품허가총괄과 주무관 김남윤 / 검토 사무관 이근아 / 승인 과장 김영주. 정의(원문): - **DMF 심사부서**: 의약품심사부 또는 바이오생약심사부 내 **첨단의약품품질심사과, 생물제제과 또는 생약제제과**. - **의약품 통합정보시스템**: 인터넷으로 민원서류·구비자료 접수하는 식약처 전자민원시스템. **의약품안전나라(https://nedrug.mfds.go.kr)(대외용)** 및 **의약품 통합정보시스템(대내용)** 포함. 절차 단계: | 단계 | 내용 | |---|---| | 4.1 신청서 접수 | 담당자는 **매일** 「의약품 통합정보시스템」을 통해 접수 신청서·수수료 확인 | | 4.1.2 수수료 반환 | 「의약품 등의 허가 등에 관한 수수료 규정」 제6조. **예비심사 기간 중 반려·자진취하 시 납부 수수료의 80% 반환** | | 4.2 제출자료 요건 검토 | 「원료의약품 등록에 관한 규정」 제4조·제5조 | | 4.2.1 검토의뢰 | 규정 제4조제1항제3호·제4호·제5호·제6호 (**신약의 원료의약품 등록에 한함**), 의약품 행정포탈 ‘나의 민원’ 창의 ‘협의’ 버튼 사용 | | 4.3 보완 자료 요구 | 「민원 처리에 관한 법률 시행령」 제24조, 「의약품의 품목허가·신고·심사규정」 제55조 | | 4.3.2 재보완(2차) | 1차 보완 자료 제출 시점을 보완요구기간 종료로 보고 재검토. 일부 미제출 시 **10일**을 보완기간으로 2차 보완. 전부 미제출 시 **10일** 독촉(재보완에 해당) | | 4.3.3 보완기간 연장 | 민원인의 기간연장 요청은 **2회에 한함** | | 4.4 자진취하 수리 | 「민원 처리에 관한 법률 시행령」 제25조제3항. 자진취하원 수리·통지 | | 4.5 반려 | 동 시행령 제25조. 재보완 기간 내 미제출, 제출자료 부적합 시 | | 4.6 민원처리기간 연장 | 동 시행령 제21조 | | 4.7 (변경)등록 | 검토서 작성 → 적합 시 (변경)등록 완료 → **‘원료의약품 등록증’ 발급** | | **4.8 원료의약품 등록 현황 공고** | **"식품의약품안전처장은 관련 규정에 따라 원료의약품 등록, 변경등록, 변경보고 현황을 우리처 홈페이지에 공고한다. * 「의약품 통합정보시스템」에서 식약처 담당자가 원료의약품 민원 처리 완료시 의약품안전나라(https://nedrug.mfds.go.kr)에 등록, 변경등록, 변경보고, 현황이 공고됨" ※ 관련 규정: 의약품 등의 안전에 관한 규칙 제16조, 제17조** | 관련 양식(붙임): [붙임1] 원료의약품 (변경)등록 절차 흐름도, [붙임2] 등록·변경등록 신청서 양식(규칙 별지 제16호서식 <개정 2024.12.30.>), [붙임3] 등록 검토서, [붙임4] 변경등록 검토서, [붙임5] 자료 보완 요청 공문, [붙임6] 재보완 요청, [붙임7] 보완자료 독촉, [붙임8] 보완기간 연장 승인 회신, [붙임9] 민원 신청 자진취하 수리, [붙임10-1] 반려–자료미비, [붙임10-2] 반려–자료미제출, [붙임11] 민원처리기간 연장 통지서, [붙임12] 등록 공문, [붙임13] 변경등록 공문, [붙임14] 변경등록 공문–양도양수시, [붙임15] 원료의약품 등록증 양식. 등록/변경등록 공문 문구(붙임12·13, 원문): *"등록사항은 동 법 제31조의2제2항에 따라 우리 처 홈페이지에 공고할 예정이오니 「약사법」등 관계 법규를 준수하시기 바라며, 「지방세…」"* — **면허세 납부(해당 시·군·구) → 납부영수증 제출 → 등록증 교부** 흐름이 [붙임1]에 명시. 전자민원 접수처: **의약품 전자민원창구 `ezdrug.mfds.go.kr`** (신청서 양식 상단 안내: "의약품 전자민원창구(ezdrug.mfds.go.kr)에서도 신청할 수 있습니다.") #### (3) 처리기간·수수료 표 (MaPP 5장 원문) | 대상 | 구분 | 처리기간 | |---|---|---| | 원료의약품 **등록** 신청 | 기본 | **20일** | | 원료의약품 등록 신청 | **신약의 원료의약품** / 「의약품 등의 안전에 관한 규칙」제15조제1항제1호가목 **단서**\*에 따른 등록 | **90일** | | 원료의약품 **변경등록** 신청 | 기본(**양도양수 포함**) | **20일** | | 원료의약품 변경등록 신청 | 동 단서\* 자료를 제출하여 변경등록하는 경우 | **90일** | \* 단서 = *제조판매품목이 적합판정서 사본을 제출할 수 없는 불가피한 사유가 있는 경우* | 종목 | 전자민원 | 방문·우편 민원 | |---|---|---| | 등록대상 원료의약품 **등록** 신청 | **802,000원** | **887,000원** | | 등록대상 원료의약품 **변경등록** 신청 | **401,000원** | **443,000원** | ### 2.6 변경등록 vs 변경보고(연차보고) 해설서 원문: > 「의약품 등의 안전에 관한 규칙」제17조제1항에 따른 **중요한 변경(Major Changes)** 과 같은 조 제2항에 따른 **경미한 변경(Minor Changes)** 으로 구분하고 있으며, 중요한 변경사항은 **「변경등록」**으로, 경미한 변경사항은 **「변경보고(연차보고)」**로 갈음될 수 있음. > - 변경등록 대상인 경우에는 **변경 등록이 완료된 후** 해당 원료의약품을 제조(수입)·판매하여야 하며, > - 변경보고 대상인 경우에는 변경된 내용으로 제품을 제조(수입)·판매하되, **다음해 1월 말까지** 동 변경사항을 식약처장에게 보고하여야 함. > - 변경보고의 경우 ‘원료의약품의 연차보고 간소화 방안 알림(´16.12.16, 의약품심사조정과)’에 따라 변경보고 민원 신청 시 **등록증 사본을 첨부**하고, 변경보고 완료 후 신청인이 **등록증 이면 기재 및 공문 보관**하여 관리함 (원료의약품 등록증 원본 우편 제출 불필요). > - 변경보고 대상임에도 업체에서 신속히 변경하고자 하는 경우 **변경등록이 가능함** (예: **신청인 명칭변경, 수입자 주소변경(도로명 주소 반영 등), 단순 제조소의 명칭 변경** 등). 기타 Q&A(해설서 Ⅳ장): - **Q50** 공정서(USP, EP) 각조 개정에 따른 **중금속 항 삭제**는 변경보고 대상인가? > A. 원칙적으로 식약처장이 인정하는 공정서 개정 사항(‘중금속 시험항목’ 삭제 포함)은 **변경보고 대상에서 제외**되며, 다만 업체는 「의약품의 품목허가·신고·심사규정」(식약처 고시) [별표 1의2] 공정서 및 의약품집 범위 지정 및 「원료의약품 등록에 관한 규정」(식약처 고시) **제6조**에 따라 해당 공정서가 개정되는 경우 그 최신의 개정판을 적용하여 운영하여야 함. - 허여서(자료공유허여서) 품목: *"허여서 품목의 변경관리 적정화를 위해 통상 최초 신청인의 변경등록 완료 시까지 미제출하는 경우 **약사감시 대상**으로 분류됨(필요시 행정처분 실시)."* > **크롤러 시사점**: 화면의 `최종변경일자`가 바뀌면 **변경등록**, `최종연차보고년도`가 바뀌면 **변경보고(연차보고)** 이벤트다. **두 컬럼을 서로 다른 이벤트 타입으로 분리 집계해야 한다.** 그리고 연차보고는 **매년 1월 말**에 몰리므로 1~2월 스파이크는 정상이다. ### 2.7 취하·취소·반려 - **자진취하**: 민원인이 요청 → 담당자가 **자진취하원 수리·통지** (MaPP 4.4, 「민원 처리에 관한 법률 시행령」제25조제3항). 예비심사 기간 중 자진취하 시 수수료 80% 반환. - **반려**: 재보완 기간 내 자료 미제출, 제출자료 부적합 (MaPP 4.5). - **등록 취하 신청 시 의무**: 해설서 [첨부5] 자료공유허여서 관련 문구 — *"등록 취하 신청 시 **자료공유허여서를 제출한 모든 등록자**를 (포함) 또는 취하여부를 확인하고 그 결과를 제출할 것임을 약속합니다."* - 화면 컬럼 **`취소/취하구분`** 의 실측 값: **`정상`**. 취하/취소 상태의 실제 코드값 문자열은 미확인 → 부록 B. ### 2.8 공고 제도 — 이 프로젝트의 법적 원천 | 층위 | 근거 | 공고 의무 문구 | |---|---|---| | 법률 | 약사법 제31조의2제2항 후단 | "해당 원료의약품의 **성분 및 제조원 등** 총리령으로 정하는 사항을 **공고하여야 한다**" | | 총리령 | 의약품 등의 안전에 관한 규칙 제16조 후단 | "식품의약품안전처장은 다음 각 호의 사항을 **인터넷 등으로 공고하여야 한다**" (성분·명칭 / 등록자 성명 / 등록번호 및 등록 연월일 / 제조소 명칭·소재지 / 제15조제2항 자료 미첨부 사실) | | 총리령 | 동 규칙 제17조제3항 후단 | "**변경사항을 인터넷 등에 공고하여야 한다**" | | 고시 | 원료의약품 등록에 관한 규정 제5조제1항 | "등록증을 교부하고 **식품의약품안전처 홈페이지 등에 공고하여야 하며**" | | 고시 | 동 제5조제2항 | 수급 불균형 우려 시 "등록증 교부 및 **인터넷 공고 등을 유보할 수 있다**" | | 지침 | MaPP 지침서-0943-03 §4.8 | "민원 처리 완료시 **의약품안전나라(https://nedrug.mfds.go.kr)에 등록, 변경등록, 변경보고, 현황이 공고됨**" | **과거 방식(2013~2021): 주간 공고문** - 예: **식품의약품안전처 공고 제2013-101호**, 「등록대상 원료의약품(DMF) 등록 공고(7월 둘째주)」, 게시일 **2013년 7월 8일**. - 근거 법령 표기: 「의약품 등의 안전에 관한 규칙」 **제15조, 제17조** 및 ‘원료의약품 등록에 관한 규정’(식약처고시) **제2조**, 그리고 「의약품 등의 안전에 관한 규칙」 **제16조, 제17조제3항**. - 본문: "등록사항을 **붙임1 및 2**(첨부파일 참조)와 같이 공고합니다" - 신청 유형 구분: **신규등록, 변경등록, 연차보고** - 공고 주기: **주간 단위(매주)** - 원문 출처: `http://www.mfds.go.kr/index.do?mid=70&pageNo=1&seq=14766&cmd=v`, 첨부는 전자민원창구 `http://ezdrug.mfds.go.kr` 확인 안내. - 게시판 `https://nedrug.mfds.go.kr/bbs/117` 의 마지막 글: **"등록대상 원료의약품(DMF) 등록 공고(2021년 2월 1,2주차, 2021.2.1.-2021.02.14)"**, 등록일 **2021-02-19**, 조회 58,193 (§5). **현재 방식(사실상): 상시 갱신 테이블** - `https://nedrug.mfds.go.kr/pbp/CCBAC03` 이 등록·변경등록·변경보고·취하 상태를 **누적 테이블**로 상시 공개. 2026-09-02 실측 총 **9,840건**, 최신 `최초등록일자` = **2026-09-01**. - **결론: 이 프로젝트의 "신규/변경/취하 탐지"는 공고문 파싱이 아니라 CCBAC03 테이블의 일일 스냅샷 diff 로 구현한다.** ### 2.9 등록 공고 건수 통계 (연도별) 해설서 Ⅰ장 「원료의약품 등록 공고 현황」(**‘20.11.30. 기준, 취하 포함**). **( ) 안의 숫자는 재공고된 숫자**를 의미함. | 구분 | ’03 | ’04 | ’05 | ’06 | ’07 | ’08 | ’09 | ’10 | ’11 | ‘12 | ‘13 | ‘14 | ‘15 | ‘16 | |---|---|---|---|---|---|---|---|---|---|---|---|---|---|---| | 신약 원료약품 | 9 | 12 | 18 | 21 | 28 | 30 | 54 | 60 | 64 (28) | 75 (17) | 93 (27) | 97 (23) | 122 (32) | 123 (32) | | 별도지정 고시품목 등 | - | - | 457 | 50 | 110 | 108 | 101 | 142 (3) | 844 (637) | 623 (375) | 676 (434) | 318 (204) | 312 (231) | 236 (184) | | **합 계** | **9** | **12** | **475** | **71** | **138** | **138** | **155** | **202 (3)** | **908 (665)** | **698 (392)** | **769 (461)** | **415 (227)** | **434 (263)** | **359 (216)** | | 구분 | ‘17 | ‘18 | ‘19 | ‘20 | 합계 | |---|---|---|---|---|---| | 신약 원료약품 | 130 (35) | 130 (47) | 117 (46) | 177 (43) | **1,360 (330)** | | 별도지정 고시품목 등 | 283 (187) | 404 (209) | 426 (266) | 515 (219) | **5,605 (2,949)** | | **합 계** | **413 (222)** | **534 (256)** | **543 (312)** | **692 (262)** | **6,965 (3,279)** | > **정합성 체크**: 2020.11.30. 누계 6,965건 → 2026-09-02 CCBAC03 총 **9,840건**. 5년 9개월간 약 2,875건 증가(연 ~500건)로, §2.2의 연도별 보도 수치(2021년 967건, 2022년 671건, 2023년 487건, 2024년 545건, 2025년 상반기 653건)와 대체로 정합. **초기 백필(backfill) 시 9,840건 전량을 받아 기준선으로 삼는다.** --- ## 3. 등록번호 체계 완전 해부 **이것이 이 프로젝트 데이터 모델의 핵심 키다.** ### 3.1 원본 근거 (식약처 FAQ, 해설서 제5개정판 Ⅳ장 Q51) > **Q51.** DMF 대상원료의약품 중 공고품목현황에서 등록번호는 어떻게 부여되는지 궁금합니다. 예를 들면 **"20110531-71-B-317-05"** 에서 앞에 8자리 숫자는 수리된 날짜인 것 같은데, 등록번호의 규칙이 있는지, 어떤 기준으로 번호가 매겨지는 것인지 궁금합니다. 또한 등록번호 뒤에 괄호가 있는 부분은 무슨 의미인지 > > **A.** > - **20110531** : 등록수리일자 > - **71** : 원료의약품 등록에 관한 규정(식약처고시) **[별표1]에 따른 각 성분의 일련번호** > - **B** : 원료의약품 등록에 관한 규정의 **부칙에 따른 시행일을 고려한 일련번호** > - **A~E** 는 별표1 제1호 내지 제99호의 성분, **2003년 1월 1일 - 2008년 1월 1일부터 시행** > - **F** 는 별표1 제100호 내지 제113호의 성분, **2009년 1월 1일부터 시행** > - **G** 는 별표1 제114호 내지 123호의 성분, **2010년 1월 1일부터 시행** > - **H** 는 별표1 제124호 내지 141호의 성분, **2011년 1월 1일부터 시행** > - **I** 는 별표1 제142호 내지 208호의 성분, **2013년 1월 1일부터 시행** > - **K** 는 별표1의2 제1호 내지 19호의 성분, **2018년 1월 1일부터 시행** > - **J** 는 별표1 **209호**의 「의약품동등성 확보 필요 대상 의약품 지정」성분 및 **210호**의 주사제 원료의약품(**2017년 12월 25일부터 시행**), **211호**의 신청사가 제조(수입)하여 등록하고자 하는 원료의약품(**2018년 1월 1일부터 시행**) > - **317** : **동일 알파벳 군에서 접수된 성분 순으로 부여한 일련번호** > - **05** : **동일 성분에 대하여 부여한 일련번호**(단, 일련번호 **‘J’는 제외**) > - **괄호** : 20110531-71-B-317-05**(1~9)** 에 해당하는 **동일제조소, 동일성분에 대하여 허여서로 등록된 순으로 부여한 일련번호** ### 3.2 자리별 해부표 | 위치 | 토큰 | 예시(`20110531-71-B-317-05(1)`) | 의미 | 타입 | 값 범위 / 규칙 | 파생 활용 | |---|---|---|---|---|---|---| | 1 | `accept_date` | `20110531` | **등록수리일자** (YYYYMMDD) | date | `(19\|20)\d{6}` | 등록 접수 시점 시계열 분석. `최초등록일자` 컬럼과 대개 일치하나 재공고 시 불일치 가능 | | 2 | `ingr_no` | `71` | 「원료의약품 등록에 관한 규정」 **[별표1] 성분 일련번호** | int | 1~211+ (별표1의2는 K군에서 1~19) | **성분 마스터 조인 키.** 동일 성분의 모든 등록건을 묶는 축 | | 3 | `group` | `B` | **부칙 시행일 군(群)** 알파벳 | char | `A`~`K` (관측: A,B,C,D,E,F,G,H,I,J,K) | 등록 의무 시행 시기 코호트 분류 | | 4 | `serial` | `317` | 동일 알파벳 군 내 **접수 순번** | int | 1~4자리+ | 군 내 누적 접수 순서 | | 5 | `sub` | `05` | **동일 성분 내 일련번호** (J군은 없음) | int? | 1~2자리, **J군 미부여** | 같은 성분에 대한 몇 번째 등록인지 = **경쟁 등록 수** | | 6 | `grant` | `(1)` | **허여서(자료공유허여서) 등록 순번** — 동일제조소·동일성분 | int? | `(1)`~`(9)` | 허여서 기반 파생 등록 식별. **원 등록번호와 묶어야 함** | **군(group) → 별표 호수 → 시행일 매핑표** (파서에 상수 테이블로 내장) | 군 | 별표 | 호수 범위 | 시행일 | |---|---|---|---| | A~E | 별표1 | 제1호 ~ 제99호 | 2003.1.1. ~ 2008.1.1. | | F | 별표1 | 제100호 ~ 제113호 | 2009.1.1. | | G | 별표1 | 제114호 ~ 제123호 | 2010.1.1. | | H | 별표1 | 제124호 ~ 제141호 | 2011.1.1. | | I | 별표1 | 제142호 ~ 제208호 | 2013.1.1. | | J | 별표1 | 제209호(의약품동등성 확보 필요 대상), 제210호(주사제 원료), 제211호(신청사 제조·수입 등록) | 209·210호: 2017.12.25. / 211호: 2018.1.1. | | K | 별표1의2 | 제1호 ~ 제19호 (한약(생약)제제) | 2018.1.1. | > **주의**: 알파벳 순서가 시행일 순서와 **완전히 일치하지 않는다.** `I`(2013) → `J`(2017/2018) → `K`(2018)이지만, J와 K는 별표가 다르다(J=별표1, K=별표1의2). **정렬 키로 알파벳을 그대로 쓰지 말고 매핑 테이블의 시행일을 쓸 것.** ### 3.3 실측 등록번호 샘플 (2026-09-02 CCBAC03 1페이지) | 등록번호 | 대상의약품 | 성분명 | 신청인 | 제조소명 | 제조국가 | 최초등록일자 | 파싱 결과 | |---|---|---|---|---|---|---|---| | `20260901-209-J-2270` | 별표1 | 탐스로신염산염 | 주식회사지맥스파마켐 | Hema Pharmaceuticals Pvt. Ltd. | 인도 | 2026-09-01 | date=2026-09-01, ingr=209, group=J, serial=2270, sub=없음(J군 규칙 일치 ✅) | | `20260901-209-J-2268` | 별표1 | 미분화부데소니드 | ㈜하이플 | Avik Pharmaceutical Limited | 인도 | 2026-09-01 | 동일 패턴. 성분명에 **‘미분화’ 접두어** 존재(§2.4 미분화 등록 규정과 일치) | | `수6580-16-ND(20)` | **신물질** | 프레가발린 | 엠피크코리아(주) | — | — | — | **별도 포맷.** §3.5 참조 | | `20121228-168-I-169-04` | (공공데이터포털 API 샘플값) | 포르모테롤푸마르산염수화물 | (주)대웅제약 | SICOR SOCIETA'ITALIANA CORTICOSTEROIDO S.R.L. , [미분화공정 제조소] Micro-Macinazione SA. | 이탈리아,스위스 | 2015-02-26(발급일자) | date=2012-12-28, ingr=168, group=I, serial=169, sub=04 | | `20110531-71-B-317-05` | (FAQ 예시) | — | — | — | — | — | date=2011-05-31, ingr=71, group=B, serial=317, sub=05 | > **중요 관측 1**: `20121228-168-I-169-04` 건은 **제조소명·제조소소재지·제조국가가 콤마로 다중 값**이다 (`SICOR ... , [미분화공정 제조소] Micro-Macinazione SA.` / `Rho(MI) - Via Terrazzano, 77, Italy , 6995 Madonna del Piano, Switzerland` / `이탈리아,스위스`). **제조소는 1:N 이다.** 화면 HTML에서도 `` 안에 중첩 `
...
` 로 여러 행이 들어간다. → **정규화 테이블(dmf_site) 분리 필요.** > > **중요 관측 2**: 등록수리일자(등록번호 접두 8자리)와 `최초등록일자` 컬럼이 같은 날인 신규 건이 다수지만(`20260901-...` ↔ `2026-09-01`), API 샘플처럼 **발급일자 2015-02-26 vs 등록번호 20121228** 로 크게 벌어지는 경우가 있다. **두 날짜를 반드시 별도 필드로 저장할 것.** ### 3.4 파싱 정규식 (확정안) ```python # -*- coding: utf-8 -*- """DMF 등록번호 파서 — dmf_crawler/domain/permit_no.py""" from __future__ import annotations import re from dataclasses import dataclass, asdict from datetime import date from typing import Optional # 포맷 A: 일반(별표1 / 별표1의2) 등록번호 # 20110531-71-B-317-05 (기본) # 20110531-71-B-317-05(1) (허여서 파생) # 20260901-209-J-2270 (J군: sub 없음) RE_STANDARD = re.compile( r"^(?P(?:19|20)\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01]))" r"-(?P\d{1,4})" r"-(?P[A-Z])" r"-(?P\d{1,6})" r"(?:-(?P\d{1,3}))?" r"(?:\((?P\d{1,3})\))?$" ) # 포맷 B: 신물질(신약 원료) 계열 — 예: 수6580-16-ND(20) # 접두 한글 1자(수/제 등) + 숫자 + '-' + 숫자 + '-ND' + 선택 괄호 숫자 RE_NEW_SUBSTANCE = re.compile( r"^(?P[가-힣]{1,2})(?P\d{3,7})" r"-(?P\d{1,3})" r"-(?PND)" r"(?:\((?P\d{1,3})\))?$" ) GROUP_TABLE = { "A": ("별표1", "제1호~제99호", "2003-01-01"), "B": ("별표1", "제1호~제99호", "2003-01-01"), "C": ("별표1", "제1호~제99호", "2003-01-01"), "D": ("별표1", "제1호~제99호", "2003-01-01"), "E": ("별표1", "제1호~제99호", "2008-01-01"), "F": ("별표1", "제100호~제113호", "2009-01-01"), "G": ("별표1", "제114호~제123호", "2010-01-01"), "H": ("별표1", "제124호~제141호", "2011-01-01"), "I": ("별표1", "제142호~제208호", "2013-01-01"), "J": ("별표1", "제209호·제210호·제211호", "2017-12-25"), "K": ("별표1의2", "제1호~제19호", "2018-01-01"), } @dataclass class PermitNo: raw: str fmt: str # "standard" | "new_substance" | "unknown" accept_date: Optional[date] = None ingr_no: Optional[int] = None group: Optional[str] = None group_table: Optional[str] = None # 별표1 / 별표1의2 group_range: Optional[str] = None group_effective_from: Optional[str] = None serial: Optional[int] = None sub: Optional[int] = None grant: Optional[int] = None # 허여서 괄호 번호 is_grant_derived: bool = False base_key: Optional[str] = None # 괄호 제거한 기준 등록번호 ingr_group_key: Optional[str] = None # 성분 코호트 키 "71-B" def to_dict(self) -> dict: d = asdict(self) if self.accept_date is not None: d["accept_date"] = self.accept_date.isoformat() return d def parse_permit_no(raw: str) -> PermitNo: """DMF 등록번호를 구조화한다. 어떤 입력이든 예외를 던지지 않는다.""" s = (raw or "").strip() # 전각 괄호/하이픈, 비단절 공백 정규화 s = (s.replace("(", "(").replace(")", ")") .replace("-", "-").replace("–", "-").replace("—", "-") .replace(" ", " ").replace(" ", "")) m = RE_STANDARD.match(s) if m: g = m.groupdict() d = g["accept_date"] table, rng, eff = GROUP_TABLE.get(g["group"], (None, None, None)) grant = int(g["grant"]) if g["grant"] else None base = s.split("(")[0] return PermitNo( raw=raw, fmt="standard", accept_date=date(int(d[0:4]), int(d[4:6]), int(d[6:8])), ingr_no=int(g["ingr_no"]), group=g["group"], group_table=table, group_range=rng, group_effective_from=eff, serial=int(g["serial"]), sub=int(g["sub"]) if g["sub"] else None, grant=grant, is_grant_derived=grant is not None, base_key=base, ingr_group_key=f'{g["ingr_no"]}-{g["group"]}', ) m = RE_NEW_SUBSTANCE.match(s) if m: g = m.groupdict() seq = int(g["seq"]) if g["seq"] else None return PermitNo( raw=raw, fmt="new_substance", serial=int(g["doc_no"]), sub=int(g["year_seq"]), grant=seq, base_key=s.split("(")[0], ) return PermitNo(raw=raw, fmt="unknown", base_key=s or None) if __name__ == "__main__": samples = [ "20110531-71-B-317-05", "20110531-71-B-317-05(1)", "20260901-209-J-2270", "20260901-209-J-2268", "20121228-168-I-169-04", "수6580-16-ND(20)", "이상한값", ] for x in samples: print(x, "->", parse_permit_no(x).to_dict()) ``` ### 3.5 신물질(신약) 포맷 `수6580-16-ND(20)` - 실측 1건: `수6580-16-ND(20)` / 대상의약품 = **신물질** / 성분명 = 프레가발린 / 신청인 = 엠피크코리아(주). - 구조 추정(⚠️ **미검증** — FAQ Q51은 별표1 포맷만 설명): `수` = 수입 문서 접두, `6580` = 문서/접수번호, `16` = 연도 또는 연차 일련번호, `ND` = New Drug(신약/신물질), `(20)` = 허여서/파생 순번. - **파서는 이 포맷을 절대 표준 포맷으로 강제 파싱하지 말고 `fmt="new_substance"` 로 분기**한다. `대상의약품 == "신물질"` 인 행은 대체로 이 포맷일 것으로 예상하되, 전수 검증은 부록 B 항목. ### 3.6 등록번호 기반 파생 지표 (리포트 시트에 직결) | 파생 지표 | 계산식 | RA 실무 의미 | |---|---|---| | `ingr_group_key` = `{ingr_no}-{group}` | 등록번호 2·3토큰 | **같은 성분의 모든 등록건** 묶음 → "이 성분에 몇 개 제조원이 붙었나" | | `sub` 최댓값 | 동일 `ingr_group_key` 내 max(sub) | 해당 성분의 **누적 등록 경쟁 강도** | | `is_grant_derived` | 괄호 존재 여부 | **허여서(자료공유) 파생 등록** — 원 등록자와 묶어 보여야 오해가 없다 | | `group_effective_from` | GROUP_TABLE | 등록 의무 코호트 — "언제부터 의무였던 성분인가" | | `accept_date` vs `최초등록일자` gap | 일수 차 | 심사 소요기간 추정(단, 재공고 건은 왜곡) | --- ## 4. 의약품안전나라 `/pbp/CCBAC03` — 화면 구조 조사 (수집 대상 아님) > ⚠️ 이 절은 수집 설계가 아니라 **화면 컬럼과 API 필드의 대응 관계를 파악하기 위한 조사 기록**이다. 이 프로젝트는 이 화면을 수집하지 않는다. 공식 Open API(§6)만 수집하며, HTML 크롤링(엑셀 POST 포함)은 하지 않는다. 근거: `design/00-DATA-SOURCE-DECISION.md`. 이 절에서 지금도 유효한 것은 §4.5(정렬 키=서버 필드명)와 §4.6(컬럼 목록)의 **화면 컬럼 ↔ 서버 필드명 대응관계**이며, 나머지(§4.2~4.4, §4.7~4.11)는 참고용 조사 기록으로만 보존한다. ### 4.1 페이지 식별 | 항목 | 값 | |---|---| | URL | `https://nedrug.mfds.go.kr/pbp/CCBAC03` | | `` | `의약품안전나라 > 의약품등 정보 > 의약품 및 화장품 품목정보 > 원료의약품등록(DMF) 공고 ` | | 보조 title | `현재메뉴 > 3차메뉴 > 2차메뉴 > 1차메뉴 > 식품의약품안전처 의약품통합정보시스템2` | | 메뉴 경로 | 의약품등 정보 → 의약품 및 화장품 품목정보 → **원료의약품등록(DMF) 공고** | | 총 건수 (2026-09-02 실측) | **9,840건** (`총 9,840건` / `총 9840건`) | | 페이지 수 | `totalPages` = **984** (limit=10) / **197** (limit=50) | | 렌더링 | **Thymeleaf 서버사이드 렌더링** — JS 실행 불필요. HTML 소스에 `th:onclick`, `th:text` 주석이 그대로 남아 있음 | | 페이지 크기(HTML) | 1페이지 limit=10 → **400,143 bytes** / limit=50 → **501,975 bytes** | ### 4.2 HTTP 응답 헤더 (실측) ``` HTTP/1.1 200 OK Date: Wed, 02 Sep 2026 12:57:38 GMT Server: Apache Allow: GET, POST, OPTIONS Strict-Transport-Security: max-age=63072000 Expires: 0 Cache-Control: no-cache, no-store, max-age=0, must-revalidate Access-Control-Allow-Headers: Origin, Content-Type, content-type, Content-Style-Type, Accept, Authorization,DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,If-Modified-Since, x-requested-with, Content-Security-Policy, X-UA-Compatible, X-Content-Type-Options, X-FRAME-OPTIONS, Cache-Control, Pragma X-XSS-Protection: 1; mode=block Pragma: no-cache Access-Control-Allow-Origin: *.mfds.go.kr Strict-Transport-Security: max-age=31536000 ; includeSubDomains X-Content-Type-Options: nosniff Content-Language: ko-KR Access-Control-Allow-Methods: GET, POST, OPTIONS Set-Cookie: key=value; SameSite=Lax;Secure;;HttpOnly;Secure Set-Cookie: JSESSIONID=fM1_XKbx-MbQK_vNXBB34GnMHg_cxN9doYh7Pw34.ext21; path=/; secure; HttpOnly; Max-Age=14400; Expires=Wed, 02-Sep-2026 16:57:49 GMT;HttpOnly;Secure Set-Cookie: elevisor_for_j2ee_uid=g7t8mq4p6at05; path=/; Max-Age=31536000; Expires=Thu, 02-Sep-2027 12:57:49 GMT;HttpOnly;Secure Transfer-Encoding: chunked Content-Type: text/html;charset=UTF-8 ``` > **엔지니어링 포인트** > - `Allow: GET, POST, OPTIONS` — **GET·POST 둘 다 허용**. 목록은 GET, 엑셀은 POST. > - `JSESSIONID` (Max-Age=14400 = 4시간) + `elevisor_for_j2ee_uid` (1년) 쿠키 발급. **`requests.Session()` 으로 쿠키를 물고 다녀야 엑셀 POST가 안정적이다.** 서버 앞단에 **Elevisor for J2EE**(WAS 모니터링/보안 솔루션)가 있다. > - `Cache-Control: no-cache, no-store` — 조건부 요청(ETag/If-Modified-Since) 최적화 불가. **매일 전량 재수집이 전제.** > - `Access-Control-Allow-Origin: *.mfds.go.kr` — 브라우저 fetch로는 교차출처 불가. 서버사이드(파이썬) 수집이 정답. ### 4.3 검색 form 구조 (원문 HTML 속성 그대로) ```html <!-- 상단 통합검색(무관) --> <form method="get" action="/search"> <input type="text" style="width:200px;" name="keyword" id="keywordTop" placeholder="검색어로 메뉴·정보 검색 가능" value="" alt="통합검색 검색어 입력창" title="통합검색 검색어 입력창" /> </form> <!-- ★ 실제 목록 검색 form --> <form id="searchForm" action="/pbp/CCBAC03/getList" method="get" onsubmit="return goSearch()"> <input type="hidden" name="totalPages" id="totalPages" value="984" /> <input type="hidden" name="page" id="page" value="1" /> <input type="hidden" name="limit" id="limit" value="10" /> <input type="hidden" name="sort" id="sort" value="" /> <input type="hidden" name="sortOrder" id="sortOrder" value="" /> <input type="hidden" name="searchYn" id="searchYn" value="" /> <input type="hidden" name="searchGbn" value="entpName"/> <input type="hidden" name="searchDmfPermitNo" value="dmfPermitNo"/> <input type="hidden" name="searchIngrKorName" value="ingrKorName"/> <!-- 발급일자(필수) --> <label for="date_trMinDate">발급일자</label> <input ... name="dmfAcceptDateStart" id="date_trMinDate" value="" title="발급일자 시작일 (ex:YYYYMMDD)" pattern="^(19|20)\d{2}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[0-1])" /> <input ... name="dmfAcceptDateEnd" id="date_trMaxDate" value="" title="발급일자 종료일 (ex:YYYYMMDD)" pattern="^(19|20)\d{2}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[0-1])" /> <!-- 검색구분 select (Thymeleaf 원문) --> <option th:text="|성분명|" th:value="'ingrCode'" th:selected="'ingrCode' eq ${isSelected}"></option> <option th:text="|등록번호|" th:value="'acceptNo'" th:selected="'acceptNo' eq ${isSelected}"></option> <label for="title">업소명</label> <input type="text" name="searchItem" id="title" title="업소명" /> <!-- [220418][ygha] 조회 조건(등록번호, 성분명) 추가 Start --> <label for="title">등록번호</label> <input type="text" name="searchDmfPermitNo" id="title" title="등록번호" /> <label for="title">성분명</label> <input type="text" name="searchIngrKorName" id="title" title="성분명" /> <!-- [220418][ygha] 조회 조건(등록번호, 성분명) 추가 End --> </form> ``` **검색 파라미터 확정표** | 파라미터명 | 화면 라벨 | 타입 | 필수 | 값/형식 | 비고 | |---|---|---|---|---|---| | `page` | 페이지 번호 | int | ○ | 1 ~ `totalPages` | `getList(page_no)` 가 세팅 | | `limit` | 페이지 크기 | int | ○ | **10 / 20 / 30 / 40 / 50** (UI 제공값) | `changePageSize()` 로 변경. **10000 같은 큰 값도 서버가 수용**(엑셀 POST에서 확인) | | `sort` | 정렬 컬럼 | string | ✕ | §4.5 정렬키 목록 | 빈 문자열이 기본 | | `sortOrder` | 정렬 방향 | string | ✕ | `true`(ASC) / `false`(DESC) — **JS가 boolean 문자열을 넣는다** | `sort(sort, order)` 참조 | | `searchYn` | 검색 여부 | string | ✕ | 최초 진입 시 `isFirst` 값, 이후 `''` | | | `totalPages` | 총 페이지 | int | ✕ | 서버가 다시 계산하므로 무의미 | hidden 유지용 | | `dmfAcceptDateStart` | 발급일자 시작 | date | **○(JS 검증)** | `YYYY-MM-DD` | 비면 `alert("시작 검색일(발급일자)을 입력하세요")` | | `dmfAcceptDateEnd` | 발급일자 종료 | date | **○(JS 검증)** | `YYYY-MM-DD` | 비면 `alert("종료 검색일(발급일자)을 입력하세요")`, start > end 면 거부 | | `searchItem` | 업소명 | string | ✕ | 부분일치 추정 | hidden `searchGbn=entpName` 과 짝 | | `searchDmfPermitNo` | 등록번호 | string | ✕ | 부분일치 추정 | **동명 hidden(`value="dmfPermitNo"`) 이 함께 전송되어 파라미터가 2개 중복됨** ⚠️ | | `searchIngrKorName` | 성분명 | string | ✕ | 부분일치 추정 | **동명 hidden(`value="ingrKorName"`) 중복** ⚠️ | | `searchGbn` | 검색구분(hidden) | string | ✕ | `entpName` 고정 | | > ⚠️ **중대한 함정**: `searchDmfPermitNo` / `searchIngrKorName` 은 **hidden 과 text input 이 같은 name 을 공유**한다. 브라우저 submit 시 `searchDmfPermitNo=dmfPermitNo&searchDmfPermitNo=<사용자입력>` 처럼 **값이 2개** 전송된다. 파이썬에서 재현하려면 `params=[("searchDmfPermitNo","dmfPermitNo"), ("searchDmfPermitNo", 사용자값), ...]` 형태의 **튜플 리스트**를 써야 브라우저와 동일해진다. dict 로 넘기면 값 1개만 가고 서버 동작이 달라질 수 있다. > > 우리 프로젝트는 **전량 수집**이 목표이므로 검색어 파라미터를 아예 비우고 날짜 범위만 넓게 주는 방식을 쓴다 → 이 함정을 회피한다. ### 4.4 JavaScript 함수 (원문) ```javascript function goSearch() { /*날짜 검증 1. 시작일자 유무, 2. 종료일자 유무*/ var date_trMinDate = $('#date_trMinDate').val(); var date_trMaxDate = $('#date_trMaxDate').val(); if(date_trMinDate == ""){ alert("시작 검색일(발급일자)을 입력하세요"); $('#date_trMinDate').focus(); return false; } if(date_trMaxDate == ""){ alert("종료 검색일(발급일자)을 입력하세요"); $('#date_trMaxDate').focus(); return false; } if(date_trMinDate > date_trMaxDate){ alert("시작 검색일은 종료 검색일 보다 늦을 수 없습니다"); $('#date_trMinDate').focus(); return false; } if (isFirst) { $('#searchForm #page').val(1); $('#searchForm #sort').val(''); $('#searchForm #sortOrder').val(''); } $('#searchForm #searchYn').val(isFirst ? isFirst : ''); return true; } function getList(page_no){ if (page_no) { $('#searchForm #page').val(page_no); isFirst = false; } // (이하 submit) } function changePageSize(page_size) { $('#searchForm #limit').val(page_size); $('#searchForm').submit(); } function resetForm() { $("input[type=text]").val(''); dateSetting(fixStartDay); } function sort(sort, order) { if (0 == $("#searchForm #totalPages").val()) { return false; } if (sort) { $("#searchForm #sort").val(sort); if (order === 'DESC') { $("#searchForm #sortOrder").val(false); } else { $("#searchForm #sortOrder").val(true); } } getList(1); } function getExcelFile() { $.download('/pbp/CCBAC03/getExcel', 'searchForm', 'post'); } ``` 페이지 내 정의된 전체 함수 목록(참고): `changePageSize`, `confirmDelete`, `dateSetting`, `findSessionTimer`, `fn_inoAgentYn`, `fn_message`, `fn_setAgreeYn`, `getExcelFile`, `getList`, `goSearch`, `goWindowToUrl`, `handleAnyIdAuthResponse`, `init_sessionTimer`, `lpad`, `makeInput`, `redirectToAuthPage`, `removeAccountItemById`, `requestRestLogin`, `resetForm`, `selectAnyidAccount`, `sendAnyIdLinkReq`, `sendDataToParent`, `setAnyIdNotLinked`, `showAnyIdAccountModal`, `sort`, `tempGoPCPage`, `timer_SessionTimer`. `$.download` 는 커스텀 jQuery 플러그인이며 **`/resources/js/common-pbp.js`** 에 정의되어 있다(동적 form 생성 후 POST submit 방식). ### 4.5 정렬 키 = 서버 필드명 (컬럼 ↔ 내부 필드 매핑의 결정적 증거) HTML `onclick="javascript:sort('<key>', 'DESC'); return false;"` 에서 추출한 전체 정렬 키: `noticeCode`, `dmfPermitNo`, `ingrKorName`, `entpName`, `mnfctrName`, `mnfctrPlace`, `manufCountryCodeNm`, `strDmfPermitDate`, `dmfVersion`, `yrycReportYYear`, `yrycReportNDate`, `cancelCodeNm`, `cancelDate`, `cntcJdgmnNo` HTML 주석으로 남은 **폐기된 정렬 헤더**(과거 컬럼명 증거): ```html <!--<th scope="col" th:replace="common/modules/etc :: tableSortHeader3(columnName='발급일자', columnKey='strDmfPermitDate')" />--> <!--<th scope="col" th:replace="common/modules/etc :: tableSortHeader3(columnName='업소명', columnKey='entpName')" />--> ``` → **`strDmfPermitDate` 는 원래 ‘발급일자’였고 현재 ‘최초등록일자’로 라벨만 바뀌었다.** 공공데이터포털 API 의 `DMF_PERMIT_DATE(발급일자)` 와 같은 값이다. 마찬가지로 **`entpName` 은 원래 ‘업소명’, 현재 ‘신청인’** 이다. 접근성 요약 문구(테이블 caption 대체 텍스트, 원문): > `의약품 등 심사결과정보 공개 테이블 :: 순번, 대상의약품, 등록번호, 성분명, 신청인, 제조소명, 제조소소재지, 제조국가, 최초등록일자, 최종변경일자, 최종연차보고년도, 문서번호, 취소/취하구분, 취소/취하일자` ### 4.6 컬럼 목록 (화면 15개 / 엑셀 14개) | # | 화면 헤더 | 엑셀 헤더 | 서버 필드(정렬키) | 실측 예시값 | |---|---|---|---|---| | 1 | 순번 | *(없음)* | — | `1` | | 2 | 대상의약품 | 대상의약품 | `noticeCode` ⚠️추정 | `별표1`, `신물질` | | 3 | 등록번호 | 등록번호 | `dmfPermitNo` | `20260901-209-J-2270` | | 4 | 성분명 | 성분명 | `ingrKorName` | `탐스로신염산염`, `미분화부데소니드`, `프레가발린` | | 5 | 신청인 | 신청인 | `entpName` | `주식회사지맥스파마켐`, `㈜하이플`, `엠피크코리아(주)` | | 6 | 제조소명 | 제조소명 | `mnfctrName` | `Hema Pharmaceuticals Pvt. Ltd.` | | 7 | 제조소소재지 | 제조소소재지 | `mnfctrPlace` | `Plot No. 6201/A & B, G.I.D.C., Opp. EWAC Alloys, Ciyt – Ankleshwar – 393 002, Dist.- Bharuch, Gujarat State, India` | | 8 | 제조국가 | 제조국가 | `manufCountryCodeNm` | `인도` | | 9 | 최초등록일자 | 최초등록일자 | `strDmfPermitDate` | `2026-09-01` | | 10 | 최종변경일자 | 최종변경일자 | `yrycReportNDate` ⚠️추정 | (공란) | | 11 | 최종연차보고년도 | 최종연차보고년도 | `yrycReportYYear` | (공란) | | 12 | 취소/취하구분 | 취소/취하구분 | `cancelCodeNm` | `정상` | | 13 | 취소/취하일자 | 취소/취하일자 | `cancelDate` | (공란) | | 14 | 문서번호 | 문서번호 | `dmfVersion` | `v0.0.0/2026` | | 15 | 연계심사문서번호 | 연계심사문서번호 | `cntcJdgmnNo` | (공란) | > ⚠️ `noticeCode ↔ 대상의약품`, `yrycReportNDate ↔ 최종변경일자` 는 **정렬키 개수(14) 대 컬럼 수(14, 순번 제외) 매칭으로 추론**한 것이다. 실측 검증 필요 → 부록 B. > > **`문서번호 = v0.0.0/2026`** 는 버전 문자열 형태다(`dmfVersion`). 재공고/개정 시 이 값이 올라갈 가능성이 크므로 **diff 감시 대상**으로 반드시 포함한다. ### 4.7 목록 행 HTML 원문 (파서 작성용) ```html <tr ><!--th:onclick="|javascript:location.href='@{'/pbp/CCBAC03/getItem?'+${T(gov.mfds.ndii.dsaw.cmn.PbpUtils).mapToHref(criteria)} (dmfSeq=${item.dmfSeq})}'|" class="btn_base" style="cursor:pointer;">--> <td class="pc-tr">1</td> <td><span class="s-th">대상의약품</span><span>별표1</span></td> <td class="al_c tb_horizontal"> <span class="s-th">등록번호</span> <span>20260901-209-J-2270</span> </td> <td><span class="s-th">성분명</span><span>탐스로신염산염</span></td> <td><span class="s-th">신청인</span><span>주식회사지맥스파마켐</span></td> <!--<td><span class="s-th">발급일자</span><span th:text="${item.strDmfPermitDate}"></span></td>--> <!--<td><span class="s-th">업소명</span><span th:text="${item.entpName}"></span></td>--> <td><span class="s-th">제조소명</span><span><table><tr style="border: none;"><td style="border: none;">Hema Pharmaceuticals Pvt. Ltd.</td></tr></table></span></td> <td><span class="s-th">제조소소재지</span><span><table><tr style="border: none;"><td style="border: none;">Plot No. 6201/A & B, G.I.D.C., Opp. EWAC Alloys, Ciyt – Ankleshwar – 393 002, Dist.- Bharuch, Gujarat State, India</td></tr></table></span></td> <td><span class="s-th">제조국가</span><span> <table><tr style="border: none;"><td style="border: none;">인도</td></tr></table></span></td> <td><span class="s-th">최초등록일자</span><span>2026-09-01</span></td> <td><span class="s-th">최종변경일자</span><span></span></td> <td><span class="s-th">최종연차보고년도</span><span></span></td> <td><span class="s-th">취소/취하구분</span><span>정상</span></td> <td><span class="s-th">취소/취하일자</span><span></span></td> <td><span class="s-th">문서번호</span><span>v0.0.0/2026</span></td> <td><span class="s-th">연계심사문서번호</span><span></span></td> </tr> ``` 파싱 규칙: 1. `<span class="s-th">` 는 **모바일용 라벨**이다 → 텍스트 추출 시 **반드시 제거**. 그렇지 않으면 값 앞에 "대상의약품"이 붙는다. 2. 제조소명/제조소소재지/제조국가는 **`<table><tr><td>` 중첩**이며 다중 제조소 시 `<tr>` 이 여러 개가 된다 → **리스트로 수집** 후 단순 조인이 아니라 **정규화 테이블 저장**. 3. `&`, `–`(en dash), `’`(전각 어포스트로피) 등 HTML 엔티티·유니코드 문장부호가 그대로 온다 → `html.unescape()` + NFKC 정규화 권장(단, **원문 보존 컬럼도 함께 저장**). 4. 상세 링크는 **주석 처리되어 비활성**이다: `/pbp/CCBAC03/getItem?<criteria>&dmfSeq=${item.dmfSeq}`. 즉 **현재 UI에서 상세 페이지로 갈 수 없다.** 서버 라우트 `/pbp/CCBAC03/getItem` 자체는 살아 있을 가능성이 있으나 `dmfSeq` 값이 HTML에 노출되지 않아 **직접 접근 불가** → 부록 B. (Thymeleaf 표현식에서 서버 클래스명 `gov.mfds.ndii.dsaw.cmn.PbpUtils` 가 노출된다 — 시스템 패키지 `gov.mfds.ndii.dsaw`.) 5. 성분명 셀 안에는 별도 팝업 링크가 붙는 행도 존재: `onclick="javascript:pbpPopup('/pbp/CCBBB01T/getItemDetail?itemSeq=200500904', '의약품상세정보', 1200, 0, 0, 0); return false;"` → **완제의약품 상세 팝업**. `itemSeq` 는 품목기준코드. ### 4.8 목록 GET 호출 (실측 성공) ```bash curl -sS -L --max-time 60 \ -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" \ -o p2.html -w "%{http_code} %{size_download}\n" \ "https://nedrug.mfds.go.kr/pbp/CCBAC03/getList?page=2&limit=50" # => 200 501975 (행 51,52,53... 총 50행 확인) ``` 날짜 필터 GET 도 200 OK 이며 `총 9,840건` / `총 9840건` / `name="totalPages" id="totalPages" value="197"` 을 응답에 포함(= limit 50 기준 197페이지). > **결론**: 날짜 필터를 넓게 주어도 **전체 9,840건이 그대로 잡힌다.** 즉 `dmfAcceptDateStart=2002-01-01`, `dmfAcceptDateEnd=<오늘>` 이면 전량이다. ### 4.9 엑셀 다운로드 (★ 핵심 수집 경로) **버튼 HTML** ```html <button type="button" class="btn_small btn_icon" title="엑셀다운로드" onclick="getExcelFile()"> <span class="icon_xls">엑셀다운로드</span> </button> ``` (버튼 라벨: `총 9,840건 엑셀다운로드`) **엔드포인트**: `POST https://nedrug.mfds.go.kr/pbp/CCBAC03/getExcel` (form-urlencoded, `searchForm` 전체 필드 전송) **응답 헤더 (실측)** ``` HTTP/1.1 200 OK Date: Wed, 02 Sep 2026 12:59:37 GMT Server: Apache Allow: GET, POST, OPTIONS Content-Transfer-Encoding: binary Cache-Control: no-cache, no-store, max-age=0, must-revalidate Content-Disposition: attachment; filename="의약품등심사결과공개.xlsx"; X-Content-Type-Options: nosniff Content-Length: 3575 Set-Cookie: fileDownloadToken=True;HttpOnly;Secure Content-Type: application/download; UTF-8; charset=UTF-8 ``` **두 번의 실측 결과 비교** | 시도 | 조건 | 응답 크기 | 시트 행 수 | 해석 | |---|---|---|---|---| | 1차 | 날짜 파라미터 없이 최소 필드만 POST | **3,575 bytes** | **1행(헤더만)** | **날짜 범위를 안 주면 빈 파일이 온다** | | 2차 | 날짜 범위 + `limit=10000` POST | **1,379,451 bytes** | **9,841행 (헤더 1 + 데이터 9,840)** | **전량 수신 성공** | 파일 시그니처: `504b 0304 1400 0808` = `PK..` (정상 xlsx/zip). 시트명 `Sheet0`. **엑셀 헤더 행(원문 XML)** ```xml <row r="1"> <c r="A1" t="inlineStr"><is><t>대상의약품</t></is></c> <c r="B1" t="inlineStr"><is><t>등록번호</t></is></c> <c r="C1" t="inlineStr"><is><t>성분명</t></is></c> <c r="D1" t="inlineStr"><is><t>신청인</t></is></c> <c r="E1" t="inlineStr"><is><t>제조소명</t></is></c> <c r="F1" t="inlineStr"><is><t>제조소소재지</t></is></c> <c r="G1" t="inlineStr"><is><t>제조국가</t></is></c> <c r="H1" t="inlineStr"><is><t>최초등록일자</t></is></c> <c r="I1" t="inlineStr"><is><t>최종변경일자</t></is></c> <c r="J1" t="inlineStr"><is><t>최종연차보고년도</t></is></c> <c r="K1" t="inlineStr"><is><t>취소/취하구분</t></is></c> <c r="L1" t="inlineStr"><is><t>취소/취하일자</t></is></c> <c r="M1" t="inlineStr"><is><t>문서번호</t></is></c> <c r="N1" t="inlineStr"><is><t>연계심사문서번호</t></is></c> </row> <row r="2"> <c r="A2" t="inlineStr"><is><t>별표1</t></is></c> <c r="B2" t="inlineStr"><is><t>20260901-209-J-2270</t></is></c> <c r="C2" t="inlineStr"><is><t>탐스로신염산염</t></is></c> <c r="D2" t="inlineStr"><is><t>주식회사지맥스파마켐</t></is></c> <c r="E2" t="inlineStr"><is><t>Hema Pharmaceuticals Pvt. Ltd.</t></is></c> <c r="F2" t="inlineStr"><is><t>Plot No. 6201/A & B, G.I.D.C., Opp. EWAC Alloys, Ciyt – Ankleshwar – 393 002, Dist.- Bharuch, Gujarat State, India</t></is></c> <c r="G2" t="inlineStr"><is><t>인도</t></is></c> ... </row> ``` zip 내부 구조: `['[Content_Types].xml', '_rels/.rels', 'docProps/app.xml', 'docProps/core.xml', 'xl/sharedStrings.xml', 'xl/styles.xml', 'xl/workbook.xml', 'xl/_rels/workbook.xml.rels', 'xl/worksheets/sheet1.xml']`, `sheet1.xml` 크기 **7,804,898 bytes**. 시트 XML 선두(원문): ```xml <?xml version="1.0" encoding="UTF-8"?> <worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><dimension ref="A1"/><sheetViews><sheetView workbookViewId="0" tabSelected="true"/></sheetViews><sheetFormatPr defaultRowHeight="15.0"/><sheetData> ``` > ### ⚠️⚠️ 최중요 함정: openpyxl 이 이 파일을 0행으로 읽는다 > > 실측에서 **openpyxl 로 열면 `total rows: 1`** 이 나왔고, 경고도 떴다: > ``` > C:\...\openpyxl\styles\stylesheet.py:237: UserWarning: Workbook contains no default style, apply openpyxl's default > warn("Workbook contains no default style, apply openpyxl's default") > ``` > 원인은 시트 XML 의 **`<dimension ref="A1"/>`** — 서버가 생성한 xlsx 가 시트 차원을 A1 하나로만 기록해 두었다. openpyxl 은 이 `dimension` 을 신뢰하므로 **데이터 9,840행을 통째로 놓친다.** > > **해결책 3가지 (권장 순서)** > 1. `zipfile` 로 `xl/worksheets/sheet1.xml` 을 직접 열어 `<row>` 를 정규식/`iterparse` 로 파싱 (실측으로 **9,841행** 정확히 나옴) > 2. `openpyxl.load_workbook(f, read_only=False)` 로 열고 `ws.reset_dimensions()` 후 `ws.iter_rows()` > 3. `pandas.read_excel(..., engine="calamine")` (python-calamine) — dimension 무시 > > 아래 §4.10 코드는 1번을 채택했다. ### 4.10 완결 수집 코드 (복붙 동작 수준) ```python # -*- coding: utf-8 -*- """ dmf_crawler/sources/nedrug_ccbac03.py 의약품안전나라 원료의약품등록(DMF) 공고 전량 수집기. - 1차: POST /pbp/CCBAC03/getExcel 로 xlsx 전량 다운로드 - 2차(폴백): GET /pbp/CCBAC03/getList 페이지네이션 HTML 파싱 """ from __future__ import annotations import datetime as dt import html as _html import io import re import time import zipfile from pathlib import Path from typing import Any, Dict, Iterator, List, Optional import requests from bs4 import BeautifulSoup # pip install beautifulsoup4 lxml BASE = "https://nedrug.mfds.go.kr" LIST_URL = f"{BASE}/pbp/CCBAC03" GETLIST_URL = f"{BASE}/pbp/CCBAC03/getList" GETEXCEL_URL = f"{BASE}/pbp/CCBAC03/getExcel" UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") COLUMNS = [ "대상의약품", "등록번호", "성분명", "신청인", "제조소명", "제조소소재지", "제조국가", "최초등록일자", "최종변경일자", "최종연차보고년도", "취소/취하구분", "취소/취하일자", "문서번호", "연계심사문서번호", ] FIELD_MAP = { "대상의약품": "target_drug", "등록번호": "permit_no", "성분명": "ingredient_ko", "신청인": "applicant", "제조소명": "site_name", "제조소소재지": "site_address", "제조국가": "site_country", "최초등록일자": "first_permit_date", "최종변경일자": "last_change_date", "최종연차보고년도": "last_annual_report_year", "취소/취하구분": "cancel_type", "취소/취하일자": "cancel_date", "문서번호": "doc_no", "연계심사문서번호": "linked_review_doc_no", } EARLIEST = "2002-01-01" # DMF 제도 도입(2002.7.) 이전까지 여유 있게 def new_session() -> requests.Session: s = requests.Session() s.headers.update({ "User-Agent": UA, "Accept-Language": "ko-KR,ko;q=0.9", "Referer": LIST_URL, }) # JSESSIONID / elevisor_for_j2ee_uid 쿠키 획득 (엑셀 POST 안정화에 필요) s.get(LIST_URL, timeout=60) return s def _search_form(start: str, end: str, limit: int, page: int = 1) -> List[tuple]: """브라우저가 실제로 보내는 form 필드를 그대로 재현(동명 hidden 중복 포함).""" return [ ("totalPages", "0"), ("page", str(page)), ("limit", str(limit)), ("sort", ""), ("sortOrder", ""), ("searchYn", "Y"), ("searchGbn", "entpName"), ("searchDmfPermitNo", "dmfPermitNo"), ("searchIngrKorName", "ingrKorName"), ("dmfAcceptDateStart", start), ("dmfAcceptDateEnd", end), ("searchItem", ""), ] def download_excel(session: requests.Session, start: str = EARLIEST, end: Optional[str] = None, limit: int = 100000, out_path: Optional[Path] = None) -> bytes: """전량 xlsx 를 받는다. 날짜 범위를 반드시 채워야 빈 파일이 아니다.""" end = end or dt.date.today().isoformat() resp = session.post( GETEXCEL_URL, data=_search_form(start, end, limit), headers={"Content-Type": "application/x-www-form-urlencoded"}, timeout=300, ) resp.raise_for_status() body = resp.content if not body.startswith(b"PK"): raise RuntimeError(f"xlsx 가 아님: {body[:64]!r}") if len(body) < 10_000: raise RuntimeError( f"엑셀이 비어 있음({len(body)} bytes). 날짜 파라미터를 확인하라.") if out_path: out_path.write_bytes(body) return body _ROW_RE = re.compile(rb"<row[^>]*>(.*?)</row>", re.S) _CELL_RE = re.compile(rb"<c[^>]*>(.*?)</c>", re.S) _TEXT_RE = re.compile(rb"<t[^>]*>(.*?)</t>", re.S) def parse_excel_rows(xlsx_bytes: bytes) -> Iterator[Dict[str, Any]]: """openpyxl 이 <dimension ref="A1"/> 때문에 0행으로 읽는 문제를 우회한다.""" with zipfile.ZipFile(io.BytesIO(xlsx_bytes)) as z: sheets = [n for n in z.namelist() if n.startswith("xl/worksheets/") and n.endswith(".xml")] if not sheets: raise RuntimeError("worksheet xml 없음") raw = z.read(sorted(sheets)[0]) header: Optional[List[str]] = None for m in _ROW_RE.finditer(raw): cells: List[str] = [] for c in _CELL_RE.finditer(m.group(1)): texts = _TEXT_RE.findall(c.group(1)) val = "".join(t.decode("utf-8") for t in texts) cells.append(_html.unescape(val).strip()) if header is None: header = cells continue cells += [""] * (len(header) - len(cells)) row_ko = dict(zip(header, cells)) yield {FIELD_MAP.get(k, k): v for k, v in row_ko.items()} def fetch_list_page(session: requests.Session, page: int, limit: int = 50, start: str = EARLIEST, end: Optional[str] = None) -> str: end = end or dt.date.today().isoformat() r = session.get(GETLIST_URL, params=_search_form(start, end, limit, page), timeout=60) r.raise_for_status() r.encoding = "utf-8" return r.text def parse_list_html(html_text: str) -> List[Dict[str, Any]]: """HTML 폴백 파서. <span class="s-th"> 라벨 제거 + 중첩 table 리스트화.""" soup = BeautifulSoup(html_text, "lxml") out: List[Dict[str, Any]] = [] for tr in soup.select("table tbody tr"): tds = tr.find_all("td", recursive=False) if len(tds) < 15: continue vals: List[Any] = [] for td in tds: for lab in td.select("span.s-th"): lab.decompose() inner = td.find("table") if inner: vals.append([c.get_text(strip=True) for c in inner.select("td") if c.get_text(strip=True)]) else: vals.append(td.get_text(" ", strip=True)) rec = dict(zip(["순번"] + COLUMNS, vals)) rec.pop("순번", None) out.append({FIELD_MAP.get(k, k): v for k, v in rec.items()}) return out def total_count(html_text: str) -> Optional[int]: m = re.search(r"총\s*([\d,]+)\s*건", html_text) return int(m.group(1).replace(",", "")) if m else None def crawl_all_via_html(session: requests.Session, limit: int = 50, sleep_sec: float = 1.5) -> List[Dict[str, Any]]: first = fetch_list_page(session, 1, limit) m = re.search(r'name="totalPages"\s+id="totalPages"\s+value="(\d+)"', first) total_pages = int(m.group(1)) if m else 1 rows = parse_list_html(first) for p in range(2, total_pages + 1): time.sleep(sleep_sec) # 서버 배려: 1일 1회 배치이므로 넉넉히 rows += parse_list_html(fetch_list_page(session, p, limit)) return rows if __name__ == "__main__": s = new_session() blob = download_excel(s, out_path=Path("dmf_full.xlsx")) records = list(parse_excel_rows(blob)) print(f"수집 {len(records):,}건") print(records[0]) ``` ### 4.11 관련 nedrug 라우트 (같은 시스템 내 형제 화면) | 경로 | 화면 | |---|---| | `/pbp/CCBAC03` | 원료의약품등록(DMF) 공고 ★ | | `/pbp/CCBAC03/getList` | 목록 (GET) | | `/pbp/CCBAC03/getExcel` | 엑셀 (POST) | | `/pbp/CCBAC03/getItem?...&dmfSeq=` | 상세 (주석 처리 = 비활성) | | `/pbp/CCBBB01T/getItemDetail?itemSeq=200500904` | 의약품 상세정보 팝업 | | `/pbp/CCBAE01` | 품목허가현황 | | `/pbp/CCBBA01` | 업체정보 | | `/pbp/CCBDB01` | 기능성화장품제품정보(심사) | | `/pbp/CCBAQ03/getItem?bbsYn=Y&bbscttNo=3898&orderNo=` | (구)변경지시 | | `/CCBAR01F012/getList/getInfo` | (미확인 라우트, 검색결과에만 등장) | | `/searchDrug?...` | 의약품등 정보검색 | | `/search?keyword=...` | 통합검색 | | `/cntnts/80` | 공공데이터 개요 | | `/cntnts/77` | eCTD 이용안내 | | `/cntnts/236` | 전자결제 및 수수료안내 | | `/bbs/117` | 원료의약품등록(DMF) 정보 게시판 ★ | | `/bbs/119/968/` | 보도자료 | | `/safetyuseinfo` | 안전사용정보 | | `/eng/index` | MFDS Drug Safety Korea (영문) | | `/join` | 회원가입 | | `/PPL0000` | (미확인) | | `/resources/js/common-pbp.js` | `$.download` 플러그인 정의 | 소셜 공유 버튼이 노출하는 정식 URL(정본 확인용): `https://nedrug.mfds.go.kr/pbp/CCBAC03` 푸터: `Copyright ⓒ Ministry of Food and Drug Safety. All Rights Reserved.` / 저작권정책 링크 `https://www.mfds.go.kr/wpge/m_34/de010803l001.do` --- ## 5. 의약품안전나라 `/bbs/117` — 주간 공고 게시판(레거시) ### 5.1 개요 | 항목 | 값 | |---|---| | URL | `https://nedrug.mfds.go.kr/bbs/117` | | `<title>` | `의약품안전나라 > 의약품등 정보 > 의약품 및 화장품 품목정보 > 원료의약품등록(DMF) 정보 ` | | 총 페이지 | `totalPages` = **71** (limit=10) → 약 **705건** | | 목록 컬럼 | **연번 / 제목 / 조회건수 / 등록자 / 등록일자** | | **마지막 게시글** | **"등록대상 원료의약품(DMF) 등록 공고(2021년 2월 1,2주차, 2021.2.1.-2021.02.14)"**, `bbscttNo=714`, 등록일 **2021-02-19**, 조회 **58,193**, 등록자 마스킹(`○**`) | | 상태 | **2021-02 이후 갱신 중단(사실상 아카이브)** | 최근 게시글 5건(실측): | 연번 | bbscttNo | 제목 | 등록일자 | 조회 | |---|---|---|---|---| | 1 | 714 | 등록대상 원료의약품(DMF) 등록 공고(2021년 2월 1,2주차, 2021.2.1.-2021.02.14) | 2021-02-19 | 58,193 | | 2 | 713 | 등록대상 원료의약품(DMF) 등록 공고(2021년 1월 4주차, 2021.1.25.-2021.01.31) | 2021-02-05 | 3,489 | | 3 | 712 | 등록대상 원료의약품(DMF) 등록 공고(2021년 1월 3주차, 2021.1.18.-2021.01.24) | 2021-01-28 | 1,762 | | 4 | 711 | 등록대상 원료의약품(DMF) 등록 공고(2021년 1월 2주차) | 2021-01-21 | — | | 5 | 710 | 등록대상 원료의약품(DMF) 등록 공고(2020년 12월 4주차) | 2021-01-08 | — | ### 5.2 form / JS 구조 (원문) ```html <form id="searchForm" onsubmit="return goSearch()"> <input type="hidden" name="totalPages" id="totalPages" value="71" /> <input type="hidden" name="page" id="page" value="1" /> <input type="hidden" name="searchYn" id="searchYn" value="" /> <input type="hidden" name="limit" id="limit" value="10" /> <input type="text" class="wd40" name="title" id="title" title="제목" th:value="${param.title}" /> <input type="text" class="wd80" name="title" id="title" title="제목" value="" /> </form> ``` ```javascript function list(page_no) { if(page_no){ $("#searchForm #page").val(page_no); isFirst = false; } $("#searchForm").attr("action", "/bbs/"+bbsNo).submit(); } function moveDetail(bbsNo, bbscttNo){ window.location.href = '/bbs/'+bbsNo+'/'+bbscttNo+'/'; } function resetForm() { $('input[type=text]').val(''); $('input[type=date]').val(''); $('[name=ctgryNo] option:eq(0)').prop('selected', 'selected'); } function fileAddPopup(){ window.open("/bbs/1/filePopup","","width=600, height=300, resizable=no, scrollbars=no, status=no;"); } function listBoard(){ window.location.href = referrer; } ``` **URL 패턴 확정** - 목록: `GET https://nedrug.mfds.go.kr/bbs/117?page={n}&limit={10|20|30|40|50}&title={검색어}` - **상세: `GET https://nedrug.mfds.go.kr/bbs/117/{bbscttNo}/`** — 예 `https://nedrug.mfds.go.kr/bbs/117/714/` - 검색 파라미터: `title`(제목, ⚠️ **동명 input 2개 중복**), `page`, `limit`, `searchYn`, `ctgryNo`(카테고리 select) - 페이지네이션: `onclick="javascript:list(n)"`, 처음/이전/다음/마지막 아이콘 - 목록 행 앵커 원문 예: `<a onclick="javascript:moveDetail(117,714)" href="#" title="등록대상 원료의약품(DMF) 등록 공고(2021년 2월 1,2주차, 2021.2.1.-2021.02.14) 새창 새창으로 열립니다.">` 첨부파일: 실측한 목록 HTML에는 첨부 정보가 없으며, **첨부는 상세 페이지에서 `downFile(docId)` 로 내려받는 구조**로 보인다(주석 처리된 `onKeyDown(docId)` 함수 존재). 과거 공고문 첨부는 **붙임1·붙임2** 형태(hwp/pdf 추정) → 부록 B. ### 5.3 프로젝트 결론 - **일일 크롤링 대상에서 제외한다.** 2021-02 이후 신규 글이 없다. - 단, **역사 데이터 백필 용도로 1회성 아카이브 크롤링은 가치가 있다**(약 705건 × 상세/첨부). 주차별 신규/변경/연차보고 구분이 명시된 원 공고문을 확보하면 §2.9 통계와 교차검증 가능. - 유사 아카이브: **식품의약품안전평가원 KDMF 게시판** `https://www.nifds.go.kr/brd/m_87/list.do` — "등록대상 원료의약품(DMF) 등록 공고" 게시, 실측 **총 694건 / 70페이지**, 최신 글이 **2020년 9~11월**, 주간 단위, 담당 허가총괄과. 역시 갱신 중단 상태. - 또 다른 미러: **KHIDI 제약글로벌정보센터** `https://www.khidi.or.kr/board/view?...menuId=MENU01872...` — 「등록대상 원료의약품(DMF) 등록 공고(7월 둘째주)」(2013-07-08, 공고 제2013-101호) 등 과거 공고를 재게시. --- ## 6. 공공데이터포털 OpenAPI ### 6.1 메타데이터 | 항목 | 값 | |---|---| | API 명 | **식품의약품안전처_원료의약품등록(DMF)현황** | | 데이터셋 페이지 | `https://www.data.go.kr/data/15057075/openapi.do` | | 제공기관 | **식품의약품안전처** | | 서비스 URL(엔드포인트) | **`https://apis.data.go.kr/1471000/MdcDmfInfoService01`** (HTTP 표기도 통용: `http://apis.data.go.kr/1471000/MdcDmfInfoService01/getMdcDmfList01`) | | 오퍼레이션 | **`getMdcDmfList01`** | | 전체 호출 URL | `https://apis.data.go.kr/1471000/MdcDmfInfoService01/getMdcDmfList01` | | 기관 코드 | `1471000` (식약처) | | 인증 방식 | **serviceKey (URL Encode)** — 공공데이터포털 발급 인증키 | | 응답 형식 | **XML (기본) / JSON** — `type` 파라미터 | | 트래픽 제한 | **개발계정 10,000건/일**, 운영계정은 활용사례 등록 후 증설 신청 가능 | | 비용 | **무료** | | 이용허락범위 | **"이용허락범위 제한 없음"** | | 최종 수정일 | **2025-09-19** | | 데이터 갱신주기 | 문서에 명시 없음 ⚠️ | 용도 설명(포털 원문 요지): "등록번호, 성분명, 업체명, 제조소명, 제조소 소재지, 제조국가명, 발급일자 등을 제공하여 특정 원료의약품이 어떤 성분으로 구성되고 어느 업체·제조소에서 생산되어 언제 등록되었는지 객관적으로 확인할 수 있다. 제약사·연구자는 제품 개발 시 **원료 사용 가능성 및 원료 제공처 파악**에 활용할 수 있다." ### 6.2 요청 파라미터 (포털 명세 원문 파싱) | 항목명 | 파라미터 | 크기 | 필수 | 샘플 | 설명 | |---|---|---|---|---|---| | 업체명 | `entp_name` | 200 | 옵 | 업체명 | 업체명 | | 성분명 | `ingr_kor_name` | 3000 | 옵 | 성분명 | 성분명 | | 페이지 번호 | `pageNo` | 5 | 옵 | 1 | 페이지번호 | | 한 페이지 결과 수 | `numOfRows` | 3 | 옵 | 3 | 한 페이지 결과 수 | | 인증키 | `serviceKey` | 100 | **필** | (키) | 인증키 (URL Encode) — "공공데이터포털에서 발급받은 인증키" | | 데이터포맷 | `type` | 4 | 옵 | xml | 응답데이터 형식(xml/json) default : xml | > ⚠️ `numOfRows` 크기가 **3자리**로 명시되어 있다 → **최대 999건/페이지**로 추정. 전량(9,840건) 수집 시 최소 10회 이상 페이징 필요. ### 6.3 응답 필드 (포털 명세 원문 파싱) **공통 헤더/바디** | 항목명 | 필드 | 크기 | 필수 | 샘플 | 설명 | |---|---|---|---|---|---| | 결과코드 | `resultCode` | 4 | 필 | `00` | 결과코드 | | 결과메시지 | `resultMsg` | 50 | 필 | `NORMAL SERVICE.` | 결과메시지 | | 한 페이지 결과 수 | `numOfRows` | 3 | 옵 | 3 | | | 페이지 번호 | `pageNo` | 5 | 옵 | 1 | | | 전체 결과 수 | `totalCount` | 7 | 옵 | 1 | | **데이터 항목 (7개)** | 항목명 | 필드 | 크기 | 필수 | 샘플 값 (원문 그대로) | |---|---|---|---|---| | 등록번호 | **`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. ,[미분화공정 제조소] Micro-Macinazione SA.` | | 제조소 소재지 | **`MNFCTR_PLACE`** | 2000 | 옵 | `Rho(MI) - Via Terrazzano, 77, Italy ,6995 Madonna del Piano, Switzerland` | | 제조국가명 | **`MANUF_COUNTRY_CODE_NM`** | 1000 | 옵 | `이탈리아,스위스` | | 발급일자 | **`DMF_PERMIT_DATE`** | 30 | 옵 | `2015-02-26` | > **결정적 한계**: 이 7개 필드에는 **`최종변경일자`, `최종연차보고년도`, `취소/취하구분`, `취소/취하일자`, `문서번호`, `연계심사문서번호`, `대상의약품`이 전부 없다.** 이 프로젝트의 요구사항인 **변경·취하 탐지가 API 단독으로는 불가능**하다. > > 또한 `MNFCTR_NAME` 크기가 **150** 인데 샘플 값 자체가 이미 100자에 육박한다 → 다중 제조소 건에서 **잘림(truncation) 위험**이 있다. CCBAC03 화면/엑셀은 잘리지 않는다(제조소소재지가 2000 크기인 API 대비 화면 값이 더 길다). ### 6.4 에러 코드 전표 (포털 명세 원문) | 에러명 | 코드 | 메시지 | |---|---|---| | `APPLICATION_ERROR` | 01 | GW 내부 처리 중 예기치 않은 오류가 발생했습니다. 잠시 후 다시 호출하고, 문제가 반복되면 활용지원센터로 문의해 주세요. | | `HTTP_ERROR` | 04 | 허용되지 않은 HTTP 요청이거나 기관 API 응답 처리에 실패했습니다. 요청 방식과 호출 URL을 확인해 주세요. | | `SERVICETIMEOUT_ERROR` | 05 | 기관 API 또는 GW 연계 서비스와의 연결에 실패했거나 응답 대기시간을 초과했습니다. 잠시 후 다시 호출해 주세요. | | `INVALID_REQUEST_PARAMETER_ERROR` | 10 | 요청 파라미터의 값이나 형식이 올바르지 않습니다. API 명세에서 파라미터 이름, 형식 및 허용값을 확인해 주세요. | | `NO_OPENAPI_SERVICE_ERROR` | 12 | 요청한 오픈API 서비스가 존재하지 않거나 폐기되었습니다. 호출 URL에 오타가 없는지 확인해 주세요. | | `SERVICE_KEY_IS_NULL` | 20 | 요청에 API 인증키가 포함되지 않았습니다. 공공데이터포털에서 발급받은 인증키를 요청 파라미터에 포함해 주세요. | | `PERMISSION_DENIED` | 20 | GW 접근 권한 검사에서 요청이 거부되었습니다. 해당 API의 활용신청 및 접근 권한을 확인해 주세요. | | `SERVICE_ACCESS_DENIED_ERROR` | 20 | 해당 API 서비스에 대한 이용 권한이 확인되지 않습니다. 활용신청 여부와 승인 또는 일시중지 상태를 확인해 주세요. | | `LIMITED_NUMBER_OF_SERVICE_REQUESTS_EXCEEDS_ERROR` | 22 | API 서비스의 일일 호출 허용량을 초과했습니다. 호출량이 초기화된 이후 다시 이용하거나 트래픽 증설을 신청해 주세요. | | `LIMITED_NUMBER_OF_SERVICE_REQUESTS_PER_SECOND_EXCEEDS_ERROR` | 23 | 짧은 시간에 많은 요청이 발생하여 초당 호출 허용량을 초과했습니다. 잠시 후 다시 호출해 주세요. | | `BLACKLIST_IP_ACCESS_ERROR` | 29 | 차단된 IP에서 호출한 요청입니다. 호출 서버의 IP를 확인하고, 차단 해제가 필요한 경우 활용지원센터로 문의해 주세요. | | `SERVICE_KEY_IS_NOT_REGISTERED_ERROR` | 30 | 등록되지 않은 API 인증키입니다. 인증키가 정확한지와 해당 서비스의 활용신청이 정상적으로 완료되었는지 확인해 주세요. | | `DEADLINE_HAS_EXPIRED_ERROR` | 31 | API 인증키의 사용 기한이 만료되었습니다. 공공데이터포털에서 이용 기간을 확인하거나 갱신해 주세요. | > **운영 시사점**: 코드 **22(일일 한도)**, **23(초당 한도)**, **29(IP 차단)**, **31(키 만료)** 는 반드시 별도 알림으로 처리해야 한다. 특히 **31(키 만료)** 는 조용히 실패하므로 Windows 알림 트리거 대상. ### 6.5 호출 코드 (복붙 동작 수준) ```python # -*- coding: utf-8 -*- """ dmf_crawler/sources/datago_dmf_api.py 공공데이터포털 식품의약품안전처_원료의약품등록(DMF)현황 API 클라이언트. CCBAC03 크롤링 결과의 교차검증(sanity check)용. """ from __future__ import annotations import os import time from typing import Any, Dict, Iterator, List, Optional import requests ENDPOINT = "https://apis.data.go.kr/1471000/MdcDmfInfoService01/getMdcDmfList01" # data.go.kr 은 Decoding/Encoding 두 종류의 키를 준다. # requests 가 자동 인코딩하므로 **Decoding 키**를 그대로 params 에 넣는 것이 안전하다. SERVICE_KEY = os.environ.get("DATAGO_SERVICE_KEY", "") FATAL_CODES = {"20", "29", "30", "31"} # 재시도 무의미 RETRY_CODES = {"01", "04", "05", "22", "23"} class DataGoError(RuntimeError): def __init__(self, code: str, msg: str): super().__init__(f"[{code}] {msg}") self.code, self.msg = code, msg def fetch_page(page_no: int = 1, num_of_rows: int = 100, entp_name: Optional[str] = None, ingr_kor_name: Optional[str] = None, session: Optional[requests.Session] = None, timeout: int = 30) -> Dict[str, Any]: if not SERVICE_KEY: raise RuntimeError("환경변수 DATAGO_SERVICE_KEY 가 비어 있다.") s = session or requests.Session() params = { "serviceKey": SERVICE_KEY, "pageNo": page_no, "numOfRows": num_of_rows, "type": "json", } if entp_name: params["entp_name"] = entp_name if ingr_kor_name: params["ingr_kor_name"] = ingr_kor_name r = s.get(ENDPOINT, params=params, timeout=timeout) r.raise_for_status() # 오류 시 XML 로 떨어지는 경우가 있어 방어적으로 처리 ctype = r.headers.get("Content-Type", "") if "json" not in ctype and not r.text.lstrip().startswith("{"): raise DataGoError("XML", r.text[:500]) data = r.json() body = data.get("body", data) header = data.get("header", {}) code = str(header.get("resultCode", "00")) if code not in ("00", "0"): raise DataGoError(code, str(header.get("resultMsg", ""))) return body def iter_all(num_of_rows: int = 100, sleep_sec: float = 0.4, max_retry: int = 3) -> Iterator[Dict[str, Any]]: s = requests.Session() page = 1 total: Optional[int] = None seen = 0 while True: for attempt in range(max_retry): try: body = fetch_page(page, num_of_rows, session=s) break except DataGoError as e: if e.code in FATAL_CODES or attempt == max_retry - 1: raise time.sleep(2 ** attempt) else: # pragma: no cover raise RuntimeError("unreachable") if total is None: total = int(body.get("totalCount", 0)) items: List[Dict[str, Any]] = body.get("items", []) or [] if isinstance(items, dict): # 단건일 때 dict 로 오는 케이스 items = [items] if not items: return for it in items: seen += 1 yield it if total and seen >= total: return page += 1 time.sleep(sleep_sec) if __name__ == "__main__": rows = list(iter_all(num_of_rows=100)) print(f"API 수집 {len(rows):,}건") print(rows[0]) ``` ### 6.6 프로젝트 내 위치 | 용도 | 채택 여부 | |---|---| | 주 수집원 | ✕ (필드 부족) | | **총건수 교차검증** (CCBAC03 `총 N건` vs API `totalCount`) | ○ | | **CCBAC03 장애 시 최소 폴백** (신규 등록 감지만) | ○ | | 성분명·업체명 단건 조회 API 제공 | ○ (부가 기능) | | 제조소 문자열 정본 | ✕ (150자 제한 truncation 위험) | --- ## 7. 식의약 데이터 포털 / 기타 국내 채널 | 채널 | URL | 성격 | 확인 상태 | |---|---|---|---| | 식의약 데이터 포털 | `https://data.mfds.go.kr/` | 식약처 자체 공공데이터 포털 | 미확인 | | 공공데이터 상세 | `https://data.mfds.go.kr/OPCAA01F01` | 데이터셋 상세 | **WebFetch 미수행(사용자 승인 거부)** ⚠️ | | 공공데이터 검색(의약품 분류) | `https://data.mfds.go.kr/OPCAA01F01/search?selectedTab=tab1&taskDivsCd=2&taskDivsDtlCd=3&srchSrvcKorNm=&btnSearch=` | 분류 검색 (`taskDivsCd`/`taskDivsDtlCd` 파라미터) | 미확인 | | 공공데이터 검색(다른 분류) | `https://data.mfds.go.kr/OPCAA01F01/search?selectedTab=tab1&taskDivsCd=3&taskDivsDtlCd=7&rchSrvcKorNm=&btnSearch=` | 동일 | 미확인 | | 의약품안전나라 공공데이터 개요 | `https://nedrug.mfds.go.kr/cntnts/80` | 개방 정책 | ✅ 확인 | | 식약처 공공자료 개방 | `https://www.mfds.go.kr/usr/opendata_13/list.do` | 파일데이터 목록 | 미확인 | | 공공데이터포털 검색 | `https://www.data.go.kr/tcs/dss/selectDataSetList.do?keyword="식품의약품안전처"&recmSe=N` | 식약처 데이터셋 전체 | 미확인 | | (참고) 낱알식별 API | `https://www.data.go.kr/data/15057639/openapi.do?recommendDataYn=Y` | 다른 식약처 API 예시 | — | | (참고) 건강기능식품 영양DB | `https://www.data.go.kr/data/15085712/openapi.do` | 다른 식약처 API 예시 | — | | (참고) 구 데이터셋 URL 형식 | `https://www.data.go.kr/dataset/15020627/openapi.do` | 레거시 URL 패턴 | — | **`https://nedrug.mfds.go.kr/cntnts/80` 확인 내용 (원문)** - 개방 취지: "개인ㆍ기업 및 단체 등이 식품의약품안전처의 의약품 공공데이터를 편리하고 손쉽게 활용하여 대국민서비스를 제공할 수 있도록" - 제공 형식: **파일 데이터(CSV, EXCEL) 및 OpenAPI** - 공공데이터 정의: "공공기관이 법령 등에서 정하는 목적을 위하여 생성 또는 취득하여 관리하고 있는" 정보로, 재활용 가능한 형태로 제공 - **이용 조건 (3항, 원문 그대로)** 1. **상업적, 비영리적 이용 모두 가능** 2. **출처 표시 필수** 3. **내용에 대한 임의 가공 금지** - 공공누리 유형: 페이지에 명시 없음 → 별도 확인 필요 ⚠️ - 연락처: 민원업무 **1577-1255**, 기술지원 **1544-9563** (평일 9:00~18:00) > ⚠️ **"내용에 대한 임의 가공 금지"** 조항은 이 프로젝트에 직접 영향이 있다. **원본 값을 변형해 재배포하지 말 것.** 리포트에서는 원문 컬럼을 그대로 보존하고, 파생 지표는 **별도 컬럼/시트**로 분리하며, **출처(식품의약품안전처 의약품안전나라, 조회일자)를 모든 시트에 명기**한다. 기타 국내 참조 채널: - 식약처 「원료의약품 등록 제도(DMF) 해설서 제3개정판(민원인 안내서) 개정(안) 의견조회」: `https://www.mfds.go.kr/brd/m_74/view.do?seq=43076&...` - 한국제약바이오협회(KPBMA) 해설서 제5개정판 다운로드: `https://www.kpbma.or.kr/api/info/law/policy/download/211364` (**PDF 118페이지, 약 1.5MB** — 실측 다운로드 성공, `%PDF-1.4`) - KPBMA 「원료의약품 등록에 관한 규정」 일부개정 알림: `https://www.kpbma.or.kr/info/law/statute/select/210036` - 식약처 「원료등록(DMF) 기간 대폭 단축!」 보도 첨부: `https://www.mfds.go.kr/brd/m_99/down.do?brd_id=ntc0021&seq=48245&data_tp=A&file_seq=2` - 식약처 「붙임1. 개별 항목 검증 룰」: `https://www.mfds.go.kr/brd/m_1060/down.do?brd_id=data0011&seq=14313&data_tp=A&file_seq=2` - 대한화장품협회(KCIA) 「2021년 자주하는 질문집(의약품 분야)」: `https://kcia.or.kr/inc/down.php?dir=BOARD&file_name=202111_163704850587622_2.pdf&rename=붙임_2021년+자주하는+질문집(의약품+분야).pdf` - KDI 경제정보센터: 「원료의약품 등록제도(DMF), 궁금하면 클릭하세요!」(`num=196213`, **2019.12.27.**, 식약처 융복합혁신제품지원단 허가총괄팀), 「원료의약품 등록 제도(DMF) 꼼꼼히 알기!」(`num=209012`, **2020.12.29.**, 식약처 허가총괄담당관, 1페이지 HWP), 「식약처 공문서, '의약품안전나라'로 쉽고 편하게 받으세요!」(`num=230838`) - 정부24 민원안내: `https://www.gov.kr/mw/AA020InfoCappView.do?HighCtgCD=A09006&CappBizCD=14700000458&tp_seq=` - 관세무역개발원 「원료의약품 등록 기간 '120일→20일'로 대폭 단축」: `https://www.kctdi.or.kr/kctdi/research/research11.do?mode=view&articleNo=3707&...` **2019.12.27. KDI 자료 요지(원문)** — 「원료의약품 등록에 관한 규정」 10월 개정 반영: - 변경등록 및 변경보고 기준을 명확히 함 - **외국 공정서(USP, EP) 개정에 근거한 중금속 시험항목 삭제** - **미분화 원료의약품 관리기준 신규 설정** - **화학의약품 및 한약(생약)제제 해설서 통합** --- ## 8. 해외 DMF 소스 (FDA / EDQM / PMDA) ### 8.1 요약 비교표 | 항목 | 🇺🇸 **FDA DMF** | 🇪🇺 **EDQM CEP** | 🇯🇵 **PMDA MF (原薬等登録原簿)** | 🇰🇷 **MFDS KDMF** (참고) | |---|---|---|---|---| | 공식 명칭 | Drug Master Files (DMFs) | Certificates of Suitability (CEP/COS) | 原薬等登録原簿(マスターファイル, MF) | 원료의약품 등록(DMF) | | 목록 페이지 | `https://www.fda.gov/drugs/drug-master-files-dmfs/list-drug-master-files-dmfs` | `https://extranet.edqm.eu/publications/recherches_CEP.shtml` | `https://www.pmda.go.jp/review-services/drug-reviews/master-files/0008.html` | `https://nedrug.mfds.go.kr/pbp/CCBAC03` | | 대량 다운로드 | **Excel(.xlsx) 분기별** | **`https://extranet.edqm.eu/4DLink1/4DCGI/EXPORT_WEB_CEP.txt`** (TSV) | **`https://www.pmda.go.jp/files/000240884.xlsx`** (418KB) / PDF `000240885.pdf` (1.39MB) | `POST /pbp/CCBAC03/getExcel` | | 갱신 주기 | **분기(quarterly)** | 상시(웹DB) / 다운로드 파일은 즉시 생성 | **월 2회(15일경·월말 목표)** | 상시(민원 처리 완료 시 즉시) | | 실측 접근 결과 | ❌ **404/403 — WAF 차단** (`https://www.fda.gov/apology_objects/abuse-detection-apology.html` 로 리다이렉트) | ✅ **200 OK, 1,459,534 bytes** | ✅ **200 OK, 428,927 bytes, 5,023행** | ✅ 200 OK, 9,841행 | | 크롤링 난이도 | **상** (봇 차단, 브라우저 자동화/수동 다운로드 필요) | **하** (단일 URL GET) | **하** (단일 URL GET, 단 파일명이 갱신마다 변경) | **중** (세션 쿠키 + POST + xlsx 파싱 함정) | | 로그인 | 불필요 | 불필요(공개 DB) | 불필요 | 불필요 | ### 8.2 🇺🇸 FDA Drug Master Files **DMF 유형 (Wikipedia 확인)** | 유형 | 내용 (원문) | |---|---| | **Type I** | Manufacturing Site, Facilities, Operating Procedures and Personnel | | **Type II** | Drug Substance, Drug Substance Intermediate, and Material Used in Their Preparation, or Drug Product | | **Type III** | Packaging Material | | **Type IV** | Excipient, Colorant, Flavor, Essence, or Material Used in Their Preparation | | **Type V** | FDA Accepted Reference Information | **목록 파일 메타데이터 (검색 스니펫 기준, ⚠️ 페이지 직접 확인 실패)** > "The list of DMFs, which is updated **quarterly**, contains DMFs received by **December 31, 2025**, for which acknowledgment letters were sent before **January 22, 2026**. The list is **current through DMF 043437**, with changes to the DMF activity status, DMF type, holder name and subject (title) made since the last update of **September 30, 2025**." **컬럼 구조** (John Snow Labs 재배포 데이터셋 기준, coverage **1939–2022**, **quarterly updates**): | # | 컬럼 | 설명 | |---|---|---| | 1 | `DMF_Id` | Unique identifier number for each filing | | 2 | `Is_DMF_Status_Active` | Boolean — DMF 유효/폐쇄 여부 (원 FDA 파일은 `A`/`I` 코드) | | 3 | `Type_of_DMF` | 분류(Type I~V) | | 4 | `Submission_Date` | 제출일 | | 5 | `DMF_Holder_Name` | 제출 기관/개인 | | 6 | `DMF_Subject` | DMF 대상 설명 | **접근 실패 로그 (실측)** ``` GET https://www.fda.gov/drugs/forms-submission-requirements/drug-master-files-dmfs -> 404 GET https://www.fda.gov/drugs/drug-master-files-dmfs/list-drug-master-files-dmfs -> 404 GET https://cacmap.fda.gov/drugs/gdufa-ii-drug-master-files-dmfs/list-drug-master-files-dmfs -> 403 curl -A "Mozilla/5.0 ... Chrome/120 Safari/537.36" https://www.fda.gov/... -> 404, 최종 URL https://www.fda.gov/apology_objects/abuse-detection-apology.html (본문 10 bytes) ``` > **결론**: FDA 는 **Akamai 계열 봇 탐지(abuse-detection)** 로 curl/requests 를 차단한다. 필요 시 **Playwright/Chrome 자동화 또는 수동 분기별 다운로드**로만 확보 가능. **본 프로젝트 v1 범위에서는 제외**하고, v2 확장 항목으로 둔다. 기타 FDA 관련 URL(참조): `https://www.fda.gov/media/186970/download`, `https://www.fda.gov/drugs/developmentapprovalprocess/formssubmissionrequirements/drugmasterfilesdmfs/default.htm`, 상용 재배포처 `https://www.pharmacompass.com/us-drug-master-files-dmfs` (403), `https://dmf-list.backgroundscheck.info/`, `https://fdapals.com/services/dmf-fda-guidance/`, 2006년 가이드 PDF `https://www.gmp-navigator.com/files/guidemgr/Drug_Master_Files_Current_Information_2006.pdf`, Duke 도서관 블로그 `https://mclibrary.duke.edu/about/blog/2014-12-04/fda-drug-master-files` (404). ### 8.3 🇪🇺 EDQM CEP (Certificates of Suitability) **검색 화면**: `https://extranet.edqm.eu/publications/recherches_CEP.shtml` — `<title>Certificates catalogue`, **200 OK, 27,941 bytes** **검색 폼 필드(원문 HTML)** ```html Download CEP data file ``` **전량 다운로드 (실측 성공)** ```bash curl -sS -L --max-time 120 -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" \ -D edqm_export_headers.txt -o edqm_export.txt \ -w "%{http_code} %{size_download} %{content_type}\n" \ "https://extranet.edqm.eu/4DLink1/4DCGI/EXPORT_WEB_CEP.txt" # => 200 1459534 application/download ``` 응답 헤더: `Content-Type: application/download`, `Content-Length: 1459534`. 파일은 **BOM(``) 포함 UTF-8, 탭 구분(TSV)**. **컬럼 (11개, 원문 헤더 그대로)** ``` Monograph Number Substance Type CEP Certificate (CEP) Holder Holder SPOR ORG-ID / SPOR LOC-ID Certificate (CEP) Number Issue Date CEP Status CEP Renewal due End date CEP Closure Date of last Procedure ``` **샘플 행 2건 (원문 그대로)** ``` 0 (6,7)-3-hydroxymethyl-7-(z-2-methoxyimino-2-(fur-2-yl)acetamido)ceph-3-em-4-carb, (640/2 sodium) TSE Glaxo Wellcome London GB R0-CEP 2000-275 - Rev 01 21/11/2001 Withdrawn by Holder 03/08/2006 03/08/2006 0 1,2-dihydrotriamcinolone TSE Pharmacia & Upjohn Company Kalamazoo US R0-CEP 2001-231 - Rev 00 09/04/2002 Expired 09/04/2007 15/06/2007 ``` 관측 사항: - 날짜 포맷 **`DD/MM/YYYY`** (한국 소스의 `YYYY-MM-DD` 와 다름 → 파서 분기 필요) - `Status CEP` 값: `Withdrawn by Holder`, `Expired` 등 — **취하/만료 상태가 명시된다.** 한국 `취소/취하구분` 과 대응 가능. - CEP 번호 형식: `R0-CEP 2000-275 - Rev 01` (Rev 리비전 포함) - `Type CEP`: `TSE`(TSE risk), 그 외 화학/허브 유형 존재 - 신규 DB 기능 안내(EDQM 공지, 403으로 본문 미확인): "public CEP database ... includes additional columns showing **SPOR ORG and LOC-ID** information for the holder ... as well as the **renewal date** for the CEP (where not yet renewed) and the **closure date of the last procedure**. Users can view and print a **tabulated history** of a selected CEP (revision type, outcome, closure date)." EDQM 관련 URL: `https://www.edqm.eu/en/databases` (403), `https://www.edqm.eu/en/certification`, `https://www.edqm.eu/en/what-is-the-cep-2.0`, `https://www.edqm.eu/en/certification-policy-documents-guidelines`, `https://www.edqm.eu/en/actions-on-ceps`, `https://www.edqm.eu/en/knowledge-database`, 공지 `https://www.edqm.eu/en/-/deployment-of-a-new-edqm-cep-database-feature-full-download-of-cep-data` (403), FAQ `https://faq.edqm.eu/pages/viewpage.action?pageId=1377010` (미확인), 사용자 가이드 `PA_PH_CEP (23) 56, May 2024` (Scribd 사본 `https://www.scribd.com/document/849026918/User-Guide-for-Certification-on-Line-Database-PA-PH-CEP-23-56-May-2024`), 재배포처 `https://www.pharmacompass.com/certificates-of-suitability-products-cep`. > **주의**: `www.edqm.eu` 는 **403(봇 차단)** 이지만 `extranet.edqm.eu` 는 열린다. **수집은 extranet 만 사용.** ### 8.4 🇯🇵 PMDA MF (原薬等登録原簿) **공시 페이지**: `https://www.pmda.go.jp/review-services/drug-reviews/master-files/0008.html` (「原薬等登録原簿(MF)の公示について」) - 법적 근거: **医薬品医療機器等法(薬事法) 第80条の6 第3項** - 갱신 주기: **月2回(15日ごろ及び月末を目途)**, 最新のMF番号順に掲載 - 현황 시점(실측): **As of August 15, 2026** / 시트 셀 원문 `2026年8月15日 時点` - 파일: - **Excel**: `https://www.pmda.go.jp/files/000240884.xlsx` (418KB) — 실측 **200 OK, 428,927 bytes, `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`** - **PDF**: `https://www.pmda.go.jp/files/000240885.pdf` (1.39MB) - 시트명: **`2026.8.15公示`** (갱신마다 시트명·파일명이 바뀐다 ⚠️) - 총 행 수(실측): **5,023행** - 2019년 1월 이후: 등록자 요청 시 **原薬等国内管理人(국내관리인) 정보(명칭·주소)** 선택 게재 - 2011년 3월 31일 등록 정리분(경과조치)에 대한 이력도 별도 게재 - 국내관리인 관련 3종 서식 제공: 신규 게재 의뢰서 / 변경 신고(공개판) / 변경 신고(비공개판) **컬럼 (9개, 원문 그대로)** ``` MF登録番号 | 初回登録年月日 | 最新登録年月日 | 登録区分 | 登録品目名 | 登録者氏名 | 登録者住所 | 原薬等国内管理人の名称 | 原薬等国内管理人の住所 ``` **샘플 행 4건 (원문 그대로)** ``` ['308MF10111', '2026/08/05', '2026/08/05', '医薬品等原薬', 'ラサギリンメシル酸塩「立山」', '立山化成株式会社', '富山県射水市大江1133番地', None, None] ['308MF10110', '2026/08/05', '2026/08/05', '医薬品等原薬', 'トファシチニブクエン酸塩「立山」', '立山化成株式会社', '富山県射水市大江1133番地', None, None] ['308MF10109', '2026/08/05', '2026/08/05', '医薬品等原薬', 'リオシグアト', 'Zhejiang Hengkang Pharmaceutical Co.,Ltd.', 'No.11 Chengen Road, Pubagang Town, Sanmen, Zh', None, None] ['308MF10108', '2026/08/05', '2026/08/05', '医薬品等原薬', 'リオシグアト 製造専用', '白鳥製薬株式会社', '千葉県習志野市津田沼6-11-24', None, None] ``` 관측 사항: - MF 번호 형식: **`308MF10111`** (앞 3자리 + `MF` + 5자리 일련번호) - 날짜 포맷 **`YYYY/MM/DD`** - 登録区分 값: `医薬品等原薬` - **전각 문자(Zhejiang,  )** 가 대량 사용됨 → **NFKC 정규화 필수** - `登録者住所` 가 **중간에서 잘려 있다**(`... Sanmen, Zh`) → 원본 자체가 truncated - **1행 상단에 병합 셀 형태의 시점 표기**가 있어 헤더가 2행부터 시작한다: 1행 `[None×8, '2026年8月15日 時点']`, 2행이 실제 헤더 **한글 콘솔 인코딩 함정 (실측 에러)** ``` UnicodeEncodeError: 'cp949' codec can't encode character '録' in position 5: illegal multibyte sequence ``` → Windows 기본 콘솔(cp949)이 일부 한자(`録` 등)를 출력하지 못한다. **`PYTHONIOENCODING=utf-8` 환경변수 설정 또는 `sys.stdout.reconfigure(encoding="utf-8")` 필수.** 이 프로젝트 전체(Windows 11 + 스케줄러)에 해당하는 문제이므로 부트스트랩 스크립트에 반영할 것. PMDA 관련 URL: 개요 `https://www.pmda.go.jp/review-services/drug-reviews/master-files/0007.html`, 등록정리 신고 `https://www.pmda.go.jp/review-services/drug-reviews/master-files/0003.html`, 영문 Q&A/가이드 `https://www.pmda.go.jp/files/000280630.pdf`, 최신 유의사항 `https://www.pmda.go.jp/files/000237522.pdf`, MF관리실 자료 `https://www.pmda.go.jp/files/000227202.pdf`, 일본제네릭제약협회 `https://www.jga.gr.jp/jgapedia/deals/_19341.html`, 일본제약공업협회 신뢰성보증 자료 `https://www.jpma.or.jp/information/quality/jirei/bbh7c90000000jbr-att/2023-3.pdf`. MF 제도 요지(0007.html 확인): 국내외 原薬等製造業者가 제조방법·제조관리·품질관리 정보를 등록하여 **"原薬等製造業者のノウハウが保護されます"**. **임의 등록(voluntary)**, 대상은 의약품·의료기기·재생의료등제품·체외진단용의약품의 원료. **외국 제조업자는 原薬等国内管理人 선임 및 일본어 서류 제출 필수.** ### 8.5 해외 소스 다운로드 코드 ```python # -*- coding: utf-8 -*- """dmf_crawler/sources/overseas.py — EDQM CEP / PMDA MF 다운로더""" from __future__ import annotations import csv import io import re import sys import unicodedata import zipfile from pathlib import Path from typing import Dict, Iterator, List import requests if sys.platform == "win32": # cp949 콘솔 대응 try: sys.stdout.reconfigure(encoding="utf-8") except Exception: pass UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" EDQM_EXPORT = "https://extranet.edqm.eu/4DLink1/4DCGI/EXPORT_WEB_CEP.txt" PMDA_PAGE = "https://www.pmda.go.jp/review-services/drug-reviews/master-files/0008.html" def fetch_edqm_cep(out: Path | None = None) -> List[Dict[str, str]]: r = requests.get(EDQM_EXPORT, headers={"User-Agent": UA}, timeout=180) r.raise_for_status() text = r.content.decode("utf-8-sig") # BOM 제거 if out: out.write_text(text, encoding="utf-8") return list(csv.DictReader(io.StringIO(text), delimiter="\t")) def find_pmda_xlsx_url() -> str: """공시 페이지에서 최신 xlsx 링크를 찾는다(파일명이 갱신마다 바뀌므로).""" r = requests.get(PMDA_PAGE, headers={"User-Agent": UA}, timeout=60) r.raise_for_status() m = re.search(r'href="(/files/\d+\.xlsx)"', r.text) if not m: raise RuntimeError("PMDA xlsx 링크를 찾지 못했다") return "https://www.pmda.go.jp" + m.group(1) _ROW_RE = re.compile(rb"]*>(.*?)", re.S) _CELL_RE = re.compile(rb"]*>(.*?)", re.S) _TEXT_RE = re.compile(rb"]*>(.*?)", re.S) def fetch_pmda_mf(out: Path | None = None) -> Iterator[List[str]]: url = find_pmda_xlsx_url() r = requests.get(url, headers={"User-Agent": UA}, timeout=180) r.raise_for_status() if out: out.write_bytes(r.content) with zipfile.ZipFile(io.BytesIO(r.content)) as z: # sharedStrings 를 쓰는 정상 xlsx 이므로 openpyxl 도 동작하지만, # 의존성을 줄이려 XML 을 직접 훑는다. shared: List[str] = [] if "xl/sharedStrings.xml" in z.namelist(): raw = z.read("xl/sharedStrings.xml") shared = [t.decode("utf-8") for t in _TEXT_RE.findall(raw)] sheet = sorted(n for n in z.namelist() if n.startswith("xl/worksheets/") and n.endswith(".xml"))[0] data = z.read(sheet) for m in _ROW_RE.finditer(data): cells: List[str] = [] for c in _CELL_RE.finditer(m.group(1)): chunk = c.group(1) if b't="s"' in c.group(0): idx = re.search(rb"(\d+)", chunk) cells.append(shared[int(idx.group(1))] if idx else "") else: texts = _TEXT_RE.findall(chunk) if texts: cells.append("".join(t.decode("utf-8") for t in texts)) else: v = re.search(rb"(.*?)", chunk, re.S) cells.append(v.group(1).decode("utf-8") if v else "") # 전각 -> 반각 정규화 (PMDA 데이터는 전각이 많다) yield [unicodedata.normalize("NFKC", x).strip() for x in cells] if __name__ == "__main__": ceps = fetch_edqm_cep(Path("edqm_cep.txt")) print(f"EDQM CEP {len(ceps):,}건 / 컬럼 {list(ceps[0].keys())}") rows = list(fetch_pmda_mf(Path("pmda_mf.xlsx"))) print(f"PMDA MF {len(rows):,}행 / 헤더 {rows[1]}") ``` ### 8.6 v1 범위 결정 | 소스 | v1 (매일 06:00) | v2 (확장) | |---|---|---| | MFDS CCBAC03 | ✅ 매일 | — | | data.go.kr API | ✅ 매일(교차검증만) | — | | PMDA MF | ⬜ **주 1회** (월 2회 갱신이므로) | 성분 매칭 리포트 | | EDQM CEP | ⬜ **주 1회** | CEP 보유 성분 대조 | | FDA DMF | ❌ (WAF 차단) | Playwright 분기 1회 | --- ## 9. robots.txt·저작권·이용약관·크롤링 정책 ### 9.1 robots.txt (실측 원문) ```bash curl -sS -L --max-time 30 https://nedrug.mfds.go.kr/robots.txt ``` ``` User-agent: * Disallow: / ``` **전면 차단**이다. `Allow` 도 `Sitemap` 도 없다. ### 9.2 식약처 저작권정책 (`https://www.mfds.go.kr/wpge/m_34/de010803l001.do`) 확인 내용: - **공공저작물 자유이용**: 저작권법 **제24조의2** — *"국가 또는 지방자치단체가 업무상 작성하여 공표한 저작물...은 허락 없이 이용할 수 있다"* - **예외 4가지 (자유이용 제외)** 1. 국가안전보장에 관련되는 정보 2. 개인의 사생활 또는 사업상 비밀 3. 다른 법률에 따라 보호되는 정보 4. 한국저작권위원회에 등록되어 국유/공유재산으로 관리되는 저작물 - **출처 표시 의무**: 저작권법 **제37조**에 따라 출처 명시, 원저작자명이 표시된 저작물은 그 이름도 표기 - **공공누리 미적용 콘텐츠**: 사전에 담당부서와 협의 필요 - 제3자 지식재산권 존중, 보호 저작물의 무단 상업적 이용·변경 금지 - 푸터 표기: `Copyright ⓒ Ministry of Food and Drug Safety. All Rights Reserved.` ### 9.3 의약품안전나라 공공데이터 이용 조건 (`/cntnts/80`) 1. 상업적, 비영리적 이용 모두 가능 2. **출처 표시 필수** 3. **내용에 대한 임의 가공 금지** ### 9.4 공공누리(KOGL) 유형 - 공공누리(Korea Open Government License) 자체는 **제1~4유형** 체계이지만, **본 프로젝트가 확인한 어느 페이지에도 DMF 공고 데이터에 대한 명시적 공공누리 유형 표기가 없었다.** ⚠️ 부록 B 확인 항목. - 참고: `https://en.wikipedia.org/wiki/Korea_Open_Government_License`, `https://en.wikipedia.org/wiki/Copyright_law_of_South_Korea` - 공공데이터포털 API(15057075)는 **"이용허락범위 제한 없음"** 으로 명시되어 있어 API 경로가 법적으로 가장 명확하다. ### 9.5 이용약관 관련 확인 사항 검색으로 확인된 nedrug 이용약관 취지: "제공 정보가 허위이거나 허위로 의심할 만한 상당한 이유가 있으면 서비스 이용을 제한할 수 있으며, 이용자는 서비스를 통해 게시·전송한 정보에 대해 모든 책임을 진다." **크롤링·자동수집을 직접 금지하는 조항은 확인되지 않았다.** ⚠️ 전문 미확인 → 부록 B. ### 9.6 이 프로젝트의 크롤링 정책 (확정) robots.txt 전면 차단 vs 법적 자유이용이라는 긴장 관계에서, **"법적으로는 허용되나 기술적 예의를 최대한 지킨다"** 는 원칙을 채택한다. | 정책 | 값 | 근거 | |---|---|---| | 수집 빈도 | **1일 1회, 06:00 KST** | 데이터가 민원 처리 시점에만 갱신되므로 일 1회로 충분 | | 1회 요청 수 | **엑셀 POST 1회 (+ 세션 확보 GET 1회) = 총 2회** | HTML 페이지네이션(197~984회)을 피한다 | | HTML 폴백 시 | 페이지 간 **`sleep 1.5초` 이상**, `limit=50` | 서버 부하 최소화 | | User-Agent | 실제 브라우저 UA + (선택) 연락처 주석 | 위장이 아니라 호환성 목적 | | 동시성 | **1 (직렬)** | 절대 병렬 요청 금지 | | 재시도 | 지수 백오프, 최대 3회, 총 대기 ≤ 30초 | | | 실패 시 | **API 폴백 → 그래도 실패면 Windows 알림 + 전날 스냅샷 유지** | | | 데이터 재배포 | **원문 그대로 보존 + 출처·조회일자 명기**, 임의 가공 결과는 별도 시트 | `/cntnts/80` 3항 | | 출처 표기 문구 | `출처: 식품의약품안전처 의약품안전나라(nedrug.mfds.go.kr) 원료의약품등록(DMF) 공고, 조회일 YYYY-MM-DD` | 저작권법 제37조 | | 개인정보 | 신청인·제조소는 **법인 정보**이나, 게시판 등록자명(`○**`)처럼 마스킹된 값은 **수집·저장 금지** | | | 로그 보존 | 요청 URL·응답코드·바이트수·소요시간만. 응답 본문 원본은 **일 1개 스냅샷만 보관, 90일 롤링** | | > **경고**: robots.txt 를 무시하는 결정이므로, **대량·고빈도 요청은 절대 금지**다. 위 정책(일 2회 요청)을 벗어나는 순간 IP 차단 및 법적 리스크가 현실화된다. 개발 중 반복 테스트는 **로컬에 저장한 스냅샷 파일**로 수행할 것. --- ## 10. 제약사 RA 실무자의 모니터링 관점 ### 10.1 왜 RA가 DMF 공고를 매일 보는가 공공데이터포털의 활용 목적 서술(원문): *"제약사 및 연구자 등은 DMF 현황 파악으로 제품 개발 시 **원료 사용 가능성 및 원료 제공처 파악** 등에 활용할 수 있다."* RA(Regulatory Affairs) 실무 관점(검색 확인 원문): *"제품에 대한 허가정보 변경은 RA부서에서 빈번히 일어나는 업무 중 하나이며, **등록된 제조원이 매각되어 새로운 제조원으로 변경해야 하는 상황**이 발생할 수 있다."* RA 업무에는 **인허가 관련 법령 및 규정 모니터링**이 포함된다. 채용 공고 예시(직무 실체 확인): `https://www.bzpp.co.kr/biz/businessDetailView/BR250423A00015` — 「[중견사]원료 의약품 RA (허가신청 및 등록)/차장-부장급」. 실무 교육: `https://comento.kr/edu/learn/연구개발/인허가개발-G676` 「제약 RA 현직자 실무 체험: 신약 허가 준비, 의약품 품목 갱신 등」. > ⚠️ 검색 결과에서 **"경쟁사 모니터링"에 대한 구체적 실무 문헌은 확인되지 않았다.** 아래 4축 중 ③은 데이터 구조상 자명한 활용이지만, 업계 관행 근거는 **미검증**이다. ### 10.2 모니터링 4축 → 리포트 시트 매핑 | 축 | 감시 대상 필드 | RA가 알고 싶은 것 | 이벤트 정의 | 대응 리포트 시트 | |---|---|---|---|---| | **① 성분(API)** | `성분명`, 등록번호의 `ingr_no`+`group` | "내가 담당하는 성분에 새 등록이 떴나?" / "이 성분 소싱 대안이 늘었나?" | 관심 성분 워치리스트에 신규 행 발생 | `내_관심성분` | | **② 제조원(Site)** | `제조소명`, `제조소소재지`, `제조국가` | "우리 제조원이 다른 회사 DMF에도 올라갔나?" / "제조원이 매각·이전됐나?" | 동일 `permit_no` 의 site_* 값 변경, 또는 특정 제조소명이 새 등록에 등장 | `제조원_변동`, `국가별_동향` | | **③ 경쟁사(신청인)** | `신청인` | "경쟁사가 어떤 원료를 확보하고 있나?" | 워치리스트 신청인의 신규/변경 발생 | `경쟁사_동향` | | **④ 등록 상태 변화** | `최종변경일자`, `최종연차보고년도`, `취소/취하구분`, `취소/취하일자`, `문서번호` | "내가 쓰는 원료가 취하됐나?"(**공급중단 리스크**) / "변경등록이 걸렸나?" | 상태 필드 값 변경 | `변경`, `취하_취소`(★최우선 알림) | ### 10.3 이벤트 타입 확정 (diff 엔진 사양) 전일 스냅샷 `S(t-1)` 과 금일 스냅샷 `S(t)` 를 `permit_no`(정규화된 `base_key` 아님, **원문 등록번호**)를 키로 비교한다. | 이벤트 | 판정 조건 | 심각도 | 알림 | |---|---|---|---| | `NEW` | `permit_no ∈ S(t) \ S(t-1)` | info | 관심 성분/경쟁사 매칭 시 | | `WITHDRAWN` | `취소/취하구분` 이 `정상` → 다른 값으로 변경, 또는 `취소/취하일자` 신규 부여 | **critical** | 항상 | | `CHANGED_REG` | `최종변경일자` 값 변경 | warn | 항상 | | `ANNUAL_REPORT` | `최종연차보고년도` 값 변경 | info | 집계만 | | `SITE_CHANGED` | `제조소명`/`제조소소재지`/`제조국가` 값 변경 | **critical** | 항상 (규칙 제17조제1항제1호 = 중요 변경) | | `APPLICANT_CHANGED` | `신청인` 값 변경 | warn | 양도양수 가능성 (처리기한 25일 조항 참조) | | `DOC_VERSION_CHANGED` | `문서번호`(`dmfVersion`) 값 변경 | info | 재공고 추적 | | `LINKED_REVIEW_SET` | `연계심사문서번호` 신규 부여 | info | 원료-완제 연계심사 진행 신호 | | `DISAPPEARED` | `permit_no ∈ S(t-1) \ S(t)` | **critical** | 데이터 삭제 or 크롤링 실패 → **먼저 총건수 검증** | > **필수 안전장치**: `DISAPPEARED` 가 다수 발생하면 대개 **크롤링 실패(빈 엑셀 3,575 bytes)** 다. `총 N건` 값이 전일 대비 **±5% 이상 급감**하면 diff 를 중단하고 전일 스냅샷을 유지한 채 Windows 알림을 띄운다. ### 10.4 파생 분석 지표 (리포트 부가 시트) | 지표 | 정의 | 활용 | |---|---|---| | 성분별 등록 제조원 수 | `성분명` 별 distinct(`제조소명`) | 소싱 대안 폭 = 공급 안정성 | | 성분별 신청인 수 | `성분명` 별 distinct(`신청인`) | 시장 경쟁 강도 | | 국가별 점유 | `제조국가` 집계 (다중 국가는 분해 후 집계) | **인도·중국 의존도** — 지정학 리스크 | | 신청인 Top-N | `신청인` 별 등록 건수 | 원료 상사·수입사 랭킹 | | 제조소 Top-N | `제조소명` 별 등록 건수 | 글로벌 API 제조사 랭킹 | | 월별 신규 추이 | `최초등록일자` 월별 count | §2.9 통계와 연속성 유지 | | 알파벳군(A~K) 구성 | 등록번호 `group` 집계 | 제도 확대 코호트 분석 | | 취하율 | `취소/취하구분 != 정상` 비율 | 제도 건전성 | | 허여서 파생 비율 | `is_grant_derived == True` 비율 | 자료공유 관행 규모 | | 심사 소요일 | `최초등록일자 − accept_date(등록번호 접두)` | 처리기한(20/90일) 준수 실태 | ### 10.5 RA 워치리스트 설정 파일 (권장 스키마) ```yaml # config/watchlist.yaml watchlist: ingredients: # ① 성분 축 - 탐스로신염산염 - 프레가발린 - 부데소니드 # '미분화부데소니드' 도 부분일치로 잡히게 할 것 ingredient_match: contains # contains | exact | regex sites: # ② 제조원 축 - Hema Pharmaceuticals - Avik Pharmaceutical site_match: contains applicants: # ③ 경쟁사 축 - 주식회사지맥스파마켐 - 엠피크코리아 applicant_match: contains countries: # 국가 관심 축 - 인도 - 중국 alerts: always_critical: # ④ 상태 변화는 워치리스트와 무관하게 항상 알림 - WITHDRAWN - SITE_CHANGED - DISAPPEARED daily_digest: - NEW - CHANGED_REG - ANNUAL_REPORT - APPLICANT_CHANGED - DOC_VERSION_CHANGED - LINKED_REVIEW_SET ``` ### 10.6 도메인 특이 규칙 (오탐 방지) 1. **‘미분화’ 접두어**: `미분화부데소니드` 처럼 미분화 원료는 **별도 등록**이므로 `부데소니드` 와 다른 행이다. 성분 매칭은 **부분일치 + 미분화 플래그 별도 표시**. 2. **염류·수화물**: `탐스로신염산염`, `포르모테롤푸마르산염수화물` 처럼 염/수화물이 성분명에 붙는다. 규정 제2조 2호가 "별표1의 원료의약품과 그 **염류 및 수화물**"이므로 **모두 등록 대상**. 성분 정규화 시 염 접미어(염산염/푸마르산염/수화물/메실산염 등) 사전을 별도 관리. 3. **다중 제조소**: 콤마 구분 다중 값이며 `[미분화공정 제조소]` 같은 **역할 표기 접두어**가 붙는다. 단순 split(",") 하면 주소 안의 콤마(`Rho(MI) - Via Terrazzano, 77, Italy`)와 충돌 → **`" ,"`(공백+콤마) 또는 화면 HTML 의 중첩 `` 기준 분해**가 안전. 4. **회사명 표기 흔들림**: `㈜하이플` vs `(주)하이플`, `주식회사지맥스파마켐` vs `(주)지맥스파마켐`. NFKC 정규화 + `주식회사|㈜|(주)|㈔` 접두/접미 제거 후 **정규화 키** 생성 필수. 5. **연차보고 1월 스파이크**: 규칙 제17조제2항이 **매년 1월 31일까지** 보고이므로 1~2월 `ANNUAL_REPORT` 폭증은 정상. 6. **재공고(재등록)**: §2.9 통계에서 괄호로 표기된 "재공고" 건이 전체의 47%(3,279/6,965)에 달한다. **같은 성분·제조소가 반복 등장하는 것은 이상이 아니다.** 7. **공고 유보**: 규정 제5조제2항에 따라 수급 불균형 시 **공고가 유보**될 수 있다 → 특정 성분이 공고에 안 뜬다고 미등록으로 단정 금지. --- ## 11. 언론·유사 서비스가 DMF를 다루는 방식 | 매체/서비스 | URL | 다루는 방식 | |---|---|---| | **데일리팜** | `https://www.dailypharm.com/Users/News/NewsView.html?ID=324241` / 모바일 `http://m.dailypharm.com/newsView.html?ID=295979` | **제도 변화에 따른 시간대별 추이 분석**. 「규제완화 여파...원료약 등록 1년새 237→653건」(2025-06-26)에서 연도별 변동 패턴(2021년 정점 → 감소 → 2025년 반등)을 추적. 「필수약·공급중단 보고대상 의약품 'DMF 등록' 유예」(2023-01-12)처럼 **규제 완화·산업 지원 정책 프레임**으로 보도 | | **히트뉴스** | `https://www.hitnews.co.kr/news/articleView.html?idxno=54607` (2024-05-09) / `?idxno=16548` (2020-04-26) | **정책 변화 해설**. GMP 증명서 대체·처리기한 단축 등 제도 개편을 업계 영향 관점에서 심층 보도. 2020년 기사는 의약품안전나라 개편(‘e약은要’, 메일링 구독, 공급중단 예측 AI, **전자허가증 전환 — 완제 2020년, 원료 2021년, 나머지 2022년**)을 다룸 | | **약업신문 / 약사공론** | `https://www.kpanews.co.kr/article/show.asp?idx=211278&category=C` | 반기·연간 등록 건수 집계 기사 (2022년 상반기 653건 = 전년 동기 256건 대비 2.8배 등) | | **KHIDI 제약글로벌정보센터** | `https://www.khidi.or.kr/board/view?...menuId=MENU01872...` | **법령 및 고시 > 의약품 인허가정보** 게시판에 공고 원문·지침 재게시. 「등록대상 원료의약품(DMF) 등록 공고(7월 둘째주)」(`no1=912&linkId=26604564`), 「[지침]원료의약품 등록(DMF) 처리 절차」(2017-03-30, 식품의약품안전평가원 바이오생약심사부, `linkId=26605812`), 「원료의약품 등록에 관한 규정」(2021-02-24, 제2021-8호, `no1=3182&linkId=48852840`) | | **PharmaCompass** | `https://www.pharmacompass.com/us-drug-master-files-dmfs`, `https://www.pharmacompass.com/certificates-of-suitability-products-cep` | FDA DMF / EDQM CEP 를 **상용 재가공 DB**로 제공 (403으로 상세 미확인) | | **John Snow Labs** | `https://www.johnsnowlabs.com/marketplace/fda-drug-master-files-directory/` | FDA DMF 목록을 **정제 데이터셋 상품**으로 판매 (컬럼 6개, 분기 갱신, 1939–2022) | | **dmf-list.backgroundscheck.info** | `https://dmf-list.backgroundscheck.info/` | FDA 분기 스프레드시트 비공식 미러 | **시사점**: 국내에는 **"매일 DMF 신규/변경/취하를 자동 탐지해 알려주는 서비스가 없다."** 언론은 반기/연간 집계 기사만 내고, 정부는 상시 테이블만 던져둔다. **이 프로젝트의 존재 이유가 바로 그 공백이다.** --- ## 12. 수집 대상 필드 확정안 > ⚠️ **전면 재작성됨 (2026-09-02)**. 이 절은 원래 CCBAC03 화면/엑셀 14개 컬럼을 기준으로 작성되어 있었으나, `design/00b-baseline-data-analysis.md` 의 실측(기존 `DMF_현황.xlsx` 9,084건 전수 분석)에 따라 **공식 Open API 7필드 + 파생 필드 9개** 기준으로 다시 썼다. 이 프로젝트는 화면을 수집하지 않는다(§4 참조). 원본 14컬럼 버전은 git 이력에 보존된다. ### 12.1 확정 필드표 (원본 7 + 파생 9 = 16) | # | 필드명(영문 snake_case) | 한글명 | 출처 | 타입 | 널 허용 | 정규화 규칙 | 예시 | |---|---|---|---|---|---|---|---| | 1 | `dmf_permit_no` | 등록번호 | API (`DMF_PERMIT_NO`) | string | 아니오 (**PK**) | 원문 그대로 저장. 자연 키, 9,084건 중복 0건 실측(00b §5.1). §3 파서로 파생 필드를 뽑되 **파싱 실패해도 이 필드는 그대로 유지**(00b D7) | `20260901-209-J-2270` | | 2 | `ingr_kor_name` | 성분명 | API (`INGR_KOR_NAME`) | string | 아니오 | 원문 보존. 그룹핑용 정규화는 파생 필드 #15 에서 별도 수행(21그룹 표기 흔들림 실측, 00b §8.2) | `탐스로신염산염` | | 3 | `entp_name` | 업체명(신청인) | API (`ENTP_NAME`) | string | 아니오 | 원문 보존. 정규화는 파생 필드 #16 에서 별도 수행(2그룹 표기 흔들림 실측, 00b §8.3) | `(주)삼오제약` | | 4 | `mnfctr_name` | 제조소명 | API (`MNFCTR_NAME`) | string | 아니오 | 원문 그대로 보존. **콤마로 단순 분할 금지**(제조소명 자체에 `Co., Ltd.` 콤마 포함) — 다중 제조소 분해는 신뢰할 수 없어 하지 않는다. 연속 공백·마침표 중복 등 원본 오류 42건 실측(00b §8.4)이지만 원문은 고치지 않고 그대로 저장 | `BDR LIFESCIENCES PVT. LTD..` | | 5 | `mnfctr_place` | 제조소소재지 | API (`MNFCTR_PLACE`) | string | 아니오 | 원문 보존. 표시 폭만 제한(목록 시트는 앞 60자, 상세는 전체) — 최대 473자 실측(00b §8.6, D10) | 최대 473자 문자열 | | 6 | `manuf_country_code_nm` | 제조국가명 | API (`MANUF_COUNTRY_CODE_NM`) | string | **예** (5건 결측 실측, 00b §7.1) | 원문 그대로 보존. 정규화된 리스트는 파생 필드 #14 에서 별도 생성 — 같은 국가 반복 표기 496건 실측(00b §8.1) | `중국,중국` | | 7 | `dmf_permit_date` | 발급일자 | API (`DMF_PERMIT_DATE`) | date (`YYYY-MM-DD`) | 아니오 | 파싱 실패 0건, 전량 일관 포맷 확인(00b §8.5). **고정된 최초 등록일이 아니라 변경마다 갱신되는 최종 갱신일**로 동작(00b §6) — 변경 탐지의 1차 신호 | `2026-08-18` | | 8 | `first_collected_date` | 최초수집일 | 파생(시스템) | date | 아니오 | 이 시스템이 해당 등록번호를 **처음 관측한 날**. 기존 프로토타입 xlsx 의 동일 컬럼 설계를 계승(00b §3, D9) | `2026-09-02` | | 9 | `last_change_detected_date` | 최종변경감지일 | 파생 | date | 예 (변경 이력 없으면 null) | `dmf_permit_date` 이동 또는 성분명·업체명·제조소명·소재지·국가·발급일자 등 6개 필드 diff 발생 시 그 날짜로 갱신(00b §6.3, D3) | `2026-09-02` | | 10 | `status` | 상태 | 파생 | enum(`신규`/`정상`/`변경`/`취하`) | 아니오 | `신규`=오늘 API 응답에 처음 출현, `변경`=`dmf_permit_date` 이동 또는 필드 diff 발생, `취하`=어제 있던 등록번호가 오늘 API 응답에서 소멸, `정상`=그 외(00b §4, §6.3, D2·D3) | `변경` | | 11 | `change_sequence` | 변경차수 | 파생 | int | 아니오(기본 0) | `status` 가 `변경`으로 판정될 때마다 +1 되는 누적 카운터 | `3` | | 12 | `initial_registration_date` | 최초등록일 | 파생(`dmf_permit_no` 파싱) | date | 예 (파싱 실패 시 null) | 표준 포맷 등록번호 앞 8자리(`YYYYMMDD`)를 §3 파서로 파싱. `dmf_permit_date` 와 다를 수 있다 — 44.5% 불일치 실측, 전부 괄호 붙은 갱신 건(00b §6.1) | `2005-08-31` (등록번호 `20050831-33-A-81-08(18)`에서) | | 13 | `target_drug_category` | 대상의약품구분 | 파생(`dmf_permit_no` 포맷) | enum(`별표1`/`신물질`/`기타`) | 예 | `수` 접두어 포맷 = 신물질(1,882건 실측, 00b §5.2), 표준 `YYYYMMDD-n-A-n-n` 포맷 = 별표1, 그 외 포맷(455건, "기타")은 판별 불가로 null 또는 `기타` | `신물질` | | 14 | `country_list_normalized` | 정규화된 국가 리스트 | 파생(`manuf_country_code_nm`) | list\ | 예 (원본 결측 5건) | 콤마로 분해 → 공백 제거 → 중복 제거 → 정렬 → 재결합. 496건 중복 표기(`중국,중국` 등) 해소, 정규화 후 고유 국가 49개(00b §8.1, D5) | `["중국"]` (원문 `중국,중국`에서) | | 15 | `ingredient_key_normalized` | 정규화된 성분명 키 | 파생(`ingr_kor_name`) | string | 아니오 | 공백·중점(`·`)·하이픈·괄호 제거 후 소문자화하여 그룹 키 생성. 21그룹 표기 흔들림 해소(00b §8.2, D5). 원문(#2)은 그대로 보존(D6) | `다비가트란에텍실레이트메실산염` | | 16 | `entity_key_normalized` | 정규화된 업체명 키 | 파생(`entp_name`) | string | 아니오 | `㈜`→`(주)`, `주식회사`→`(주)` 치환 후 공백·마침표 제거 및 소문자화. 2그룹 표기 흔들림 해소(00b §8.3, D5). 원문(#3)은 그대로 보존(D6) | `(주)삼진제약` | > **원문 보존 원칙(D6)**: 정규화 값(#14~#16)은 항상 별도 컬럼이며, 원본 값(#2, #3, #6)을 덮어쓰지 않는다. `/cntnts/80`(§7·§9.3)의 "내용에 대한 임의 가공 금지" 조항과도 일치한다. > > **등록번호 파싱은 §3 을 그대로 쓴다.** `dmf_permit_no` 는 API 응답이지만 값 자체는 화면 컬럼 `등록번호`와 동일하므로, §3 에서 확정한 정규식·`GROUP_TABLE`·`parse_permit_no()` 구현이 그대로 적용된다(포맷 4종: 표준 41.5%, 표준+괄호 32.7%, 신물질 20.7%, 기타 5.0% — 00b §5.2). ### 12.2 §6 응답 필드와의 매핑 확인 API 원본 필드명(대문자 스네이크, §6.3)과 이 절의 `snake_case` 필드명은 단순 소문자 변환 관계다. | API 필드 | 이 절 필드명 | |---|---| | `DMF_PERMIT_NO` | `dmf_permit_no` | | `INGR_KOR_NAME` | `ingr_kor_name` | | `ENTP_NAME` | `entp_name` | | `MNFCTR_NAME` | `mnfctr_name` | | `MNFCTR_PLACE` | `mnfctr_place` | | `MANUF_COUNTRY_CODE_NM` | `manuf_country_code_nm` | | `DMF_PERMIT_DATE` | `dmf_permit_date` | ### 12.3 저장 스키마 (SQLite/DuckDB 권장) ```sql -- 1) 일일 스냅샷(API 원본 7필드, 임의 가공 금지 원칙 준수) CREATE TABLE IF NOT EXISTS dmf_snapshot ( snapshot_date TEXT NOT NULL, -- YYYY-MM-DD dmf_permit_no TEXT NOT NULL, -- PK 구성요소, 9,084건 중복 0 실측 ingr_kor_name TEXT NOT NULL, entp_name TEXT NOT NULL, mnfctr_name TEXT NOT NULL, mnfctr_place TEXT NOT NULL, manuf_country_code_nm TEXT, -- 5건 결측 실측 dmf_permit_date TEXT NOT NULL, row_hash TEXT NOT NULL, source TEXT NOT NULL DEFAULT 'datago_api', PRIMARY KEY (snapshot_date, dmf_permit_no) ); CREATE INDEX IF NOT EXISTS ix_snap_permit ON dmf_snapshot(dmf_permit_no); CREATE INDEX IF NOT EXISTS ix_snap_ingr ON dmf_snapshot(ingr_kor_name); -- 2) 파생/이력 필드(등록번호별 최신 상태) CREATE TABLE IF NOT EXISTS dmf_derived ( dmf_permit_no TEXT PRIMARY KEY, first_collected_date TEXT NOT NULL, last_change_detected_date TEXT, status TEXT NOT NULL, -- 신규/정상/변경/취하 change_sequence INTEGER NOT NULL DEFAULT 0, initial_registration_date TEXT, -- 파싱 실패 시 NULL target_drug_category TEXT, -- 별표1/신물질/기타/NULL ingredient_key_normalized TEXT NOT NULL, entity_key_normalized TEXT NOT NULL ); -- 3) 정규화된 국가 리스트(1:N) CREATE TABLE IF NOT EXISTS dmf_country ( dmf_permit_no TEXT NOT NULL, country TEXT NOT NULL, PRIMARY KEY (dmf_permit_no, country) ); -- 4) 등록번호 파싱 결과(§3 파서 그대로 적용) CREATE TABLE IF NOT EXISTS dmf_permit_parsed ( dmf_permit_no TEXT PRIMARY KEY, permit_fmt TEXT NOT NULL, -- standard/new_substance/unknown accept_date TEXT, ingr_no INTEGER, permit_group TEXT, permit_serial INTEGER, permit_sub INTEGER, grant_seq INTEGER, base_key TEXT ); -- 5) 변경 이벤트 CREATE TABLE IF NOT EXISTS dmf_event ( event_date TEXT NOT NULL, dmf_permit_no TEXT NOT NULL, event_type TEXT NOT NULL, -- NEW/CHANGED/WITHDRAWN field TEXT, old_value TEXT, new_value TEXT, PRIMARY KEY (event_date, dmf_permit_no, event_type, field) ); -- 6) 수집 실행 로그(운영 모니터링/알림용, design/00-DATA-SOURCE-DECISION.md §7.2 안전장치 연동) CREATE TABLE IF NOT EXISTS crawl_run ( run_id TEXT PRIMARY KEY, started_at TEXT NOT NULL, finished_at TEXT, http_status INTEGER, row_count INTEGER, total_count INTEGER, -- API 의 totalCount ok INTEGER NOT NULL, -- 0/1 error TEXT ); ``` ### 12.4 xlsx 리포트 시트 구성 00b 문서가 확인한 **기존 프로토타입의 3시트 구조(`전체`/`신규`/`갱신이력`)를 계승·확장**한다(00b §2, §10 D8). | 시트 | 내용 | 원천 | 비고 | |---|---|---|---| | `전체` | 당일 API 전량 스냅샷(원본 7필드 + 파생 9필드) | `dmf_snapshot` + `dmf_derived` | 기존 프로토타입 시트 계승. 소재지는 표시 폭 제한(D10) | | `신규` | 당일 `status = 신규` 행 | `dmf_derived` | 기존 프로토타입 시트 계승 | | `갱신이력` | 실행일·누적건수·신규건수 로그 | `crawl_run` | 기존 프로토타입 시트 계승. 변경건수·취하건수·오류 로그 컬럼 추가 검토(00b 부록 B) | | `변경` | 당일 `status = 변경` 행 (old→new 나란히) | `dmf_event` | 신설 | | `취하` | 당일 `status = 취하` 행 (★ 조건부 서식 빨강) | `dmf_derived` | 신설. §10.3 의 `WITHDRAWN` 이벤트에 대응 | | `대시보드` | 국가 분포(인도·중국 61.5% 집중, 00b §7.3), 상위 성분·업체, 연도별 추이(발급일자=갱신연도 기준임을 명시, 00b §7.2, D11) | 집계 | 신설. 00b §9 "디자인 예쁘게" 요구 대응 | | `국가별_집계` | `country_list_normalized` 기준 피벗 | `dmf_country` | 신설. 국가 코드 매핑 테이블 별도 관리(00b D12) | | `성분별_집계` / `업체별_집계` | 정규화 키(#15/#16) 기준 랭킹 | `dmf_derived` | 신설 | | `출처_고지` | 출처·조회일자·이용조건(출처표시 필수, 임의가공 금지) 문구 | 고정 텍스트 | 계승 | > 시트별 상세 서식(틀 고정·자동 필터·조건부 서식·열 너비 상한)은 00b §9 의 실측 결함(freeze_panes 없음, 자동필터 없음, D열 너비 255.6) 을 근거로 `design/03-xlsx-report-spec.md` 에서 확정한다. --- ## 부록 A. 출처 목록 **확인여부 범례**: ✅ = WebFetch/curl 로 실제 내용 확인 / ⚠️ = 접근했으나 본문 미노출(첨부·JS·바이너리) / ❌ = 404/403/차단 / 🔍 = 검색 결과에만 등장, 미열람 / 🚫 = 도구 사용 거부로 미수행 ### A.1 한국 — 의약품안전나라 (nedrug.mfds.go.kr) | 제목 | URL | 확인 | |---|---|---| | 원료의약품등록(DMF) 공고 (★주 수집원) | `https://nedrug.mfds.go.kr/pbp/CCBAC03` | ✅ | | 동 목록 GET | `https://nedrug.mfds.go.kr/pbp/CCBAC03/getList` | ✅ | | 동 엑셀 POST | `https://nedrug.mfds.go.kr/pbp/CCBAC03/getExcel` | ✅ | | 동 상세(비활성) | `https://nedrug.mfds.go.kr/pbp/CCBAC03/getItem?` | ⚠️ | | 원료의약품등록(DMF) 정보 게시판 | `https://nedrug.mfds.go.kr/bbs/117` | ✅ | | robots.txt | `https://nedrug.mfds.go.kr/robots.txt` | ✅ | | 공공데이터 개요 | `https://nedrug.mfds.go.kr/cntnts/80` | ✅ | | 의약품 상세정보 팝업 | `https://nedrug.mfds.go.kr/pbp/CCBBB01T/getItemDetail?itemSeq=200500904` | 🔍 | | 품목허가현황 | `https://nedrug.mfds.go.kr/pbp/CCBAE01` | 🔍 | | 업체정보 | `https://nedrug.mfds.go.kr/pbp/CCBBA01` | 🔍 | | 기능성화장품제품정보(심사) | `https://nedrug.mfds.go.kr/pbp/CCBDB01` | 🔍 | | (구)변경지시 | `https://nedrug.mfds.go.kr/pbp/CCBAQ03/getItem?bbsYn=Y&bbscttNo=3898&orderNo=` | 🔍 | | eCTD 이용안내 | `https://nedrug.mfds.go.kr/cntnts/77` | 🔍 | | 전자결제 및 수수료안내 | `https://nedrug.mfds.go.kr/cntnts/236` | 🔍 | | 보도자료 | `https://nedrug.mfds.go.kr/bbs/119/968/` | 🔍 | | 의약품등 정보검색 | `https://nedrug.mfds.go.kr/searchDrug?amp%3BsortOrder=false&%3BsearchYn=true&%3Bpage=1&%3BsearchDivision=detail&%3BitemName=%ED%94%84%EB%9E%9C%EB%93%9C%EB%A6%AC&%3BindutyClassCode=B0&%3BsearchConEe=AND&%3BsearchConUd=AND&%3BsearchConNb=AND` | 🔍 | | 통합검색 | `https://nedrug.mfds.go.kr/search?keyword=...` | 🔍 | | 홈 | `https://nedrug.mfds.go.kr/` | 🔍 | | index | `https://nedrug.mfds.go.kr/index` | 🔍 | | 회원가입 | `https://nedrug.mfds.go.kr/join` | 🔍 | | 안전사용정보 | `https://nedrug.mfds.go.kr/safetyuseinfo` | 🔍 | | 영문 사이트 | `https://nedrug.mfds.go.kr/eng/index` | 🔍 | | (미확인 라우트) | `https://nedrug.mfds.go.kr/CCBAR01F012/getList/getInfo` | 🔍 | | (미확인 라우트) | `https://nedrug.mfds.go.kr/PPL0000` | 🔍 | | `$.download` 플러그인 | `https://nedrug.mfds.go.kr/resources/js/common-pbp.js` | 🔍 | ### A.2 한국 — 식약처 본청 / 법령 / 정부 | 제목 | URL | 확인 | |---|---|---| | 식약처 저작권정책 | `https://www.mfds.go.kr/wpge/m_34/de010803l001.do` | ✅ | | 「원료의약품 등록에 관한 규정」 고시전문 (제2024-89호) | `https://www.mfds.go.kr/brd/m_211/view.do?seq=14870&srchFr=&srchTo=&srchWord=&srchTp=&itm_seq_1=0&itm_seq_2=0&multi_itm_seq=0&company_cd=&company_nm=&Data_stts=A&page=1` | ✅ | | 「원료의약품 등록에 관한 규정」 (구 게시글) | `https://www.mfds.go.kr/brd/m_211/view.do?seq=14569&...&page=1` | 🔍 | | DMF 해설서 제3개정판 의견조회 | `https://www.mfds.go.kr/brd/m_74/view.do?seq=43076&...&page=1` | 🔍 | | 동(다른 page 파라미터) | `https://mfds.go.kr/brd/m_74/view.do?seq=43076&...&page=66` | 🔍 | | 「원료등록(DMF) 기간 대폭 단축!」 첨부 | `https://www.mfds.go.kr/brd/m_99/down.do?brd_id=ntc0021&seq=48245&data_tp=A&file_seq=2` | 🔍 | | 「붙임1. 개별 항목 검증 룰」 | `https://www.mfds.go.kr/brd/m_1060/down.do?brd_id=data0011&seq=14313&data_tp=A&file_seq=2` | 🔍 | | 식약처 공공자료 개방 | `https://www.mfds.go.kr/usr/opendata_13/list.do` | 🔍 | | 식약처 홈 | `https://www.mfds.go.kr/` | 🔍 | | 구 공고 원문 URL (2013) | `http://www.mfds.go.kr/index.do?mid=70&pageNo=1&seq=14766&cmd=v` | 🔍 | | 의약품 전자민원창구 | `http://ezdrug.mfds.go.kr` | 🔍 | | 국가법령정보센터 — 원료의약품 등록에 관한 규정 | `https://www.law.go.kr/%ED%96%89%EC%A0%95%EA%B7%9C%EC%B9%99/%EC%9B%90%EB%A3%8C%EC%9D%98%EC%95%BD%ED%92%88%EB%93%B1%EB%A1%9D%EC%97%90%EA%B4%80%ED%95%9C%EA%B7%9C%EC%A0%95` | ⚠️ | | 동 (admRulSeq=2100000252602) | `https://law.go.kr/LSW/admRulLsInfoP.do?admRulSeq=2100000252602` | ⚠️ | | 동 (admRulInfoP) | `https://www.law.go.kr/LSW/admRulInfoP.do?admRulSeq=2100000252602` | ⚠️ | | 동 (admRulSeq=2100000150569) | `https://www.law.go.kr/LSW/admRulLsInfoP.do?admRulSeq=2100000150569` | 🔍 | | 동 (admRulSeq=2100000198136) | `https://law.go.kr/LSW/admRulLsInfoP.do?admRulSeq=2100000198136` | 🔍 | | 동 조문 팝업 | `https://www.law.go.kr/conAdmrulByLsPop.do?lsiSeq=217283&joNo=0031&joBrNo=02&datClsCd=010102&dguBun=DEG&lnkText=%EC%8B%9D%ED%92%88%EC%9D%98%EC%95%BD%ED%92%88%EC%95%88%EC%A0%84%EC%B2%98%EC%9E%A5%EC%9D%B4+%EC%A0%95%ED%95%98%EC%97%AC+%EA%B3%A0%EC%8B%9C%ED%95%98%EB%8A%94&admRulPttninfSeq=1526` | 🔍 | | 정부24 — 원료의약품(등록신청, 등록사항 변경등록신청) | `https://www.gov.kr/mw/AA020InfoCappView.do?HighCtgCD=A09006&CappBizCD=14700000458&tp_seq=` | ✅ | | 식품의약품안전평가원 KDMF | `https://www.nifds.go.kr/brd/m_87/list.do` | ✅ | | 식품의약품안전평가원 홈 | `https://www.nifds.go.kr/` | 🔍 | ### A.3 한국 — 공공데이터 | 제목 | URL | 확인 | |---|---|---| | 식품의약품안전처_원료의약품등록(DMF)현황 | `https://www.data.go.kr/data/15057075/openapi.do` | ✅ | | 동 API 엔드포인트(서비스) | `https://apis.data.go.kr/1471000/MdcDmfInfoService01` | ✅(명세) | | 동 오퍼레이션 | `http://apis.data.go.kr/1471000/MdcDmfInfoService01/getMdcDmfList01` | ✅(명세) | | 식의약 데이터 포털 | `https://data.mfds.go.kr/` | 🔍 | | 공공데이터 상세 | `https://data.mfds.go.kr/OPCAA01F01` | 🚫 | | 공공데이터 검색 (taskDivsCd=2) | `https://data.mfds.go.kr/OPCAA01F01/search?selectedTab=tab1&taskDivsCd=2&taskDivsDtlCd=3&srchSrvcKorNm=&btnSearch=` | 🔍 | | 공공데이터 검색 (taskDivsCd=3) | `https://data.mfds.go.kr/OPCAA01F01/search?selectedTab=tab1&taskDivsCd=3&taskDivsDtlCd=7&rchSrvcKorNm=&btnSearch=` | 🔍 | | 공공데이터포털 식약처 데이터 목록 | `https://www.data.go.kr/tcs/dss/selectDataSetList.do?keyword=%22%EC%8B%9D%ED%92%88%EC%9D%98%EC%95%BD%ED%92%88%EC%95%88%EC%A0%84%EC%B2%98%22&recmSe=N` | 🔍 | | (참고) 의약품 낱알식별 정보 | `https://www.data.go.kr/data/15057639/openapi.do?recommendDataYn=Y` | 🔍 | | (참고) 건강기능식품 영양DB | `https://www.data.go.kr/data/15085712/openapi.do` | 🔍 | | (참고) 구 데이터셋 URL 형식 | `https://www.data.go.kr/dataset/15020627/openapi.do` | 🔍 | ### A.4 한국 — 협회·연구기관·언론 | 제목 | URL | 확인 | |---|---|---| | KPBMA — DMF 해설서 제5개정판 (PDF 118p) | `https://www.kpbma.or.kr/api/info/law/policy/download/211364` | ✅(바이너리 추출) | | KPBMA — 「원료의약품 등록에 관한 규정」 일부개정 알림 | `https://www.kpbma.or.kr/info/law/statute/select/210036` | ✅ | | 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` | ✅ | | KHIDI — [지침]원료의약품 등록(DMF) 처리 절차 | `https://www.khidi.or.kr/board/view?pageNum=1&rowCnt=10&menuId=MENU01872&maxIndex=99999999999999&minIndex=99999999999999&schType=0&upDown=0&no1=0&linkId=26605812` | ⚠️ | | KHIDI — 원료의약품 등록에 관한 규정 (제2021-8호) | `https://www.khidi.or.kr/board/view?pageNum=28&rowCnt=10&menuId=MENU01872&maxIndex=&minIndex=&schType=0&schText=&categoryId=&continent=&country=&upDown=0&boardStyle=&no1=3182&linkId=48852840` | ✅ | | KDI — 원료의약품 등록 제도(DMF) 꼼꼼히 알기! | `https://eiec.kdi.re.kr/policy/materialView.do?datecount=&num=209012&pg=&pp=20&recommend=&topic=L` | ✅ | | KDI — 원료의약품 등록제도(DMF), 궁금하면 클릭하세요! | `https://eiec.kdi.re.kr/policy/materialView.do?num=196213&topic=L&pp=20` | ✅ | | KDI — 식약처 공문서, '의약품안전나라'로 쉽고 편하게 받으세요! | `https://eiec.kdi.re.kr/policy/materialView.do?num=230838` | 🔍 | | 관세무역개발원 — 원료의약품 등록 기간 '120일→20일' | `https://www.kctdi.or.kr/kctdi/research/research11.do?mode=view&articleNo=3707&title=%EC%9B%90%EB%A3%8C%EC%9D%98%EC%95%BD%ED%92%88+%EB%93%B1%EB%A1%9D+%EA%B8%B0%EA%B0%84+%27120%EC%9D%BC%E2%86%9220%EC%9D%BC%27%EB%A1%9C+%EB%8C%80%ED%8F%AD+%EB%8B%A8%EC%B6%95` | 🔍 | | KCIA — 2021년 자주하는 질문집(의약품 분야) PDF | `https://kcia.or.kr/inc/down.php?dir=BOARD&file_name=202111_163704850587622_2.pdf&rename=%EB%B6%99%EC%9E%84_2021%EB%85%84+%EC%9E%90%EC%A3%BC%ED%95%98%EB%8A%94+%EC%A7%88%EB%AC%B8%EC%A7%91(%EC%9D%98%EC%95%BD%ED%92%88+%EB%B6%84%EC%95%BC).pdf` | 🔍 | | 데일리팜 — 규제완화 여파...원료약 등록 1년새 237→653건 | `https://www.dailypharm.com/Users/News/NewsView.html?ID=324241` | ✅ | | 데일리팜(모바일) — 필수약·공급중단 보고대상 의약품 'DMF 등록' 유예 | `http://m.dailypharm.com/newsView.html?ID=295979` | ✅ | | 히트뉴스 — 수입 원료의약품 등록, 이제 'GMP 증명서'만 있으면 OK | `https://www.hitnews.co.kr/news/articleView.html?idxno=54607` | ✅ | | 히트뉴스 — "국민·업자 원하는 대로…" 의약품안전나라, 탈바꿈 | `https://www.hitnews.co.kr/news/articleView.html?idxno=16548` | ✅ | | 약사공론 — 약가제도 개편 무엇이 바뀌고, 어떻게 준비해야 하나 | `https://www.kpanews.co.kr/article/show.asp?idx=211278&category=C` | 🔍 | | 코멘토 — 제약 RA 현직자 실무 체험 | `https://comento.kr/edu/learn/%EC%97%B0%EA%B5%AC%EA%B0%9C%EB%B0%9C/%EC%9D%B8%ED%97%88%EA%B0%80%EA%B0%9C%EB%B0%9C-G676` | 🔍 | | 비즈니스피플 — [중견사]원료 의약품 RA 채용 | `https://www.bzpp.co.kr/biz/businessDetailView/BR250423A00015` | 🔍 | | 질병관리청 — 약품/식품정보 | `https://health.kdca.go.kr/healthinfo/biz/health/gnrlzHealthInfo/healthInfo/medcinFoodInfoMain.do` | 🔍 | ### A.5 미국 (FDA) | 제목 | URL | 확인 | |---|---|---| | Drug Master Files (DMFs) | `https://www.fda.gov/drugs/forms-submission-requirements/drug-master-files-dmfs` | ❌ 404 | | List of Drug Master Files (DMFs) | `https://www.fda.gov/drugs/drug-master-files-dmfs/list-drug-master-files-dmfs` | ❌ 404 | | Drug Master Files (DMFs) — cacmap 미러 | `https://cacmap.fda.gov/drugs/forms-submission-requirements/drug-master-files-dmfs` | 🔍 | | List of DMFs — cacmap 미러 | `https://cacmap.fda.gov/drugs/gdufa-ii-drug-master-files-dmfs/list-drug-master-files-dmfs` | ❌ 403 | | (레거시 URL) | `https://www.fda.gov/drugs/developmentapprovalprocess/formssubmissionrequirements/drugmasterfilesdmfs/default.htm` | 🔍 | | FDA media download | `https://www.fda.gov/media/186970/download` | 🔍 | | 봇 차단 안내 페이지 | `https://www.fda.gov/apology_objects/abuse-detection-apology.html` | ✅(차단 확인) | | John Snow Labs — FDA DMF Directory | `https://www.johnsnowlabs.com/marketplace/fda-drug-master-files-directory/` | ✅ | | PharmaCompass — US DMF Database | `https://www.pharmacompass.com/us-drug-master-files-dmfs` | ❌ 403 | | dmf-list 미러 | `https://dmf-list.backgroundscheck.info/` | 🔍 | | FDApals — DMF 서비스 | `https://fdapals.com/services/dmf-fda-guidance/` | 🔍 | | GMP Navigator — Drug Master Files (2006) PDF | `https://www.gmp-navigator.com/files/guidemgr/Drug_Master_Files_Current_Information_2006.pdf` | 🔍 | | Duke Medical Library 블로그 | `https://mclibrary.duke.edu/about/blog/2014-12-04/fda-drug-master-files` | ❌ 404 | | Wikipedia — Drug Master File | `https://en.wikipedia.org/wiki/Drug_Master_File` | ✅ | | Scribd — Drug Master File 프레젠테이션 | `https://www.scribd.com/presentation/672546507/Drug-Master-File` | 🔍 | ### A.6 유럽 (EDQM) | 제목 | URL | 확인 | |---|---|---| | EDQM Certification Database (Certificates catalogue) | `https://extranet.edqm.eu/publications/recherches_CEP.shtml` | ✅ | | **CEP 전량 다운로드 (TSV)** | `https://extranet.edqm.eu/4DLink1/4DCGI/EXPORT_WEB_CEP.txt` | ✅ | | Databases | `https://www.edqm.eu/en/databases` | ❌ 403 | | Deployment of a new EDQM CEP database feature: full download of CEP data | `https://www.edqm.eu/en/-/deployment-of-a-new-edqm-cep-database-feature-full-download-of-cep-data` | ❌ 403 | | Certification of Suitability (CEP) | `https://www.edqm.eu/en/certification` | 🔍 | | What is the CEP 2.0? | `https://www.edqm.eu/en/what-is-the-cep-2.0` | 🔍 | | Certification Policy Documents & Guidelines | `https://www.edqm.eu/en/certification-policy-documents-guidelines` | 🔍 | | Actions on CEPs | `https://www.edqm.eu/en/actions-on-ceps` | 🔍 | | KNOWLEDGE Database | `https://www.edqm.eu/en/knowledge-database` | 🔍 | | FAQ — How do I know if I have the latest valid version of a CEP? | `https://faq.edqm.eu/pages/viewpage.action?pageId=1377010` | 🚫 | | User Guide for Certification on-Line Database (PA_PH_CEP (23) 56, May 2024) | `https://www.scribd.com/document/849026918/User-Guide-for-Certification-on-Line-Database-PA-PH-CEP-23-56-May-2024` | 🔍 | | PharmaCompass — CEP/COS Database | `https://www.pharmacompass.com/certificates-of-suitability-products-cep` | 🔍 | | LinkedIn — EDQM CEP Database 소개 | `https://www.linkedin.com/posts/regulatory-affairs-insights_regulatoryaffairs-edqm-cep-activity-7375459258387042304-Xp8S` | 🔍 | | Wikipedia — EDQM | `https://en.wikipedia.org/wiki/European_Directorate_for_the_Quality_of_Medicines_%26_HealthCare` | 🔍 | ### A.7 일본 (PMDA) | 제목 | URL | 확인 | |---|---|---| | 原薬等登録原簿(MF)の公示について | `https://www.pmda.go.jp/review-services/drug-reviews/master-files/0008.html` | ✅ | | **MF 공시 Excel** | `https://www.pmda.go.jp/files/000240884.xlsx` | ✅ | | MF 공시 PDF | `https://www.pmda.go.jp/files/000240885.pdf` | 🔍 | | 原薬等登録原簿(MF) 개요 | `https://www.pmda.go.jp/review-services/drug-reviews/master-files/0007.html` | ✅ | | MF의 登録整理届書について | `https://www.pmda.go.jp/review-services/drug-reviews/master-files/0003.html` | 🔍 | | 영문 Q&A/실시 가이드 | `https://www.pmda.go.jp/files/000280630.pdf` | 🔍 | | MF 신청·수속 최신 유의사항 | `https://www.pmda.go.jp/files/000237522.pdf` | 🔍 | | 医薬品基準課MF管理室 자료 | `https://www.pmda.go.jp/files/000227202.pdf` | 🔍 | | 日本ジェネリック製薬協会 — MF登録について | `https://www.jga.gr.jp/jgapedia/deals/_19341.html` | 🔍 | | 日本製薬工業協会 — MF 신뢰성보증 | `https://www.jpma.or.jp/information/quality/jirei/bbh7c90000000jbr-att/2023-3.pdf` | 🔍 | | Wikipedia — PMDA | `https://en.wikipedia.org/wiki/Pharmaceuticals_and_Medical_Devices_Agency` | 🔍 | ### A.8 참고·법제 일반 / 검색 노이즈 | 제목 | URL | 확인 | 비고 | |---|---|---|---| | Wikipedia — Korea Open Government License | `https://en.wikipedia.org/wiki/Korea_Open_Government_License` | 🔍 | 공공누리 | | Wikipedia — Copyright law of South Korea | `https://en.wikipedia.org/wiki/Copyright_law_of_South_Korea` | 🔍 | | | Wikipedia — Ministry of Food and Drug Safety | `https://en.wikipedia.org/wiki/Ministry_of_Food_and_Drug_Safety` | 🔍 | | | Wikipedia — Approved Drug Products with Therapeutic Equivalence Evaluations | `https://en.wikipedia.org/wiki/Approved_Drug_Products_with_Therapeutic_Equivalence_Evaluations` | 🔍 | Orange Book | | Wikipedia — DMF (동음이의) | `https://en.wikipedia.org/wiki/DMF` | 🔍 | 노이즈 | | Wikipedia — Distribution Media Format | `https://en.wikipedia.org/wiki/Distribution_Media_Format` | 🔍 | 노이즈 | | Wikipedia — Dimethylformamide | `https://en.wikipedia.org/wiki/Dimethylformamide` | 🔍 | 노이즈 | | Wikipedia — 3,5-Difluoromethcathinone | `https://en.wikipedia.org/wiki/3,5-Difluoromethcathinone` | 🔍 | 노이즈 | | Wikipedia — α-Difluoromethyl-DOPA | `https://en.wikipedia.org/wiki/%CE%91-Difluoromethyl-DOPA` | 🔍 | 노이즈 | | Wikipedia — Non-racemic MDMA | `https://en.wikipedia.org/wiki/Non-racemic_MDMA` | 🔍 | 노이즈 | | Wikipedia — NED-19 | `https://en.wikipedia.org/wiki/NED-19` | 🔍 | 노이즈 | | Wikipedia — Urimalsaem | `https://en.wikipedia.org/wiki/Urimalsaem` | 🔍 | 노이즈 | | ResearchGate — DMF-scMT-seq | `https://www.researchgate.net/publication/380826072_DMF-scMT-seq_linking_methylome_and_transcriptome_within_single_cells_with_digital_microfluidics` | 🔍 | 노이즈(약어 충돌) | **총 보존 URL 수: 100개** (중복 제거 기준) --- ## 부록 B. 미해결 질문 / 실측 필요 항목 ### B.1 데이터 스키마 확정을 위한 필수 실측 - [ ] **`취소/취하구분(cancelCodeNm)` 의 전체 코드값 집합** — `정상` 외에 어떤 문자열이 오는가? (`취하`, `취소`, `자진취하`, `직권취소` …) → 전량 엑셀에서 `SELECT DISTINCT` 수행 - [ ] **`대상의약품(noticeCode)` 의 전체 값 집합** — `별표1`, `신물질` 외에 `별표1의2`(한약(생약)), `인태반` 값이 존재하는가? - [ ] **`최종변경일자` ↔ `yrycReportNDate` 매핑 검증** — 정렬키를 실제로 눌러 응답 순서가 바뀌는지 확인 (`sort=yrycReportNDate&sortOrder=false`) - [ ] **`대상의약품` ↔ `noticeCode` 매핑 검증** — 동일 방법 - [ ] **`문서번호(dmfVersion)` 의 값 패턴** — `v0.0.0/2026` 외 형식(`v1.0.0/2025` 등)이 존재하는가? 변경 시 실제로 증가하는가? - [ ] **`연계심사문서번호(cntcJdgmnNo)` 가 채워진 행의 비율과 형식** - [ ] **신물질 등록번호 포맷의 전수 조사** — `수6580-16-ND(20)` 외 접두어(`제`, `허` 등)와 변형이 몇 종인가? `대상의약품 == '신물질'` 인 행 전량 추출해 정규식 커버리지 확인 - [ ] **등록번호 알파벳군의 실제 관측 집합** — A~K 전부 존재하는가? 문서에 없는 군(예: `L`)이 있는가? - [ ] **다중 제조소 행의 구분자 규칙** — 화면 HTML 중첩 `` 개수와 엑셀 셀 내 콤마 개수가 일치하는가? (엑셀은 `,` 로 병합되어 주소 내부 콤마와 충돌할 수 있음) ### B.2 수집 파이프라인 검증 - [ ] **`getExcel` POST 의 최소 필수 파라미터 집합 확정** — 원 실측에서 사용한 정확한 POST body 가 로그에서 잘렸다. `dmfAcceptDateStart`/`End` 만으로 충분한지, `limit` 값이 결과 건수에 영향을 주는지 A/B 검증 - [ ] `limit=10000` 이 아니라 `limit=100000` 이어도 서버가 전량을 반환하는가? 상한선은? - [ ] **세션 쿠키 없이 `getExcel` POST 가 성공하는가?** (`fileDownloadToken` 쿠키의 역할) - [ ] **`/pbp/CCBAC03/getItem?dmfSeq=N` 라우트가 실제로 살아 있는가?** 살아 있다면 상세 페이지에 목록에 없는 추가 필드(자료 미첨부 사실 = 규칙 제16조 5호 등)가 있는가? - [ ] `sort`/`sortOrder` 파라미터로 **`최초등록일자 DESC` 정렬 후 상위 N건만 받는 경량 모드**가 가능한가? (일일 신규 감지용 최적화) - [ ] 엑셀 `Sheet0` 의 `sharedStrings.xml` 사용 여부 — 실측에서는 `inlineStr` 만 관측됨. 데이터가 커지면 sharedStrings 로 바뀔 가능성 대비 - [ ] **공공데이터포털 API `numOfRows` 상한** (명세상 3자리 = 999?) 및 실제 `totalCount` 가 9,840과 일치하는지 ### B.3 법령·제도 확인 - [ ] **「원료의약품 등록에 관한 규정」 제2024-89호 전문(.hwpx) 다운로드 후 조문 확인** — 본 문서 §2.4·§2.5 의 조문 인용은 2020년 해설서 재수록본이므로 현행성 검증 필요 - [ ] **제5조 처리기한**: 해설서의 `17주/13주` vs MaPP의 `20일/90일` — 현행 고시의 실제 문구는? - [ ] **등록 취하·취소에 관한 명시 조문** — 규칙/고시 어디에도 "취소" 사유 조항을 확인하지 못했다. 약사법 제31조의2에는 취소 규정이 없다. 화면 컬럼 `취소/취하구분`의 법적 근거는? - [ ] **[별표1] 성분 목록 전체(1~211호) 확보** — 등록번호 `ingr_no` 를 성분명으로 역매핑하려면 필수. 현행 고시 별표1 파싱 필요 - [ ] **[별표1의2] 한약(생약) 성분 목록(1~19호) 확보** - [ ] 「원료의약품 등록(DMF) 처리 절차」 지침(2017-03-30) HWP 첨부 확보 — 등록번호 부여 방식의 공식 절차 문서 - [ ] **DMF 해설서 최신판(제6개정판 이상)이 존재하는가?** 현재 확보본은 제5개정판(2020.12.) - [ ] MaPP 지침서-0943-03 (2025.2.20. 6개정) 원본 PDF 출처 URL 확보 (현재는 텍스트만 보유) ### B.4 정책·법적 리스크 - [ ] **nedrug.mfds.go.kr 이용약관 전문 확인** — 자동수집 금지 조항 유무 - [ ] **DMF 공고 데이터의 공공누리 유형(1~4유형) 명시 확인** — `/cntnts/80` 에 유형 표기가 없었다 - [ ] **`robots.txt` 전면 차단과 공공데이터 개방 정책의 충돌**에 대해 식약처 문의(민원 1577-1255 / 기술지원 1544-9563) 후 공식 답변 확보 권장 - [ ] "내용에 대한 임의 가공 금지" 조항의 해석 범위 — 파생 지표(집계·랭킹) 생성이 '가공'에 해당하는가? ### B.5 해외 소스 - [ ] **FDA DMF 목록 xlsx 의 정확한 다운로드 URL** — WAF 우회(Playwright) 후 확인 필요. 검색 스니펫상 "current through DMF 043437", "last update September 30, 2025", "DMFs received by December 31, 2025" - [ ] FDA 목록의 **원본 컬럼명** (John Snow Labs 재가공본이 아닌 FDA 원본 헤더) - [ ] FDA DMF **status 코드 `A`/`I`** 의 정확한 값 표기 - [ ] **PMDA xlsx 파일명 변경 규칙** — `000240884.xlsx` 는 공시 회차마다 바뀐다. 페이지 파싱으로 최신 링크를 추출하는 로직(§8.5 `find_pmda_xlsx_url`)의 안정성 검증 - [ ] PMDA `登録区分` 의 전체 값 집합 (`医薬品等原薬` 외 医療機器·再生医療等製品·体外診断用医薬品 관련 값) - [ ] EDQM `Status CEP` 의 전체 값 집합 (`Withdrawn by Holder`, `Expired` 외 `Valid`, `Suspended` 등) - [ ] EDQM `Type CEP` 의 전체 값 집합 (`TSE` 외 `Chemical purity`, `Herbal` 등) - [ ] EDQM 다운로드 파일의 **총 행 수** (1,459,534 bytes 인데 행 수 미집계) ### B.6 리포트/운영 - [ ] 워치리스트 초기값을 누가 어떻게 채울 것인가 (사용자 입력 UI vs YAML 직접 편집) - [ ] Windows 11 기본 콘솔 cp949 문제 → `PYTHONIOENCODING=utf-8` 을 스케줄러 작업에 확실히 주입하는 방법 검증 - [ ] 전일 스냅샷이 없는 최초 실행일(cold start) 처리 — 전량을 `NEW` 로 볼 것인가, 이벤트 없이 기준선만 세울 것인가 (**후자 권장**) - [ ] 9,840행 × 28컬럼 xlsx 리포트의 생성 시간·파일 크기 목표치 설정 --- ## 이 문서와 확정 설계의 관계 `design/00-DATA-SOURCE-DECISION.md`(API 채택 결정)와 `design/00b-baseline-data-analysis.md`(기존 xlsx 실측)에 따라, 이 문서 각 절의 지위를 정리한다. | 절 | 상태 | 비고 | |---|---|---| | 0. 한눈에 보기 | ⚠️ 부분 반증 / 부분 미채택 | 불릿별 정정 표시 참조. "API 단독 불충분" 결론은 ❌ 반증, "CCBAC03 주 수집원"·"절제된 수집 정책"·"14+9 필드 확정"은 ⚠️ 미채택, 나머지는 ✅ 유효 | | 2. DMF 제도 개요 (2.1~2.9) | ✅ 유효 | 법령·연혁·처리기한·통계는 데이터 소스와 무관하게 그대로 유효 | | 3. 등록번호 체계 완전 해부 | ✅ 유효 | 파싱 규칙·정규식·`GROUP_TABLE` 이 API 의 `dmf_permit_no` 필드에도 그대로 적용됨(§12.1) | | 4. CCBAC03 화면 구조 조사 | ⚠️ 참고자료로 격하 (수집 대상 아님) | §4.5·§4.6 의 화면 컬럼↔서버 필드 대응관계만 유효. 엑셀 POST/HTML 파싱 코드(§4.9·§4.10)는 구현 대상 아님 | | 5. `/bbs/117` 주간 공고 게시판 | ✅ 유효(참고, 원문 결론 그대로) | 갱신 중단 사실과 "일일 크롤링 대상에서 제외"라는 원 결론이 그대로 유지됨. 애초에 화면 자체를 수집하지 않으므로 이 결론과 상충하지 않음 | | 6. 공공데이터포털 OpenAPI | ⚠️ 지위 격상 — 명세는 유효, "보조·검증용" 결론만 반증 | §6.1~6.5 의 API 명세·파라미터·응답 필드·에러코드는 그대로 유효하고 지금은 **유일 수집원**이다. §6.6 "주 수집원 ✕" 표는 반증됨(design/00b §6) | | 7. 식의약 데이터 포털/기타 국내 채널 | ✅ 유효 | | | 8. 해외 DMF 소스 (FDA/EDQM/PMDA) | ✅ 유효 | v1 범위 결정(§8.6)도 그대로 유효 | | 9. robots.txt·저작권·이용약관 | ✅ 유효(사실) / ⚠️ 미채택(실행 정책) | 실측 사실과 법적 검토(저작권법 제24조의2 등)는 유효. §9.6 "절제된 수집" 실행 정책은 HTML 을 크롤링하지 않으므로 적용 대상이 없음 | | 10. 제약사 RA 실무자의 모니터링 관점 | ✅ 유효 | 4축 구조와 이벤트 타입 정의는 유효하나, 이벤트 판정 조건(§10.3)의 일부(`SITE_CHANGED`·`APPLICANT_CHANGED`·`DOC_VERSION_CHANGED`·`LINKED_REVIEW_SET` 등 화면 전용 필드 기반)는 §12 의 API 기반 `status` 판정으로 대체됨 | | 11. 언론·유사 서비스가 DMF를 다루는 방식 | ✅ 유효 | | | 12. 수집 대상 필드 확정안 | ❌ 전면 재작성됨 | CCBAC03 14컬럼 기준에서 API 7필드 + 파생 9필드 기준으로 교체(design/00b 근거) | | 부록 A. 출처 목록 | ✅ 유효 | 조사에 사용된 URL 전량 보존 | | 부록 B. 미해결 질문 / 실측 필요 항목 | ⚠️ 부분 해소 | B.1 의 "등록번호 유일성", "발급일자 성격" 은 design/00b 로 해소됨. B.2(`getExcel` 파라미터 등 화면 수집 관련)는 미채택 경로라 더 이상 우선순위 아님 | **미작성 절 확인**: §1 목차가 예고한 §2~§12·부록 A·부록 B 전 절이 본문에 실제로 존재함을 확인했다(2026-09-02 재확인). 누락된 절은 없다. --- > **문서 이력**: v1.0 (2026-09-02 작성). 원천 raw dump: `agent-aefc6c5fd48ce96ef.md` (1,755줄) + 동 세션 수집 아티팩트(`ccbac03.html` 400,143 bytes, `bbs117.html` 382,506 bytes, `datago.html` 204,048 bytes, `dmf_excel2.xlsx` 1,379,451 bytes, `edqm_export.txt` 1,459,534 bytes, `pmda_mf.xlsx` 428,927 bytes, `haeseolseo.txt` 262,458 bytes = DMF 해설서 제5개정판 PDF 118p 추출, `mfds_change.txt` 53,007 bytes = MaPP 지침서-0943-03 추출). v1.1 (2026-09-02 정정) — `design/00b-baseline-data-analysis.md` 실측 반영, §0 반증/미채택 표시, §4 제목 변경, §12 전면 재작성, 관계 표 추가.