chore: 저장소 구조 정리 및 문서화, 첫 커밋
- src/dist 산출물 분리 원칙 정리(.gitignore, .gitattributes) - 루트 및 주요 폴더(config/scripts/prompts/tests/src, 런타임 폴더 5종)에 안내용 README.md 추가 - CHANGELOG.md, LICENSE, docs/ops/05-release-and-versioning.md 추가 - docs/README.md 문서 지도 갱신
This commit is contained in:
commit
56a6e2da93
159 changed files with 145825 additions and 0 deletions
591
docs/design/00b-baseline-data-analysis.md
Normal file
591
docs/design/00b-baseline-data-analysis.md
Normal file
|
|
@ -0,0 +1,591 @@
|
|||
# 기준선 데이터 분석 — 기존 `DMF_현황.xlsx` 실측
|
||||
|
||||
> **이 문서의 역할**: 사용자가 이미 운영 중이던 `DMF_현황.xlsx` 를 전수 분석한 결과. **이 프로젝트는 스크래치가 아니라 기존 프로토타입의 개선**이며, 이 문서가 그 출발점의 사실 기록이다. 데이터 모델·변경 탐지·정규화 규칙의 근거가 전부 여기서 나온다.
|
||||
|
||||
**분석일**: 2026-09-02
|
||||
**대상 파일**: `C:\Users\encep\OneDrive\문서\카카오톡 받은 파일\DMF_현황.xlsx` (757,331 bytes, 수정 2026-09-02 21:38)
|
||||
**분석 도구**: openpyxl 3.1.5, pandas 2.2.3
|
||||
|
||||
---
|
||||
|
||||
## 0. 한눈에 보기
|
||||
|
||||
- **이 프로젝트는 스크래치가 아니다.** 이미 공식 Open API 로 수집해 xlsx 를 만드는 프로토타입이 돌고 있었고, 그 산출물이 이 파일이다. 컬럼이 API 7필드와 정확히 일치한다.
|
||||
- **API 전체 건수는 9,084건이다.** 웹 화면 실측 9,840건과 **756건 차이**가 난다. 이 차이가 **API 는 취하·취소 건을 제외하고 정상 건만 반환한다**는 강력한 증거다.
|
||||
- **따라서 취하 탐지는 diff 로 가능하다.** 레코드가 API 응답에서 사라지면 취하다. "API 7필드로는 취하를 탐지할 수 없다"던 우려는 해소 방향이다.
|
||||
- **변경 탐지도 가능하다.** 등록번호 앞 8자리(최초 등록일)와 `발급일자`가 **44.5% 불일치**하며, 불일치하는 건은 전부 괄호가 붙은 갱신 건이다. 즉 **`발급일자`는 최종 갱신일로 움직이는 필드**다. 이것이 변경의 직접 신호다.
|
||||
- **등록번호는 완전한 자연 키다.** 9,084건 중 중복 0건. 별도 대체 키가 필요 없다.
|
||||
- **데이터 품질 문제 4종을 확인했다.** 국가 중복 표기 496건, 성분명 표기 흔들림 21그룹, 업체명 표기 흔들림 2그룹, 제조소명 공백 오류 42건. 정규화 계층이 반드시 필요하다.
|
||||
- **기존 xlsx 에는 서식이 전혀 없다.** 틀 고정, 자동 필터, 조건부 서식, 차트가 모두 0이다. "디자인 예쁘게" 요구가 겨냥하는 지점이 정확히 여기다.
|
||||
- 데이터는 **2003-04-18부터 2026-09-01까지 23년치**다. 성분 1,539종, 업체 438개, 제조소 3,051개, 국가 49개.
|
||||
|
||||
---
|
||||
|
||||
## 1. 목차
|
||||
|
||||
1. [파일 구조](#2-파일-구조)
|
||||
2. [컬럼과 API 필드 대응](#3-컬럼과-api-필드-대응)
|
||||
3. [건수 검증 — 취하 탐지 가능성의 결정적 근거](#4-건수-검증--취하-탐지-가능성의-결정적-근거)
|
||||
4. [등록번호 분석](#5-등록번호-분석)
|
||||
5. [변경 탐지 가능성 — 발급일자의 의미](#6-변경-탐지-가능성--발급일자의-의미)
|
||||
6. [데이터 규모와 분포](#7-데이터-규모와-분포)
|
||||
7. [데이터 품질 문제](#8-데이터-품질-문제)
|
||||
8. [기존 서식 상태와 개선 여지](#9-기존-서식-상태와-개선-여지)
|
||||
9. [확정되는 설계 결정](#10-확정되는-설계-결정)
|
||||
10. [부록 A. 재현 스크립트](#부록-a-재현-스크립트)
|
||||
11. [부록 B. 미해결 / 확인 필요](#부록-b-미해결--확인-필요)
|
||||
|
||||
---
|
||||
|
||||
## 2. 파일 구조
|
||||
|
||||
| 시트 | 범위 | 행 | 열 | 내용 |
|
||||
|---|---|---|---|---|
|
||||
| `전체` | A1:H9085 | 9,085 (헤더 1 + 데이터 9,084) | 8 | 누적 DMF 등록 목록 |
|
||||
| `신규` | A1:H1 | 1 (헤더만) | 8 | 오늘의 신규 건. 첫 실행이라 비어 있음 |
|
||||
| `갱신이력` | A1:C2 | 2 (헤더 1 + 1행) | 3 | 실행 로그 |
|
||||
|
||||
`갱신이력` 시트의 유일한 데이터 행:
|
||||
|
||||
| 실행일 | 누적건수 | 신규건수 |
|
||||
|---|---|---|
|
||||
| 2026-09-02 | 9084 | 0 |
|
||||
|
||||
**해석**: 2026-09-02 에 최초 실행되어 9,084건을 기준선으로 적재했고, 비교 대상이 없어 신규는 0으로 기록됐다. 설계 의도가 이미 "스냅샷 + 신규 + 실행 이력" 3층 구조였다는 뜻이다. **이 구조를 버리지 않고 확장한다.**
|
||||
|
||||
---
|
||||
|
||||
## 3. 컬럼과 API 필드 대응
|
||||
|
||||
| # | xlsx 컬럼 | API 응답 필드 | 비고 |
|
||||
|---|---|---|---|
|
||||
| 1 | 등록번호 | `DMF_PERMIT_NO` | 자연 키 |
|
||||
| 2 | 성분명 | `INGR_KOR_NAME` | |
|
||||
| 3 | 업체명 | `ENTP_NAME` | 화면에서는 "신청인" |
|
||||
| 4 | 제조소명 | `MNFCTR_NAME` | |
|
||||
| 5 | 제조소소재지 | `MNFCTR_PLACE` | |
|
||||
| 6 | 제조국가명 | `MANUF_COUNTRY_CODE_NM` | 콤마 다중값 |
|
||||
| 7 | 발급일자 | `DMF_PERMIT_DATE` | **최종 갱신일로 동작** (6절) |
|
||||
| 8 | 최초수집일 | *(파생)* | 이 시스템이 처음 이 레코드를 본 날 |
|
||||
|
||||
**확인 사항**: 8개 컬럼 중 7개가 API 필드와 1:1로 정확히 대응한다. 8번째 `최초수집일`은 프로토타입이 스스로 만든 파생 컬럼이다. 좋은 설계이므로 **유지하고, 여기에 `최종변경감지일`·`상태`·`변경차수`를 추가**한다.
|
||||
|
||||
**웹 화면에만 있는 컬럼**(API 미제공): `대상의약품`, `최종변경일자`, `최종연차보고년도`, `취소/취하구분`, `취소/취하일자`, `문서번호`. 이 중 실질적 손실과 대체 수단은 10절 참조.
|
||||
|
||||
---
|
||||
|
||||
## 4. 건수 검증 — 취하 탐지 가능성의 결정적 근거
|
||||
|
||||
| 소스 | 건수 | 출처 |
|
||||
|---|---|---|
|
||||
| 웹 화면 (`/pbp/CCBAC03`) | **9,840** | 조사 문서 `research/01` 실측 (2026-09-02) |
|
||||
| 공식 Open API | **9,084** | 이 파일 실측 (2026-09-02) |
|
||||
| **차이** | **756** | |
|
||||
|
||||
두 수치는 같은 날 측정됐다. 756건의 차이를 설명하는 가설은 둘뿐이다.
|
||||
|
||||
| 가설 | 내용 | 판정 |
|
||||
|---|---|---|
|
||||
| **A** | API 가 취하·취소된 건을 제외하고 **정상 건만** 반환한다 | **유력** |
|
||||
| B | API 데이터가 웹보다 오래됐다 | **기각** |
|
||||
|
||||
가설 B가 기각되는 근거: 이 파일의 최신 `발급일자`가 **2026-09-01** 이고, 조사 문서가 기록한 웹 화면의 최신 `최초등록일자`도 **2026-09-01** 로 동일하다. API 가 뒤처져 있지 않다. 하루치 지연으로는 756건을 설명할 수 없다.
|
||||
|
||||
또한 웹 화면의 `취소/취하구분` 컬럼 실측값이 `정상`이었다는 사실은, 웹 테이블이 취하 건을 **삭제하지 않고 상태 컬럼으로 표시**한다는 뜻이다. 두 사실을 합치면 756건은 취하·취소 건일 가능성이 높다.
|
||||
|
||||
### 이것이 의미하는 것
|
||||
|
||||
**API 응답에서 등록번호가 사라지면 취하로 판정할 수 있다.** 이는 이 프로젝트 변경 탐지 설계의 핵심 전제이며, 이 분석으로 강하게 뒷받침된다.
|
||||
|
||||
> ⚠️ **아직 증명은 아니다.** 이틀 이상 연속 수집해 실제로 사라지는 레코드를 관찰해야 확정된다. 그때까지는 `design/00-DATA-SOURCE-DECISION.md` §7.2 의 안전장치(건수 급감 시 diff 중단)를 반드시 유지한다.
|
||||
|
||||
---
|
||||
|
||||
## 5. 등록번호 분석
|
||||
|
||||
### 5.1 유일성
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| 전체 행 | 9,084 |
|
||||
| 고유 등록번호 | **9,084** |
|
||||
| 중복 | **0** |
|
||||
|
||||
**결론: 등록번호는 완전한 자연 키다.** 복합 키나 해시 대체 키가 필요 없다. SQLite 의 `PRIMARY KEY` 로 그대로 쓴다.
|
||||
|
||||
### 5.2 포맷 분포
|
||||
|
||||
| 포맷 | 건수 | 비율 | 예시 |
|
||||
|---|---|---|---|
|
||||
| 표준 `YYYYMMDD-n-A-n-n` | 3,774 | 41.5% | `20260901-86-D-173-26`, `20260831-209-J-2250` |
|
||||
| 표준 + 괄호 | 2,973 | 32.7% | `20230116-200-I-647-07(A)`, `20250219-32-C-423-28(1)` |
|
||||
| 신물질 `수nnnn-n-ND` | 1,882 | 20.7% | `수6580-16-ND(20)`, `수6256-1-ND`, `수582-34-ND(A)` |
|
||||
| 기타 | 455 | 5.0% | `1962-17-ND`, `20100616-122-G-60-22(1)-A(1)`, `20150918-135-H-305-43(2)-A(A)` |
|
||||
|
||||
**설계 시사점**: 파서는 **4종 이상의 변형을 관대하게 처리**해야 한다. 특히 "기타" 455건에는 `수` 접두어 없는 `1962-17-ND` 형태와 이중 괄호 `(1)-A(1)` 형태가 섞여 있다.
|
||||
|
||||
**파싱 실패는 치명적이지 않게 설계한다.** 등록번호는 문자열 그대로가 키이므로, 파싱은 파생 정보(최초 등록일, 변경 차수)를 얻기 위한 부가 작업이다. 실패해도 레코드는 정상 처리하고 파생 필드만 null 로 둔다.
|
||||
|
||||
### 5.3 괄호의 의미
|
||||
|
||||
괄호가 붙은 건은 **표준+괄호 2,973건 + 신물질 일부 + 기타 일부**로 전체의 약 3분의 1이다. 다음 절의 분석이 괄호의 의미를 밝힌다.
|
||||
|
||||
---
|
||||
|
||||
## 6. 변경 탐지 가능성 — 발급일자의 의미
|
||||
|
||||
### 6.1 실측
|
||||
|
||||
표준 포맷 등록번호 6,768건에 대해, 앞 8자리(`YYYYMMDD`)와 `발급일자`를 비교했다.
|
||||
|
||||
| 항목 | 건수 | 비율 |
|
||||
|---|---|---|
|
||||
| 앞 8자리 == 발급일자 | 3,753 | 55.5% |
|
||||
| **앞 8자리 ≠ 발급일자** | **3,015** | **44.5%** |
|
||||
|
||||
불일치 사례:
|
||||
|
||||
| 등록번호 | 앞 8자리(최초 등록) | 발급일자(최종) | 간격 |
|
||||
|---|---|---|---|
|
||||
| `20230116-200-I-647-07(A)` | 2023-01-16 | 2026-08-28 | 3년 7개월 |
|
||||
| `20180814-209-J-127(A)` | 2018-08-14 | 2026-08-25 | 8년 |
|
||||
| `20131031-84-D-126-07(A)` | 2013-10-31 | 2026-08-25 | 12년 10개월 |
|
||||
| `20050831-33-A-81-08(18)` | 2005-08-31 | 2026-08-18 | 21년 |
|
||||
| `20250219-32-C-423-28(1)` | 2025-02-19 | 2026-08-21 | 1년 6개월 |
|
||||
| `20210721-209-J-1073(6)` | 2021-07-21 | 2026-08-18 | 5년 1개월 |
|
||||
|
||||
### 6.2 해석
|
||||
|
||||
불일치 건은 **전부 괄호가 붙어 있다.** 따라서 다음 구조가 성립한다.
|
||||
|
||||
| 요소 | 의미 |
|
||||
|---|---|
|
||||
| 등록번호 앞 8자리 | **최초 등록일** (고정) |
|
||||
| 괄호 안의 값 `(A)`, `(1)`, `(18)` | **변경/갱신 표식** |
|
||||
| `발급일자` (`DMF_PERMIT_DATE`) | **최종 갱신일** (움직인다) |
|
||||
|
||||
**즉 `발급일자`는 정적인 최초 등록일이 아니라 변경 때마다 갱신되는 동적 필드다.**
|
||||
|
||||
### 6.3 이것이 의미하는 것
|
||||
|
||||
**API 7필드만으로 신규·변경·취하 세 가지를 모두 탐지할 수 있다.**
|
||||
|
||||
| 판정 | 규칙 | 근거 |
|
||||
|---|---|---|
|
||||
| **신규** | 오늘 등록번호가 있고 어제 없음 | 등록번호가 유일 키 (5.1) |
|
||||
| **변경** | 등록번호 동일 + `발급일자`가 어제보다 최신 | 발급일자가 최종 갱신일로 동작 (6.2) |
|
||||
| **변경(보조)** | 등록번호 동일 + 성분명·업체명·제조소명·소재지·국가 중 하나가 다름 | 내용 변경 |
|
||||
| **취하** | 어제 등록번호가 있고 오늘 없음 | API 가 정상 건만 반환 (4절) |
|
||||
|
||||
이로써 조사 문서 `research/01` 이 제기한 "API 단독으로는 변경·취하 탐지 요구를 충족할 수 없다"는 주장은 **반증된다.** 그 주장은 웹 화면의 `최종변경일자`·`취소/취하구분` 컬럼이 있어야만 탐지가 가능하다는 전제에 서 있었으나, 실제로는 `발급일자`의 이동과 레코드 소멸이 같은 정보를 담고 있다.
|
||||
|
||||
### 6.4 여전히 놓치는 것
|
||||
|
||||
정직하게 기록한다. API 로 탐지되지 않는 이벤트가 있다.
|
||||
|
||||
| 놓치는 것 | 이유 | 영향 | 대응 |
|
||||
|---|---|---|---|
|
||||
| **연차보고** | `최종연차보고년도` 컬럼이 API 에 없고, 연차보고 시 `발급일자`가 갱신되는지 불명 | 중간. 연차보고는 매년 1월 말에 몰린다 | 리포트에서 별도 이벤트로 다루지 않음. 확인 필요 |
|
||||
| **변경 사유·유형** | 어떤 항목이 왜 바뀌었는지 API 에 없음 | 낮음 | 리포트 각 행에 웹 화면 검색 링크를 붙여 사람이 확인 |
|
||||
| **취하 vs 취소 구분** | 소멸했다는 사실만 알고 사유를 모름 | 낮음 | "목록에서 사라짐"으로 표기 |
|
||||
| **취하 일자** | 소멸을 감지한 날짜만 앎 | 낮음 | 감지일로 기록 |
|
||||
| **대상의약품 구분** (`별표1`/`신물질`) | API 에 없음 | 낮음 | **등록번호 포맷으로 추론 가능** (`수` 접두어 = 신물질) |
|
||||
|
||||
`대상의약품`은 등록번호 포맷에서 파생할 수 있으므로 실질 손실이 아니다. 실질적으로 아쉬운 것은 연차보고 하나다.
|
||||
|
||||
---
|
||||
|
||||
## 7. 데이터 규모와 분포
|
||||
|
||||
### 7.1 기본 통계
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| 전체 레코드 | 9,084 |
|
||||
| 고유 성분명 | 1,539 |
|
||||
| 고유 업체명 | 438 |
|
||||
| 고유 제조소명 | 3,051 |
|
||||
| 고유 제조소소재지 | 4,270 |
|
||||
| 고유 제조국가명(원문) | 225 |
|
||||
| 고유 국가(콤마 분해 후) | **49** |
|
||||
| 고유 발급일자 | 2,397 |
|
||||
| 발급일자 범위 | 2003-04-18 ~ 2026-09-01 |
|
||||
| 결측치 | 제조국가명 5건. 나머지 컬럼 0건 |
|
||||
|
||||
### 7.2 연도별 등록 건수 (최근 12년)
|
||||
|
||||
| 연도 | 건수 |
|
||||
|---|---|
|
||||
| 2015 | 278 |
|
||||
| 2016 | 246 |
|
||||
| 2017 | 284 |
|
||||
| 2018 | 473 |
|
||||
| 2019 | 502 |
|
||||
| 2020 | 668 |
|
||||
| 2021 | 939 |
|
||||
| 2022 | 633 |
|
||||
| 2023 | 463 |
|
||||
| 2024 | 534 |
|
||||
| 2025 | **1,185** |
|
||||
| 2026 (9월 2일까지) | 610 |
|
||||
|
||||
> 주의: 이 집계는 `발급일자` 기준이므로 **최초 등록이 아니라 최종 갱신 연도**다. 2025년이 급증한 것은 최근 갱신된 건이 많다는 뜻이지 신규 등록이 폭증했다는 뜻이 아니다. **리포트에서 이 구분을 명확히 표기해야 사용자가 오해하지 않는다.**
|
||||
|
||||
### 7.3 제조국가 분포 (원문 기준 상위 15)
|
||||
|
||||
| 국가 | 건수 |
|
||||
|---|---|
|
||||
| 인도 | 3,351 |
|
||||
| 중국 | 2,231 |
|
||||
| 대한민국 | 845 |
|
||||
| 이탈리아 | 313 |
|
||||
| 스페인 | 210 |
|
||||
| 대한민국,중국 | 156 |
|
||||
| 일본 | 149 |
|
||||
| 대만 | 121 |
|
||||
| 독일 | 117 |
|
||||
| 프랑스 | 88 |
|
||||
| 미국 | 83 |
|
||||
| 스위스 | 81 |
|
||||
| 아일랜드 | 77 |
|
||||
| 중국,중국 | 76 |
|
||||
| 인도,인도 | 74 |
|
||||
|
||||
인도와 중국이 전체의 **61.5%** 를 차지한다. 원료의약품 공급망이 이 두 나라에 집중돼 있다는 사실이 데이터로 확인된다. **리포트 대시보드의 핵심 지표가 될 만하다.**
|
||||
|
||||
### 7.4 업체 분포 (상위 15)
|
||||
|
||||
| 업체명 | 건수 |
|
||||
|---|---|
|
||||
| (주)삼오제약 | 392 |
|
||||
| (주)파마피아 | 378 |
|
||||
| 에이징생명과학(주) | 277 |
|
||||
| (주)국전 | 271 |
|
||||
| 에이스바이오팜주식회사 | 248 |
|
||||
| 대신무약(주) | 215 |
|
||||
| 화일약품(주) | 188 |
|
||||
| (주)마성엘에스 | 176 |
|
||||
| 성우화학(주) | 155 |
|
||||
| (주)성진엑심 | 153 |
|
||||
| ㈜하이플 | 147 |
|
||||
| (주)휴시드 | 124 |
|
||||
| 이성인터내쇼날(주) | 117 |
|
||||
| 성이바이오(주) | 117 |
|
||||
| 주식회사토루 | 116 |
|
||||
|
||||
### 7.5 성분 분포 (상위 15)
|
||||
|
||||
| 성분명 | 건수 |
|
||||
|---|---|
|
||||
| 히알루론산나트륨 | 104 |
|
||||
| 메트포르민염산염 | 97 |
|
||||
| 로수바스타틴칼슘 | 95 |
|
||||
| 아세트아미노펜 | 92 |
|
||||
| 세레콕시브 | 73 |
|
||||
| 발사르탄 | 73 |
|
||||
| 암로디핀베실산염 | 72 |
|
||||
| 레바미피드 | 69 |
|
||||
| 덱시부프로펜 | 67 |
|
||||
| 에제티미브 | 67 |
|
||||
| 트라마돌염산염 | 66 |
|
||||
| 프레가발린 | 61 |
|
||||
| 엠파글리플로진 | 61 |
|
||||
| 세파클러수화물 | 58 |
|
||||
| 아토르바스타틴칼슘 | 55 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 데이터 품질 문제
|
||||
|
||||
정규화 계층에서 반드시 처리해야 할 실제 문제들이다. 모두 실측으로 확인했다.
|
||||
|
||||
### 8.1 제조국가명 — 같은 국가 반복 표기 (496건)
|
||||
|
||||
원본에 같은 국가가 콤마로 반복된다.
|
||||
|
||||
| 원문 | 건수 |
|
||||
|---|---|
|
||||
| `중국,중국` | 76 |
|
||||
| `인도,인도` | 74 |
|
||||
| `대한민국,대한민국` | 56 |
|
||||
| `이탈리아,이탈리아` | 32 |
|
||||
| `대한민국,중국,중국` | 30 |
|
||||
| `중국,중국,중국` | 23 |
|
||||
| `인도,인도,인도` | 22 |
|
||||
| `독일,독일` | 19 |
|
||||
| `스위스,스위스` | 12 |
|
||||
| `일본,일본` | 10 |
|
||||
| `스페인,스페인` | 10 |
|
||||
| `이탈리아,중국,중국,중국` | 8 |
|
||||
|
||||
**총 496건.** 제조소가 여럿이고 같은 국가에 있을 때 국가명이 그만큼 반복되는 것으로 보인다.
|
||||
|
||||
**정규화 규칙**: 콤마로 분해 → 공백 제거 → 중복 제거 → 정렬 → 재결합. 원문은 별도 컬럼에 보존한다.
|
||||
|
||||
정규화 후 고유 국가는 **49개**다.
|
||||
|
||||
```
|
||||
남아프리카 공화국, 네덜란드, 노르웨이, 뉴질랜드, 대만, 대한민국, 덴마크, 독일, 라트비아,
|
||||
루마니아, 말레이지아, 멕시코, 몰타, 미국, 바하마, 벨기에, 불가리아, 브라질, 스웨덴,
|
||||
스위스, 스페인, 슬로바키아, 슬로베니아, 싱가포르, 아르헨티나, 아일랜드, 영국, 오만,
|
||||
오스트리아, 우크라이나, 이란, 이스라엘, 이탈리아, 인도, 인도네시아, 일본, 중국,
|
||||
체코공화국, 캐나다, 크로아티아, 태국, 튀르키예, 포르투갈, 폴란드, 푸에르토리코,
|
||||
프랑스, 핀란드, 헝가리, 호주
|
||||
```
|
||||
|
||||
> 표기 특이점: `말레이지아`(표준 표기는 말레이시아), `체코공화국`(체코) 처럼 비표준 표기가 섞여 있다. 국가 코드 매핑 테이블을 두면 지도 시각화나 그룹화에 유리하다.
|
||||
|
||||
### 8.2 성분명 표기 흔들림 (21그룹)
|
||||
|
||||
공백·구분자만 다른 같은 성분이 별개 값으로 존재한다.
|
||||
|
||||
| 흔들리는 표기 |
|
||||
|---|
|
||||
| `다비가트란 에텍실레이트 메실산염` / `다비가트란에텍실레이트메실산염` |
|
||||
| `DL-메틸에페드린염산염` / `dl-메틸에페드린염산염` |
|
||||
| `L-아스파르트산-L-오르니틴` / `L-아스파르트산·L-오르니틴` |
|
||||
| `로베글리타존 황산염` / `로베글리타존황산염` |
|
||||
| `항독성간장 엑스` / `항독성간장엑스` |
|
||||
| `오메가-3-산에틸에스테르90` / `오메가3산에틸에스테르90` |
|
||||
| `싸이모신 알파 1` / `싸이모신-알파1` / `싸이모신알파1` |
|
||||
| `은행엽 건조엑스` / `은행엽건조엑스` |
|
||||
| `덱스클로르페니라민 말레산염` / `덱스클로르페니라민말레산염` |
|
||||
| `톨밥탄 분무건조분말` / `톨밥탄분무건조분말` |
|
||||
|
||||
**총 21그룹.** 1,539개 성분 중 21그룹이므로 비율은 낮지만, **성분별 집계 시트에서 같은 성분이 두 줄로 갈라져 보이는 문제**를 만든다.
|
||||
|
||||
**정규화 규칙**: 공백·중점(`·`)·하이픈·괄호를 제거하고 소문자화한 값을 그룹 키로 삼는다. 표시는 원문 중 최빈값을 대표로 쓴다. **원문은 반드시 보존한다.**
|
||||
|
||||
### 8.3 업체명 표기 흔들림 (2그룹)
|
||||
|
||||
| 흔들리는 표기 |
|
||||
|---|
|
||||
| `삼진제약(주)` / `삼진제약주식회사` |
|
||||
| `(주)유일팜테크` / `주식회사 유일팜테크` |
|
||||
|
||||
438개 업체 중 2그룹으로 매우 적다. `㈜` → `(주)`, `주식회사` → `(주)`, 공백 제거로 해소된다.
|
||||
|
||||
### 8.4 제조소명 형식 오류 (42건)
|
||||
|
||||
원본에 연속 공백이나 마침표 중복이 들어 있다.
|
||||
|
||||
| 예시 |
|
||||
|---|
|
||||
| `BDR LIFESCIENCES PVT. LTD..` (마침표 2개) |
|
||||
| `Nanjing King-Friend Biochemical Pharmaceutical Co., Ltd.` (연속 공백) |
|
||||
| `North China Pharmaceutical Group Semisyntech Co., Ltd` (연속 공백) |
|
||||
|
||||
또한 제조소가 여러 곳인 경우 콤마로 이어붙어 있고, `[출발물질제조소]` 같은 **역할 표시가 문자열 안에 섞여** 있다.
|
||||
|
||||
```
|
||||
Sichuan Renan Pharmaceutical Co, Ltd,[출발물질제조소]North China Pharmaceutical Group Semisyntech Co., Ltd
|
||||
```
|
||||
|
||||
**주의**: 제조소명 자체에 콤마가 포함되므로(`Co., Ltd.`) **콤마로 단순 분할하면 안 된다.** 다중 제조소 분해는 신뢰할 수 없으니, 정규화는 연속 공백 압축과 양끝 정리에 그친다.
|
||||
|
||||
### 8.5 발급일자 형식
|
||||
|
||||
**9,084건 전부 `YYYY-MM-DD` 형식으로 일관**된다. 파싱 실패 0건. 날짜 처리는 안전하다.
|
||||
|
||||
### 8.6 제조소소재지 길이
|
||||
|
||||
| 통계 | 값 |
|
||||
|---|---|
|
||||
| 평균 | 81자 |
|
||||
| 중앙값 | 78자 |
|
||||
| 75분위 | 107자 |
|
||||
| **최대** | **473자** |
|
||||
|
||||
**xlsx 설계 시사점**: 이 컬럼을 그대로 표시하면 열 너비가 파괴된다(기존 파일의 D열 너비가 255.6으로 최대치에 붙어 있는 이유다). **줄바꿈 + 행 높이 고정 + 열 너비 상한**을 적용하거나, 목록 시트에서는 앞 60자만 보이고 전체는 셀 메모나 상세 시트에서 보게 해야 한다.
|
||||
|
||||
---
|
||||
|
||||
## 9. 기존 서식 상태와 개선 여지
|
||||
|
||||
`전체` 시트의 서식 실측 결과다.
|
||||
|
||||
| 항목 | 현재 상태 |
|
||||
|---|---|
|
||||
| 틀 고정 (freeze_panes) | **없음** |
|
||||
| 자동 필터 | **없음** |
|
||||
| 조건부 서식 | **0개 규칙** |
|
||||
| 병합 셀 | 0 |
|
||||
| 차트 | **0** |
|
||||
| 이미지 | 0 |
|
||||
| Excel 표(ListObject) | **없음** |
|
||||
| 열 너비 지정 | A 30.6 / B 85.6 / C 33.1 / **D 255.6** / E~H 미지정 |
|
||||
| 시트 탭 색상 | **없음** |
|
||||
|
||||
**진단**: 데이터는 정확한데 **읽기 도구로서의 설계가 전혀 없다.** 9,084행을 헤더 고정 없이 스크롤해야 하고, 필터가 없어 특정 성분·업체를 찾을 수 없으며, 무엇이 새 건인지 색으로 구분되지 않는다. D열 너비 255.6은 소재지 473자를 담으려다 최대치에 닿은 것으로, 화면이 가로로 파괴된다.
|
||||
|
||||
**"디자인 예쁘게, 한눈에" 요구가 겨냥하는 지점이 정확히 여기다.** 개선 방향은 `design/03-xlsx-report-spec.md` 에서 확정하되, 이 분석에서 나오는 필수 항목은 다음과 같다.
|
||||
|
||||
- 헤더 행 고정과 자동 필터 (9,084행을 다루려면 필수)
|
||||
- 열 너비 상한과 소재지 줄바꿈 처리
|
||||
- 신규·변경·취하 상태에 따른 행 색상 구분
|
||||
- 대시보드 시트 신설: 오늘 요약, 국가 분포(인도·중국 61.5% 집중), 상위 성분·업체, 최근 추이
|
||||
- 성분별·업체별·국가별 집계 시트 (정규화된 값 기준)
|
||||
- 시트 탭 색상과 목차 하이퍼링크
|
||||
|
||||
---
|
||||
|
||||
## 10. 확정되는 설계 결정
|
||||
|
||||
이 분석으로 확정 또는 강하게 뒷받침되는 결정들이다.
|
||||
|
||||
| # | 결정 | 근거 |
|
||||
|---|---|---|
|
||||
| D1 | **등록번호를 기본 키로 쓴다.** 대체 키 불필요 | 9,084건 중복 0 (5.1) |
|
||||
| D2 | **취하는 "API 응답에서 소멸"로 판정한다** | 웹 9,840 vs API 9,084, 756건 차이 (4절) |
|
||||
| D3 | **변경은 `발급일자` 이동 + 6개 필드 diff 로 판정한다** | 발급일자가 최종 갱신일로 동작, 44.5% 불일치 (6절) |
|
||||
| D4 | **`대상의약품`(별표1/신물질)은 등록번호 포맷에서 파생한다** | `수` 접두어 1,882건 식별 (5.2) |
|
||||
| D5 | **정규화 계층을 반드시 둔다.** 국가·성분명·업체명·제조소명 4종 | 품질 문제 실측 (8절) |
|
||||
| D6 | **원문을 반드시 보존한다.** 정규화 값은 별도 컬럼 | 되돌릴 수 없는 손실 방지 |
|
||||
| D7 | **등록번호 파싱 실패를 허용한다.** 파생 필드만 null | 포맷 4종 이상, 기타 455건 (5.2) |
|
||||
| D8 | **기존 3시트 구조(전체/신규/갱신이력)를 계승·확장한다** | 프로토타입 설계 의도 존중 (2절) |
|
||||
| D9 | **`최초수집일` 파생 컬럼을 유지하고 형제 컬럼을 추가한다** | 기존 설계가 이미 옳았음 (3절) |
|
||||
| D10 | **소재지 컬럼은 표시 폭을 제한한다** | 최대 473자 (8.6) |
|
||||
| D11 | **연도별 집계는 "갱신 연도"임을 명시한다** | 발급일자가 최종 갱신일 (7.2) |
|
||||
| D12 | **국가 코드 매핑 테이블을 둔다** | 비표준 표기 존재, 49개국 (8.1) |
|
||||
|
||||
### 이 분석이 해소한 기존 미해결 항목
|
||||
|
||||
`design/00-DATA-SOURCE-DECISION.md` 부록 B 의 항목들이 다음과 같이 해소됐다.
|
||||
|
||||
| 기존 미해결 항목 | 해소 상태 |
|
||||
|---|---|
|
||||
| 전체 DMF 등록 건수(`totalCount`)는? | ✅ **9,084건** (2026-09-02) |
|
||||
| 응답에 `DMF_PERMIT_NO` 중복이 존재하는가? | ✅ **중복 0** |
|
||||
| API 에 취하·말소된 등록번호가 남아 있는가? | 🟡 **남지 않을 가능성 높음** (756건 차이). 연속 관측으로 확정 필요 |
|
||||
| `numOfRows` 실제 최대값 | ❌ 미해소 |
|
||||
| `type=json` 실동작 여부 | 🟡 프로토타입이 수집에 성공했으므로 API 자체는 동작 확인 |
|
||||
|
||||
---
|
||||
|
||||
## 부록 A. 재현 스크립트
|
||||
|
||||
이 분석을 재현하는 스크립트다. 데이터가 갱신되면 다시 돌려 비교한다.
|
||||
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""DMF_현황.xlsx 기준선 분석 재현 스크립트"""
|
||||
import re
|
||||
import collections
|
||||
import pandas as pd
|
||||
import openpyxl
|
||||
|
||||
XLSX = r"C:\Users\encep\OneDrive\문서\카카오톡 받은 파일\DMF_현황.xlsx"
|
||||
|
||||
|
||||
def inspect_structure(path: str) -> None:
|
||||
"""시트 구조와 서식 상태를 출력한다."""
|
||||
wb = openpyxl.load_workbook(path, data_only=True)
|
||||
for ws in wb.worksheets:
|
||||
rules = sum(len(r.rules) for r in ws.conditional_formatting)
|
||||
print(f"[{ws.title}] rows={ws.max_row} cols={ws.max_column} "
|
||||
f"freeze={ws.freeze_panes} filter={ws.auto_filter.ref} "
|
||||
f"cf_rules={rules} charts={len(ws._charts)}")
|
||||
|
||||
|
||||
def normalize_countries(value: str) -> str:
|
||||
"""콤마 다중값에서 중복을 제거하고 정렬해 재결합한다."""
|
||||
if not isinstance(value, str) or not value.strip():
|
||||
return ""
|
||||
parts = [p.strip() for p in value.split(",") if p.strip()]
|
||||
return ",".join(sorted(set(parts)))
|
||||
|
||||
|
||||
def normalize_ingredient(value: str) -> str:
|
||||
"""성분명 그룹 키. 공백·중점·하이픈·괄호를 제거한다."""
|
||||
return re.sub(r"[\s\u00a0()()·・\-–—,]", "", str(value)).lower()
|
||||
|
||||
|
||||
def normalize_entity(value: str) -> str:
|
||||
"""업체명 그룹 키. 법인 표기를 통일한다."""
|
||||
s = str(value).replace("㈜", "(주)")
|
||||
s = re.sub(r"주식회사", "(주)", s)
|
||||
return re.sub(r"[\s\u00a0().]", "", s).lower()
|
||||
|
||||
|
||||
def classify_permit_no(value: str) -> str:
|
||||
"""등록번호 포맷을 4종으로 분류한다."""
|
||||
if re.match(r"^\d{8}-\d+-[A-Z]+-\d+(-\d+)?$", value):
|
||||
return "std"
|
||||
if re.match(r"^\d{8}-\d+-[A-Z]+-\d+(-\d+)?\([^)]*\)$", value):
|
||||
return "std_paren"
|
||||
if re.match(r"^수\d+-\d+-[A-Z]+", value):
|
||||
return "new_substance"
|
||||
return "other"
|
||||
|
||||
|
||||
def analyze(path: str) -> None:
|
||||
df = pd.read_excel(path, sheet_name="전체", dtype=str)
|
||||
print(f"행수={len(df)} 고유등록번호={df['등록번호'].nunique()}")
|
||||
|
||||
issued = pd.to_datetime(df["발급일자"], errors="coerce")
|
||||
print(f"발급일자 {issued.min().date()} ~ {issued.max().date()}")
|
||||
|
||||
# 등록번호 앞 8자리 vs 발급일자
|
||||
std = df[df["등록번호"].str.match(r"^\d{8}-")]
|
||||
head = pd.to_datetime(std["등록번호"].str[:8], format="%Y%m%d", errors="coerce")
|
||||
same = (head == pd.to_datetime(std["발급일자"], errors="coerce")).sum()
|
||||
print(f"std {len(std)}건 중 앞8자리==발급일자 {same}건 ({same / len(std) * 100:.1f}%)")
|
||||
|
||||
# 국가 중복 표기
|
||||
redundant = df["제조국가명"].fillna("").apply(
|
||||
lambda v: "," in v and len({p.strip() for p in v.split(",")}) != len(v.split(","))
|
||||
)
|
||||
print(f"국가 중복 표기 {redundant.sum()}건")
|
||||
|
||||
# 표기 흔들림
|
||||
for name, fn, col in (
|
||||
("성분명", normalize_ingredient, "성분명"),
|
||||
("업체명", normalize_entity, "업체명"),
|
||||
):
|
||||
groups = collections.defaultdict(set)
|
||||
for v in df[col].fillna(""):
|
||||
groups[fn(v)].add(v)
|
||||
collisions = {k: v for k, v in groups.items() if len(v) > 1}
|
||||
print(f"{name} 표기 흔들림 그룹 {len(collisions)}개")
|
||||
|
||||
# 포맷 분포
|
||||
counts = collections.Counter(classify_permit_no(v) for v in df["등록번호"].fillna(""))
|
||||
print("등록번호 포맷:", dict(counts))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
inspect_structure(XLSX)
|
||||
analyze(XLSX)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 부록 B. 미해결 / 확인 필요
|
||||
|
||||
**사용자에게 확인해야 할 것**
|
||||
|
||||
- [ ] 이 xlsx 를 만든 **수집 스크립트가 어디에 있는가?** 있다면 재사용·개선의 출발점이 된다.
|
||||
- [ ] **`serviceKey` 를 이미 발급받았는가?** 프로토타입이 API 수집에 성공했으므로 키가 존재할 것이다. 어디에 저장돼 있는가.
|
||||
- [ ] 이 파일이 카카오톡으로 전달된 경위 — 본인이 만든 것인가, 다른 사람이 만든 것인가.
|
||||
- [ ] `신규` 시트가 비어 있는데, 기대하는 동작이 "당일 신규만" 인가 "최근 N일" 인가.
|
||||
- [ ] `갱신이력` 시트에 추가로 기록하고 싶은 항목이 있는가 (변경건수, 취하건수, 소요시간, 오류).
|
||||
|
||||
**데이터로 확인해야 할 것**
|
||||
|
||||
- [ ] **API 응답 건수 9,084 가 실제로 정상 건만인지** 연속 2일 이상 수집해 소멸 레코드를 관찰
|
||||
- [ ] 웹 9,840 − API 9,084 = 756건이 정말 취하·취소 건인지 표본 대조
|
||||
- [ ] 연차보고가 일어날 때 `발급일자`가 갱신되는가 (1~2월에 관측)
|
||||
- [ ] 변경 시 등록번호의 괄호 부분이 실제로 바뀌는가, 아니면 번호는 그대로이고 발급일자만 바뀌는가
|
||||
- [ ] `numOfRows` 최대값과 전량 수집에 필요한 호출 횟수
|
||||
- [ ] 제조국가명 결측 5건의 등록번호와 원인
|
||||
- [ ] "기타" 포맷 455건의 하위 패턴 분류
|
||||
|
||||
**설계에 반영해야 할 것**
|
||||
|
||||
- [ ] `research/01` 의 "API 로는 변경·취하 탐지 불가" 결론을 이 분석 결과로 정정
|
||||
- [ ] `research/07` 의 시트 컬럼 스펙을 API 7필드 + 파생 필드 기준으로 재작성
|
||||
- [ ] 국가 코드 매핑 테이블 작성 (49개국, 비표준 표기 포함)
|
||||
|
||||
---
|
||||
|
||||
*이 문서는 기준선 사실의 정본이다. 데이터가 갱신되면 부록 A 스크립트로 재분석하고 수치를 갱신한다.*
|
||||
Loading…
Add table
Add a link
Reference in a new issue