- 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 문서 지도 갱신
2562 lines
197 KiB
Markdown
2562 lines
197 KiB
Markdown
# 의약 정보 엑셀 대시보드 시각화 설계
|
||
|
||
> ## ⚠️ 문서 상태 — 정정 반영 (2026-09-02)
|
||
>
|
||
> 이 문서는 `design/00b-baseline-data-analysis.md`(기존 운영 산출물 `DMF_현황.xlsx` 전수 실측) **이전에 작성됐다.** 작성 당시의 핵심 전제 하나가 그 실측으로 반증되어 아래와 같이 정정한다.
|
||
>
|
||
> **반증된 전제**: "OpenAPI 는 7필드뿐이라 diff 판정에 부족하다 → nedrug 웹 화면(`CCBAC03`) 스크래핑이 주 소스여야 한다"(구 §5.7 주석). 반증 근거 3가지(`00b` 실측):
|
||
> - API 전체 **9,084건** vs 웹 화면 **9,840건** = **756건 차이** → API 는 취하 건을 제외하고 정상 건만 반환한다. 레코드 소멸 = 취하로 판정할 수 있다.
|
||
> - 등록번호 앞 8자리(최초 등록일)와 `발급일자`가 **44.5% 불일치**하며, 불일치 건은 전부 괄호 붙은 갱신 건이다 → `발급일자`는 최종 갱신일로 동작한다. 변경의 직접 신호.
|
||
> - 등록번호 9,084건 중 **중복 0건** → 완전한 자연 키. diff 의 기준 키로 그대로 쓸 수 있다.
|
||
>
|
||
> **여전히 유효한 것**: 대시보드 설계 원칙(§2~4), 색 접근성 근거(§6), 규제 인텔리전스(RA) 실무 관점(§4, §12), 시각화 형식 선택(§2.7, §10), 레이아웃 아이디어(§9). 이 부분은 그대로 참고한다.
|
||
>
|
||
> **정정된 것**: §8 의 시트 컬럼 스펙. API 미제공 컬럼(`최종변경일자`, `취소/취하구분`, `최종연차보고년도`, `문서번호` 등)을 원본 컬럼으로 전제했던 부분을 **API 7필드 + 파생 필드** 기준으로 다시 썼다(신설 §8.0, 재작성 §8.3·§8.4).
|
||
>
|
||
> **정본 위치**: 최종 xlsx 스펙의 정본(SSOT)은 `design/03-xlsx-report-spec.md` 다. **이 문서는 그 설계의 근거 자료(research)이지 정본이 아니다.** 본문에 남아 있는 "확정"·"최종"이라는 표현은 모두 "제안"·"권고"로 읽는다. 문서 끝 "이 문서와 확정 설계의 관계" 절에 절별 상태를 정리했다.
|
||
|
||
> **이 문서의 역할**: DMF_Crawler 가 매일 06:00 에 생성하는 xlsx 리포트의 시트 구성·컬럼·서식·색·레이아웃을 설계하기 위한 조사·근거 자료. 최종 확정은 `design/03-xlsx-report-spec.md` 에서 이루어진다.
|
||
|
||
---
|
||
|
||
## 0. 한눈에 보기
|
||
|
||
- **대시보드는 "1화면·5초"가 기준이다.** Stephen Few 의 정의(원문): *"A dashboard is a visual display of the most important information needed to achieve one or more objectives; consolidated and arranged on a single screen so the information can be monitored at a glance."* → 스크롤·필터·범례 없이 5초 안에 "오늘 뭐가 터졌나"가 보여야 한다.
|
||
- **KPI 타일은 6개로 고정한다.** (신규 / 변경 / 취하 / 오늘 총 변경 / 워치리스트 히트 / 전체 누적). 권고치는 "핵심 3~4개로 시작, 최대 6~8개"이므로 6개는 상한 안쪽이다.
|
||
- **색은 3개 의미색 + 1개 강조색 + 무채색.** 상태색은 Okabe-Ito 색각이상 대응 팔레트에서만 뽑는다: 신규 `#009E73`, 변경 `#E69F00`, 취하 `#D55E00`, 강조/링크 `#0072B2`. **순수 red/green 조합은 절대 쓰지 않는다** (Color Universal Design 명시 금지 조합).
|
||
- **색 단독 전달 금지(WCAG 1.4.1).** 모든 상태는 `상태` 텍스트 컬럼 + 기호(`+ / ◆ / ✕`) + 배경색의 3중 코딩으로 표시한다. 본문 대비비는 전부 7:1 이상(AAA)으로 맞췄다(§6 표).
|
||
- **시트는 8개를 제안한다**(정본은 `design/03-xlsx-report-spec.md`): `대시보드` / `오늘 변경분` / `전체 누적` / `성분별` / `업체별` / `워치리스트` / `스냅샷·로그` / `메타`. 시트 간 이동은 `write_url('internal:...')` 로 대시보드 ↔ 각 시트 양방향 링크를 건다.
|
||
- **피벗테이블은 파이썬에서 못 만든다.** openpyxl 공식 문서: *"it is not intended that client code should be able to create pivot tables."* XlsxWriter 도 피벗 캐시 생성 API 가 없다. → **pandas 로 집계한 정적 표 + `add_table()` + `add_chart()`** 를 기본으로 하고, 진짜 피벗이 필요하면 "피벗이 미리 들어있는 템플릿 xlsx + `pivot.cache.refreshOnLoad = True`" 를 쓴다(§9).
|
||
- **한국 DMF 도메인 특유의 축은 5개**: 성분명 / 신청인(등록업체) / 제조소·제조국 / 등록구분(등록·변경등록·연차보고·사전등록) / 취소·취하. 데일리팜 기사가 실제로 쓰는 축이 "제조국별 비중"과 "연도별 건수"이므로 대시보드에도 이 두 축을 넣는다.
|
||
- **등록번호는 파싱 가능한 구조체다.** `20110531-71-B-317-05(1~9)` = 등록수리일자-별표1 성분 일련번호-시행일군 알파벳-접수순 일련번호-동일성분 일련번호(괄호=허여서 등록순). → 등록번호에서 **등록수리일자와 성분 코드를 파생 컬럼으로 뽑아** 타임라인·정렬에 쓴다(§5.3).
|
||
- **전일 대비 diff 는 "행 전체 강조 + 상태 컬럼"으로 구현**한다. XlsxWriter `conditional_format` 의 `type: 'formula'` 로 `=$L2="신규"` 처럼 **열은 절대·행은 상대** 참조를 써서 행 전체에 서식을 건다.
|
||
- **환경은 검증됨**: `xlsxwriter 3.2.1`, `openpyxl 3.1.5`, `pandas 2.2.3` 이 이미 설치되어 있다(실측). 본 문서의 코드는 이 버전 기준이다.
|
||
|
||
---
|
||
|
||
## 1. 목차
|
||
|
||
| 절 | 제목 |
|
||
|---|---|
|
||
| 0 | 한눈에 보기 |
|
||
| 1 | 목차 |
|
||
| 2 | 엑셀 대시보드 설계 원칙 (출처 포함) |
|
||
| 3 | KPI 타일(카드) 해부 |
|
||
| 4 | 규제 인텔리전스 실무의 DMF 추적 관점 |
|
||
| 5 | 한국 DMF 도메인 데이터 구조 (실측) |
|
||
| 6 | 색 접근성: 팔레트·상태 매핑·대비비 |
|
||
| 7 | 공개 데이터(FDA/EMA/식약처) 컬럼·시트 구성에서 배울 점 |
|
||
| 8 | 시트 구성 제안 (구현 명세, §8.0 정정 반영) |
|
||
| 9 | 대시보드 레이아웃 텍스트 와이어프레임 |
|
||
| 10 | 피벗테이블 문제와 대안 |
|
||
| 11 | 구현 레시피: 동작하는 코드 |
|
||
| 12 | 한국 제약 실무자가 실제로 보고 싶어하는 것 |
|
||
| 부록 A | 출처 목록 (전체 URL) |
|
||
| 부록 B | 미해결 질문 / 실측 필요 항목 |
|
||
|
||
---
|
||
|
||
## 2. 엑셀 대시보드 설계 원칙 (출처 포함)
|
||
|
||
### 2.1 대시보드의 정의 — Stephen Few
|
||
|
||
Perceptual Edge 의 *Dashboard Confusion*(Stephen Few, 2004-03-20) PDF 를 직접 열어 본문에서 추출한 정의(원문 그대로):
|
||
|
||
> "A dashboard is a visual display of the most important information needed to achieve one or more objectives; consolidated and arranged on a single screen so the information can be monitored at a glance."
|
||
|
||
같은 문서의 부수 원칙:
|
||
|
||
- 대시보드의 의미는 **정보의 종류(KPI 여부)가 아니라 정보를 배치하는 방식**에서 나온다.
|
||
- *"Display mechanisms that clearly state their message without taking up much space are required so the entire collection of information will fit into the limited real estate of a single screen."* → **한 화면에 들어가게 하려면 표현 매체가 공간을 적게 먹어야 한다.** 스파크라인·데이터바·아이콘셋이 정당화되는 근거.
|
||
- 연료계·신호등·온도계 같은 게이지는 *적절할 때만* 쓴다. 게이지를 고집하는 것은 잘못이다.
|
||
|
||
> **DMF 리포트에 적용**: `대시보드` 시트는 세로 스크롤 없이 한 화면에 끝나야 한다. 표는 Top 10 까지만, 전체 목록은 별도 시트로 보낸다.
|
||
|
||
### 2.2 5초 규칙 (5-second rule)
|
||
|
||
출처: [Den Otter Solutions — Dashboard design 5 seconds rule](https://denottersolutions.com/en/data-insights/dashboard-design-5-seconds-rule/) (WebFetch 확인)
|
||
|
||
> "A user should be able to see whether the organisation is on track within five seconds of opening a dashboard — without filtering, scrolling or reading a legend."
|
||
|
||
같은 문서가 준 구체 수치·배치 규칙:
|
||
|
||
| 항목 | 규칙 |
|
||
|---|---|
|
||
| 레이아웃 | **F-패턴**. 좌상단 = KPI Zone, 중앙 = Trend Zone(라인 차트), 우하단 = Detail Zone(표) |
|
||
| KPI 개수 | 대시보드당 **최대 6~8개**, 시작은 **3~4개** |
|
||
| KPI 크기 | 좌상단에 3~4개를 **큰 글자 + 적/녹 신호**로 |
|
||
| 색 | **기능적으로만 사용, 최대 3색, 각 색에 고정 의미** (red = action needed, green = on-track) |
|
||
| 차트 선택 | 정확한 비교에는 **파이보다 막대** |
|
||
| 여백 | 화이트스페이스가 처리 속도를 돕는다 |
|
||
| 맥락 | 숫자는 비교 기준(예산·전년·목표)이 있어야 의미를 가진다 |
|
||
|
||
DataCamp 튜토리얼도 같은 규칙을 반복한다: *"Apply the five-second rule — critical information should be extractable within five seconds"*, *"Use colorblind-friendly palettes, avoiding red-green combinations"*.
|
||
출처: [DataCamp — How to Create a Dashboard in Excel](https://www.datacamp.com/tutorial/create-excel-dashboards-tutorial) (WebFetch 확인)
|
||
|
||
DataHub Pro 도 동일: *"if a viewer cannot grasp the headline in about five seconds, the dashboard is trying to say too much"*, *"Dashboards must fit one screen without scrolling"*.
|
||
출처: [DataHub Pro — Excel Dashboard Templates](https://www.datahubpro.co.uk/tutorials/excel-dashboard-templates) (WebFetch 확인)
|
||
|
||
### 2.3 KPI 배치와 개수
|
||
|
||
- **가장 중요한 KPI 는 좌상단에 크게.** 근접성(proximity)으로 관련 차트·지표를 묶으면 뇌가 블록 단위로 빠르게 처리한다. 넉넉한 화이트스페이스가 "숨 쉴 공간"을 준다. (검색 #2 종합)
|
||
- **6~8개가 상한**: *"every additional KPI competes for attention and slows interpretation."*
|
||
- DataHub Pro: **3~6개 KPI 카드**로 리드하고, 각 KPI 는 **"총계 + 비교 + 변화율"의 3요소**를 가져야 한다. 기간 대비 계산식은 `=(ThisMonth-LastMonth)/LastMonth`, 백분율 서식.
|
||
- 시각 위계: KPI 카드는 상단 가로 배치, **가장 중요한 차트는 좌상단**(시선이 처음 닿는 곳), 보조 차트는 그 아래.
|
||
- 마감 처리: **눈금선(gridlines)과 행/열 머리글을 숨겨** 깨끗한 캔버스를 만들고, 그리드에 정렬, 여백을 넉넉히.
|
||
- Chandoo.org 참고: 원조 "Making a Simple KPI Dashboard using MS Excel" 글(2011-08-25)은 지금도 월 12,000명이 본다고 소개된다. (WebFetch 는 403 으로 차단 — ⚠️ 본문 미검증)
|
||
|
||
### 2.4 색 최소화 — 강조색 1개
|
||
|
||
검색 #5 가 모은 실무 규칙(여러 출처가 같은 결론):
|
||
|
||
> "Pick a neutral base and exactly one accent color, with the accent appearing only on the thing you want noticed — the key KPI, the alert, the call to action. The rule is simple: three to five working colors, and that is it. Neutrals are free. One accent does the pointing."
|
||
|
||
- 권장 구조: **회색 뉴트럴 1 + 브랜드 프라이머리 1(예: 네이비) + 경보 액센트 1(예: 코랄)** = 3색 팔레트.
|
||
- *"Every bright color you add costs you a little attention elsewhere. A neutral base, one accent for 'look here,' and semantic green/red used sparingly."*
|
||
- Chandoo.org 포럼 요지: **배경은 흰색이 최선**, 어두운 색은 피하고 밝은 톤으로. 색 구성은 **작업 시작 전에 확정**하고, 숫자 없는 손그림 목업으로 레이아웃부터 잡고, 페이지/표시 크기를 초기에 고정하라(나중에 바꾸기 어렵다). 셀 스타일을 써서 동일 정보는 동일 서식으로.
|
||
- GoSkills 의 12원칙(제목 원문):
|
||
1. "What story are you trying to tell?"
|
||
2. "Decide on the layout" — 중요 정보는 상단·좌측
|
||
3. "Think of your audience"
|
||
4. "Use the right visuals"
|
||
5. "Make it interactive" — 필터·정렬·내비게이션
|
||
6. "Remove the fluff"
|
||
7. "Add context" — 목표·비교 기준 제공
|
||
8. "Use contrast"
|
||
9. "Organize and group elements"
|
||
10. "Be consistent with fonts and colors"
|
||
11. "Use colors and labels effectively"
|
||
12. "Access to more detail" — 한 페이지에 욱여넣지 말고 링크로
|
||
|
||
출처: [GoSkills — 12 Best Practices in Dashboard Design](https://www.goskills.com/excel/resources/best-practices-in-dashboard-design) (WebFetch 확인)
|
||
|
||
### 2.5 데이터 잉크 비율 (Tufte)
|
||
|
||
출처: 검색 #9 종합, [Declutter your Chart with Data-Ink Ratio](https://sudharsanasai.medium.com/declutter-your-chart-with-data-ink-ratio-6f6908727842)
|
||
|
||
Tufte 의 원칙 4개:
|
||
1. Above all else show data
|
||
2. Maximize the data-ink ratio
|
||
3. Erase non-data-ink
|
||
4. Erase redundant data-ink
|
||
|
||
실무 번역:
|
||
- 스토리를 지지하지 않는 것은 전부 제거: **눈금선, 테두리, 3D 효과, 그림자, 장식**.
|
||
- 색은 **강조에만**. *"one or two colors used effectively are more powerful than a rainbow without meaning."*
|
||
- 단, **과도한 미니멀리즘 경계**: *"we need to be careful not to go too far in pursuing minimalism, as we can end up with dashboards that feel clinical, cold, or just plain boring."*
|
||
|
||
### 2.6 대시보드 ≠ 스토리
|
||
|
||
출처: [storytelling with data — from dashboard to story](https://www.storytellingwithdata.com/blog/from-dashboard-to-story) (WebFetch 확인)
|
||
|
||
- **대시보드는 탐색(exploration)에 최적화된 것이지, 특정 인사이트의 전달(communication)이 아니다.** 행동을 이끌어내려면 별도의 프레젠테이션이 낫다.
|
||
- 레이아웃: **화이트스페이스, 정렬, 그리드 레이아웃**을 활용해 모든 탐색 수준에서 읽고 이동하기 쉽게.
|
||
- 차트 선택 예시: 추이는 막대보다 **라인 그래프**, 단순 비교는 **닷 플롯**, 상대 비중 비교는 **누적 막대**.
|
||
- 정리: *"items that do not add information value, like gridlines and borders"* 를 제거하고 **색 수를 줄여** 시각적 복잡도를 낮춘다.
|
||
- **"단어 + 색"의 조합이 핵심 포인트 강조에 가장 강력**하다. 능동형 제목과 명시적 행동 권고를 붙여라.
|
||
|
||
> **DMF 리포트에 적용**: `대시보드` 시트 상단 부제(row 3)에 **문장형 헤드라인**을 자동 생성해 넣는다. 예: `오늘 신규 12건 · 변경 3건 · 취하 1건 — 워치리스트 2건 적중(피타바스타틴칼슘, 다파글리플로진)`.
|
||
|
||
### 2.7 차트 종류 선택 기준 (본 프로젝트 제안)
|
||
|
||
| 보여줄 것 | 차트 | XlsxWriter 타입 | 이유 |
|
||
|---|---|---|---|
|
||
| 최근 30일 일별 신규/변경/취하 | 누적 세로 막대 | `{'type':'column','subtype':'stacked'}` | 일별 총량 + 구성비를 동시에. 라인은 0 이 많은 일별 카운트에 부적합 |
|
||
| 최근 12개월 월별 등록 추이 | 라인 | `{'type':'line'}` | 시계열 방향성 |
|
||
| 제조국 Top 8 | 가로 막대 | `{'type':'bar'}` | 카테고리명(국가명)이 길다 → 가로가 읽기 쉬움. 파이 금지 |
|
||
| Top 성분 / Top 업체 | 표 + 데이터바 | 표 + `conditional_format data_bar` | 순위표는 차트보다 표가 정확 |
|
||
| KPI 타일 내 30일 미니 추이 | 스파크라인(column) | `add_sparkline(..., {'type':'column'})` | 셀 1칸, 데이터 잉크 비율 최고 |
|
||
| "오늘 변경 있었나" 30일 이력 | 스파크라인(win_loss) | `{'type':'win_loss'}` | 이진 결과 전용 |
|
||
|
||
**파이 차트는 전 시트에서 금지.** 5초 규칙 문서가 명시적으로 "정확한 비교에는 파이보다 막대"라고 했고, 제조국 비중은 8개 이상 카테고리라 파이가 판독 불가가 된다.
|
||
|
||
---
|
||
|
||
## 3. KPI 타일(카드) 해부
|
||
|
||
### 3.1 카드 1장의 구성 요소
|
||
|
||
[PK: An Excel Expert — Flooring Installation KPI Scorecard](https://www.pk-anexcelexpert.com/flooring-installation-kpi-scorecard-in-excel/) 의 실제 스코어카드 분해(WebFetch 확인). 카드 1장에 세로로 7요소:
|
||
|
||
1. **KPI 이름** (상단)
|
||
2. **신호등** (red/amber/green 상태 표시)
|
||
3. **큰 값** (본 숫자, 가장 큰 타이포)
|
||
4. **목표 값**
|
||
5. **절대 변화량**
|
||
6. **색이 붙은 화살표 + 변화율**
|
||
7. **12개월 스파크라인** (하단)
|
||
|
||
원문: *"each showing the KPI name, the traffic light, the value in large type, the target value, the absolute change, the percentage change with a coloured arrow, and a twelve-month sparkline."* 레이아웃은 **10장을 5열로** 배치.
|
||
|
||
이미지 참고(원문에 실린 URL 그대로):
|
||
- `https://www.pk-anexcelexpert.com/wp-content/uploads/2026/08/Flooring-Installation-KPI-Scorecard-2-1210x642.jpg`
|
||
- `https://nextgentemplates.com/wp-content/uploads/2026/08/Flooring-Installation-KPI-Scorecard-3-scaled.jpg` (KPI Analysis 페이지)
|
||
- `https://nextgentemplates.com/wp-content/uploads/2026/08/Flooring-Installation-KPI-Scorecard-4.jpg` (KPI Trend 페이지)
|
||
|
||
### 3.2 델타 화살표 — 커스텀 숫자 서식 (핵심 기법)
|
||
|
||
출처: [How To Excel — Create Amazing Key Performance Indicator Data Cards In Excel](https://www.howtoexcel.org/kpi-data-cards/) (WebFetch 확인)
|
||
|
||
퍼센트 변화에 방향 지시자를 넣는 **커스텀 숫자 서식(원문 그대로)**:
|
||
|
||
```
|
||
▲ 0.0 %;▼ -0.0 %
|
||
```
|
||
|
||
양수면 ▲, 음수면 ▼ 가 자동으로 붙는다.
|
||
|
||
본 프로젝트에서 쓸 변형(건수는 정수, 0 도 표기해야 함):
|
||
|
||
| 용도 | num_format 문자열 |
|
||
|---|---|
|
||
| 전일 대비 건수 델타 | `▲ #,##0;▼ -#,##0;– 0` |
|
||
| 전일 대비 비율 델타 | `▲ 0.0%;▼ -0.0%;– 0.0%` |
|
||
| 건수(천단위) | `#,##0"건"` |
|
||
| 날짜 | `yyyy-mm-dd` |
|
||
| 날짜+시각(메타 시트) | `yyyy-mm-dd hh:mm:ss` |
|
||
|
||
같은 문서의 카드 제작 방식: **병합 셀 대신 4개의 사각형 도형**(배경 1 + 투명 지표 2 + 흰 테두리 제목 1)을 정렬·그룹화. **다만 본 프로젝트는 도형을 쓰지 않는다** — XlsxWriter 로 도형 그룹을 만들 수 없고, 셀+테두리+병합으로 동일 효과를 낼 수 있으며 파일이 가볍다. (§9 와이어프레임 참조)
|
||
|
||
### 3.3 폰트 크기
|
||
|
||
[Big Excel Energy — Excel KPI Card Design](https://bigexcelenergy.com/excel-kpi-card-design/) (WebFetch 403 — 검색 스니펫으로만 확인, ⚠️ 부분 미검증):
|
||
|
||
> "A KPI card is a single tile that shows one metric as a large value, a small caption label, and a small colored delta against a target or prior period."
|
||
> 지표 숫자는 **28–44 pt, bold, 헤딩 폰트**로 카드 안에서 **단독 최대 요소**.
|
||
|
||
본 프로젝트 제안 타이포:
|
||
|
||
| 요소 | 폰트 | 크기 | 굵기 | 색 |
|
||
|---|---|---|---|---|
|
||
| 시트 제목 | 맑은 고딕 | 18 pt | bold | `#1F2933` |
|
||
| 부제/헤드라인 | 맑은 고딕 | 11 pt | regular | `#5B6770` |
|
||
| KPI 라벨 | 맑은 고딕 | 10 pt | bold | `#5B6770` |
|
||
| KPI 값 | 맑은 고딕 | **28 pt** | bold | 상태색(§6) |
|
||
| KPI 델타 | 맑은 고딕 | 10 pt | bold | `#005A32` / `#8A1D00` / `#5B6770` |
|
||
| 표 헤더 | 맑은 고딕 | 10 pt | bold | `#FFFFFF` on `#1F2933` |
|
||
| 표 본문 | 맑은 고딕 | 10 pt | regular | `#1F2933` |
|
||
| 각주/출처 | 맑은 고딕 | 9 pt | regular | `#5B6770` |
|
||
|
||
> 28 pt 를 택한 이유: 6장을 가로 한 줄에 배치해야 하므로 타일 폭이 26 char units(≈198px). 44 pt 는 5자리 숫자에서 넘친다. 28 pt 면 `1,234` 까지 안전.
|
||
|
||
### 3.4 본 프로젝트의 KPI 6종 (제안)
|
||
|
||
| # | KPI 이름 | 값 | 델타 기준 | 스파크라인 | 색 |
|
||
|---|---|---|---|---|---|
|
||
| 1 | 오늘 신규 | 오늘 신규 등록 건수 | 전일 대비 | 30일 일별 신규(column) | `#009E73` |
|
||
| 2 | 오늘 변경 | 오늘 변경등록 건수 | 전일 대비 | 30일 일별 변경(column) | `#E69F00` |
|
||
| 3 | 오늘 취하 | 오늘 취소/취하 건수 | 전일 대비 | 30일 일별 취하(column) | `#D55E00` |
|
||
| 4 | 오늘 총 변경분 | 1+2+3 | 전일 대비 | 30일 일별 합계(column) | `#0072B2` |
|
||
| 5 | 워치리스트 히트 | 워치리스트 매칭 건수 | 전일 대비 | 30일 win_loss | `#CC79A7` |
|
||
| 6 | 전체 누적 등록 | 누적 유효 등록 건수 | 전일 대비 | 30일 누적(line) | `#1F2933` |
|
||
|
||
---
|
||
|
||
## 4. 규제 인텔리전스 실무의 DMF 추적 관점
|
||
|
||
### 4.1 상용 도구가 실제로 제공하는 기능 (벤치마크)
|
||
|
||
| 도구 | 확인된 기능 | 출처 |
|
||
|---|---|---|
|
||
| **Leucine FDA Tracker** | 승인 벤더 리스트 업로드 또는 **경쟁사 워치리스트 구축** → 대상이 warning letter / 483 observation / import alert / recall 받는 즉시 알림. FDA enforcement 데이터 수집 → **문서 간 엔티티 연결** → **자사 제품·사이트 관련도 점수화(relevance score)** → 선제 알림 | [leucine.ai/fda-tracker](https://leucine.ai/fda-tracker/) (403, 검색 스니펫만) ⚠️ |
|
||
| **Vistaar AI** | 스마트 알림, **워치리스트**, 프로젝트 트래킹 포함 워크플로 관리, 200만+ 규정/가이던스 문서 컨텍스트 검색 | [vistaar.ai](https://www.vistaar.ai/) |
|
||
| **Thomson Reuters Regulatory Intelligence** | 다중 소스 수동 추적 부담 제거, 단일 솔루션에서 글로벌 규제 변화 예측·관리 | [regintel-content.thomsonreuters.com](https://regintel-content.thomsonreuters.com/) |
|
||
| **RegASK** | Life Sciences/소비재용 **agentic AI** 규제 인텔리전스. 글로벌 변화 모니터 → **영향도 평가(impact assessment)** → 컴플라이언스 워크플로 자동화 | [regask.com](https://regask.com/) |
|
||
| **Ideagen** | regulatory change tracking (품질 솔루션 내) | [ideagen.com](https://www.ideagen.com/solutions/quality/regulatory-intelligence/regulatory-change-tracking) |
|
||
| **Visualping** | 웹페이지 변경 감지 기반 법령 모니터링·알림 | [visualping.io/regulatory-intelligence](https://visualping.io/regulatory-intelligence) |
|
||
| **Compliance.ai / AgencyIQ / Clarivate Cortellis / DDi** | 규제 인텔리전스 DB·모니터링 | 부록 A |
|
||
| **PharmaCompass** | FDA DMF 를 필터링 가능한 형태로 재가공한 서드파티 DB. USFDA DMF + CEP/COS + 각국 승인약 통합 검색. Q3 2024 는 신규 DMF 접수 기록 경신, **중국 Type II DMF 제출 58% 급증**(분기 단위로 추적됨) | [pharmacompass.com/us-drug-master-files-dmfs](https://www.pharmacompass.com/us-drug-master-files-dmfs) (403) ⚠️ |
|
||
|
||
**본 프로젝트가 베낄 것**: ① 워치리스트 업로드/매칭, ② 변경 즉시 알림, ③ 관련도 점수(= 워치리스트 가중치), ④ 엔티티 연결(같은 제조소·같은 성분의 과거 이력 연결).
|
||
**베끼지 않을 것**: AI 영향도 평가(초기 범위 밖), 200만 문서 검색.
|
||
|
||
### 4.2 규제 인텔리전스 프로그램의 표준 운영 (PDA)
|
||
|
||
[PDA Letter — A Comprehensive Review of Regulatory Intelligence: Exploring Tools and Program Maturities](https://www.pda.org/pda-letter-portal/home/full-article/a-comprehensive-review-of-regulatory-intelligence-exploring-tools-and-program-maturities) — WebFetch 403 ⚠️ 미검증. 검색 #34 에서 확보된 실무 원칙:
|
||
|
||
- 규제기관 뉴스레터 구독 + **RSS 피드** 로 변경 모니터링.
|
||
- **모든 추적 항목과 후속 액션은 regulatory intelligence repository / tracker 에 아카이브**하고, 대시보드·뉴스레터·알림으로 이해관계자에게 **타깃 배포**.
|
||
- 알림은 **산업·기능 영역별로 애널리스트가 수동 검증**하고, 수신 주기를 커스터마이즈하며, 여러 알림을 묶어 뉴스레터로 변환해 공유.
|
||
|
||
> **DMF 리포트에 적용**: `스냅샷·로그` 시트가 곧 repository/tracker 다. 매일의 원본 스냅샷 해시와 diff 결과를 append-only 로 남긴다.
|
||
|
||
### 4.3 뉴스레터 구조 벤치마크 (ICON)
|
||
|
||
[ICON — Global Regulatory Intelligence monthly newsletter](https://www.iconplc.com/insights/regulatory-intelligence/regulatory-intelligence-newsletter) (WebFetch 확인)
|
||
|
||
- **기관별 섹션**: FDA, EMA, PMDA, NMPA, MHRA, TGA 등.
|
||
- **주제 카테고리**: marketing authorization(라벨링·post-approval submissions 포함), scientific guidance(quality/safety/efficacy), GxP guidance, 특수 주제(희귀약·백신·조기접근).
|
||
- **요약 방식**: *"The format is designed to give you the key points with relevant links so you can scan quickly and find more information on the source topics online."* → **핵심 포인트 + 원문 링크**, 스캔 가능하게.
|
||
- 주기: 월간, 이메일.
|
||
|
||
> **DMF 리포트에 적용**: `오늘 변경분` 시트의 각 행은 "핵심 필드 + 원문 공고 링크(하이퍼링크)"를 반드시 갖는다. 스캔 가능성이 최우선이므로 행 높이는 고정, 자동 줄바꿈은 `제조소소재지` 컬럼만 허용.
|
||
|
||
### 4.4 상태 뱃지 · 집계 축 · 워치리스트 · diff · 타임라인 (설계 결론)
|
||
|
||
| 요구사항 | 구현 방식 |
|
||
|---|---|
|
||
| **상태 뱃지** (신규/변경/취하) | `상태` 텍스트 컬럼(값: `신규` `변경` `취하` `연차보고` `사전등록`) + `기호` 컬럼(`+` `◆` `✕` `↻` `◇`) + 행 전체 조건부서식. 색 단독 의존 금지 |
|
||
| **성분별 집계** | `성분별` 시트: 성분명 × (신규/변경/취하/누적/업체수/제조국수) 정적 피벗 |
|
||
| **업체별 집계** | `업체별` 시트: 신청인 × (신규/변경/취하/누적/성분수/주요 제조국) |
|
||
| **제조국별 집계** | `대시보드` 우측 가로 막대 Top 8 + `업체별` 시트 보조 컬럼 |
|
||
| **경쟁사 워치리스트** | `워치리스트` 시트에 사용자가 관리하는 키(성분명/업체명/제조소명/제조국) → 매칭 결과 자동 채움. 매칭은 정규화 후 부분일치 |
|
||
| **주간/월간 추이** | `대시보드` 30일 누적 막대 + 12개월 라인. 원천 데이터는 `메타` 시트 우측 히든 영역 |
|
||
| **전일 대비 diff** | `오늘 변경분` 시트 = 전일 스냅샷과의 diff 결과만. `전체 누적` 시트에서는 `최종변경일자 = 오늘` 인 행을 조건부서식으로 강조 |
|
||
| **등록번호별 타임라인** | `전체 누적` 시트에 `등록수리일자`(등록번호에서 파싱) · `최초등록일자` · `최종변경일자` · `최종연차보고년도` · `취소/취하일자` 5개 날짜 컬럼을 나란히 두고, 각 날짜에 3색 컬러스케일 대신 **단색 데이터바** 적용 |
|
||
|
||
---
|
||
|
||
## 5. 한국 DMF 도메인 데이터 구조 (실측)
|
||
|
||
### 5.1 소스 3종
|
||
|
||
| 소스 | URL | 확인 | 성격 |
|
||
|---|---|---|---|
|
||
| 의약품안전나라 DMF 공고 | `https://nedrug.mfds.go.kr/pbp/CCBAC03` | ✅ WebFetch 성공 | 공고 목록(표) — **주 소스** |
|
||
| 의약품안전나라 DMF 정보 게시판 | `https://nedrug.mfds.go.kr/bbs/117` | ✅ WebFetch 성공 | 주/격주 공고 게시글 목록 |
|
||
| 공공데이터포털 OpenAPI | `https://www.data.go.kr/data/15057075/openapi.do` | ✅ WebFetch 성공 | 정형 JSON/XML — **교차검증용** |
|
||
|
||
> ⚠️ **정정**(`design/00b-baseline-data-analysis.md` 실측): 위 "주 소스/교차검증용" 구분은 작성 당시의 추정이었다. 실측 결과 **OpenAPI 7필드만으로 신규·변경·취하 판정이 가능**함이 확인됐다(§8.0). nedrug CCBAC03 은 여전히 유용하지만(대상의약품·연차보고년도 등 API 미제공 항목 및 사람이 직접 확인하는 원문 링크), "diff 판정의 주 소스"라는 역할은 더 이상 필수가 아니다.
|
||
|
||
추가 경로(문서로만 확인): 식약처 전자민원창구 `http://ezdrug.mfds.go.kr` (자료실 > 의약품 등) 에 **주간 원료의약품 등록현황(신규등록, 변경등록, 연차보고)** 게시.
|
||
|
||
### 5.2 nedrug DMF 공고 페이지(CCBAC03) 실제 컬럼 15개 — WebFetch 로 확인
|
||
|
||
| # | 컬럼명(원문) | 비고 |
|
||
|---|---|---|
|
||
| 1 | 순번 | 페이지 내 일련번호. 수집 시 버림 |
|
||
| 2 | 대상의약품 | 분류값. 관측된 값 예: `별표1`, `신물질` |
|
||
| 3 | 등록번호 | 구조화된 키(§5.3) |
|
||
| 4 | 성분명 | |
|
||
| 5 | 신청인 | = 등록업체(제조업체) |
|
||
| 6 | 제조소명 | |
|
||
| 7 | 제조소소재지 | 긴 문자열, 줄바꿈 필요 |
|
||
| 8 | 제조국가 | 집계 핵심 축 |
|
||
| 9 | 최초등록일자 | |
|
||
| 10 | 최종변경일자 | **diff 판정의 1차 신호** |
|
||
| 11 | 최종연차보고년도 | 연도(YYYY) |
|
||
| 12 | 취소/취하구분 | 관측값: `정상` (취소·취하 시 값 변경) |
|
||
| 13 | 취소/취하일자 | |
|
||
| 14 | 문서번호 | |
|
||
| 15 | 연계심사문서번호 | 완제-원료 연계심사 |
|
||
|
||
**검색 필터**: 발급일자 범위(시작~종료), 업소명, 등록번호, 성분명.
|
||
|
||
**신규/변경/취하 구분 방법**: 페이지 자체는 `취소/취하구분` 컬럼으로만 상태를 구분한다(관측 시점엔 전부 `정상`). → **"신규/변경"은 페이지가 알려주지 않으므로 전일 스냅샷 diff 로 우리가 판정해야 한다.** 이것이 본 크롤러의 존재 이유다.
|
||
|
||
### 5.3 등록번호 체계 — 완전 해설 (DMF 해설서 제5개정판 Q51, PDF 실측 추출)
|
||
|
||
예: `20110531-71-B-317-05` (괄호 변형: `20110531-71-B-317-05(1~9)`)
|
||
|
||
| 세그먼트 | 예 | 의미 |
|
||
|---|---|---|
|
||
| 1 | `20110531` | **등록수리일자** (YYYYMMDD) |
|
||
| 2 | `71` | 「원료의약품 등록에 관한 규정」(식약처고시) **[별표1]에 따른 각 성분의 일련번호** |
|
||
| 3 | `B` | 규정 부칙의 **시행일을 고려한 일련번호** (아래 표) |
|
||
| 4 | `317` | **동일 알파벳 군에서 접수된 성분 순으로 부여한 일련번호** |
|
||
| 5 | `05` | **동일 성분에 대하여 부여한 일련번호** (단, 일련번호 `J` 는 제외) |
|
||
| 괄호 | `(1~9)` | **동일제조소·동일성분에 대하여 허여서로 등록된 순서**로 부여한 일련번호 |
|
||
|
||
시행일군 알파벳(원문 그대로):
|
||
|
||
| 알파벳 | 대상 성분 | 시행 |
|
||
|---|---|---|
|
||
| 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호 주사제 원료의약품(2017.12.25 시행), 211호 신청사가 제조(수입)하여 등록하고자 하는 원료의약품(2018.1.1 시행) | 2017/2018 |
|
||
| K | [별표1의2] 제1호 ~ 제19호 | 2018.1.1 |
|
||
|
||
**파생 컬럼(구현 필수)**:
|
||
|
||
```python
|
||
import re
|
||
DMF_NO_RE = re.compile(
|
||
r"^(?P<accept_date>\d{8})-(?P<ingr_seq>\d+)-(?P<cohort>[A-Z])-"
|
||
r"(?P<serial>\d+)(?:-(?P<sub>\d+))?(?:\((?P<share>[^)]+)\))?$"
|
||
)
|
||
|
||
def parse_dmf_no(no: str) -> dict:
|
||
"""등록번호를 파생 컬럼으로 분해. 형식 불일치 시 raw 만 채우고 나머지는 None."""
|
||
s = (no or "").strip().replace(" ", "")
|
||
m = DMF_NO_RE.match(s)
|
||
if not m:
|
||
return {"등록번호": s, "등록수리일자": None, "성분일련번호": None,
|
||
"시행군": None, "접수일련번호": None, "성분내일련번호": None,
|
||
"허여서순번": None, "번호형식정상": False}
|
||
g = m.groupdict()
|
||
d = g["accept_date"]
|
||
return {
|
||
"등록번호": s,
|
||
"등록수리일자": f"{d[0:4]}-{d[4:6]}-{d[6:8]}",
|
||
"성분일련번호": int(g["ingr_seq"]),
|
||
"시행군": g["cohort"],
|
||
"접수일련번호": int(g["serial"]),
|
||
"성분내일련번호": int(g["sub"]) if g["sub"] else None,
|
||
"허여서순번": g["share"],
|
||
"번호형식정상": True,
|
||
}
|
||
```
|
||
|
||
### 5.4 등록·변경·연차보고·취하의 법적 정의 (실측)
|
||
|
||
「의약품 등의 안전에 관한 규칙」 제16조(원료의약품 등록대장과 등록증 등) — **공고 항목 5가지가 법에 못 박혀 있다**(PDF 원문):
|
||
|
||
> 식품의약품안전처장은 원료의약품 등록을 한 경우에는 원료의약품 등록대장(전자문서로 된 대장을 포함한다)에 다음 각 호의 사항을 적고, 신청인에게 별지 제17호서식의 원료의약품 등록증을 발급하여야 한다. 이 경우 식품의약품안전처장은 다음 각 호의 사항을 **인터넷 등으로 공고**하여야 한다.
|
||
> 1. 원료의약품의 성분ᆞ명칭
|
||
> 2. 등록자의 성명
|
||
> 3. 등록번호 및 등록 연월일
|
||
> 4. 제조소의 명칭 및 소재지
|
||
> 5. 제15조제2항에 따라 자료를 첨부하지 않은 경우 그 사실
|
||
|
||
제17조(변경등록 신청 등) 요지:
|
||
- **중요한 사항 변경 → 변경등록**(신청서 + 등록증 + 변경사유서 + 증빙). 변경등록 대상 예: **제조소 소재지의 변경**(단, 행정구역 개편 제외), 제조방법 중 원료약품(촉매·유기용매 포함)·제조공정 변경 등.
|
||
- **그 밖의 사항 변경 → 변경보고**: 등록증을 첨부하여 **매년 1월 31일까지** 보고. = 실무상 **연차보고**.
|
||
- 변경등록/변경보고를 받으면 등록대장·등록증에 기재하고 **변경사항을 인터넷 등에 공고**.
|
||
|
||
DMF 해설서(제5개정판, 2020.12) 원문:
|
||
|
||
> DMF 변경관리는 기 등록한 DMF 내용과 비교하여 변경수준에 따라 「의약품 등의 안전에 관한 규칙」제17조제1항에 따른 **중요한 변경(Major Changes)**과 같은 조 제2항에 따른 **경미한 변경(Minor Changes)**으로 구분하고 있으며 중요한 변경사항은 「변경등록」으로, 경미한 변경사항은 「변경보고(연차보고)」로 갈음될 수 있음.
|
||
|
||
> 변경등록 대상인 경우에는 **변경등록이 완료된 후** 해당 원료의약품을 제조(수입)·판매하여야 하며, 변경보고 대상인 경우에는 변경된 내용으로 제품을 제조(수입)·판매하되, **다음해 1월 말까지** 동 변경사항을 식약처장에게 보고하여야 함.
|
||
|
||
변경등록 vs 변경보고 비교표(해설서 붙임1 발췌 — 실측 원문):
|
||
|
||
| 항목 | 판정 | 근거 원문 |
|
||
|---|---|---|
|
||
| 신청인의 성명, 제조(영업)소 명칭·소재지 변경 | **변경보고** | "변경보고 대상" |
|
||
| **양도양수로 인한 신청인 변경** | **변경등록** | "양도양수로 인한 신청인 변경은 변경등록 대상" |
|
||
| 제조소 **이전**으로 인한 소재지 변경 | **변경등록**(필요시 실태조사) | 규정 제4조 자료요건 자료 제출 |
|
||
| 실제 제조장소 변경 없는 제조소 명칭·행정구역 변경 | **변경보고** | 공식기관 문서/공증자료, 변경된 GMP 증명서, 행정구역 변경 문서 중 1 |
|
||
| **인수합병에 의한 상호 변경** | **변경보고** | "단순 명칭변경으로 판단하여 변경보고 대상" |
|
||
| 제조소 책임자명·이메일·전화번호 변경 | **변경보고 불필요** | |
|
||
| 제조소 내 단순 Layout(제조설비·시험장비) 변경 | **대상 제외** | |
|
||
| 동일 지번 내 생산 site 변경(신축·증축·공장동 이동) | **변경보고** (Site Master File 제출) | |
|
||
| 제조방법 변경을 수반하는 site 변경 | **변경등록** | |
|
||
| 구조결정 기기분석 자료·단순 오기 정정 | **변경보고** | |
|
||
| 공정서 수재 성분 명칭 변경(일반명/화학명/CAS No.) | **변경보고** | 한글 맞춤법 등 단순 오기 포함 |
|
||
| 성상 표기방법 변경(예: 백색 → 흰색) | **변경보고** | |
|
||
| **유연물질 자료 변경(추가)** | **변경등록** | 유연물질 요약자료 + 검출/분석법 자료 |
|
||
| 자료 변경 없는 업데이트(문서개정 등) | **불필요** | |
|
||
| **사용기간(재시험기간) 변경** | **변경등록** | 3로트 안정성시험 자료(1로트 근거자료 포함) |
|
||
| 최초 등록 시 stability protocol 로 사용기간 인정받은 경우의 추가 안정성 자료 | **변경보고** | |
|
||
| **저장조건 변경** | **변경등록** | 3로트 안정성시험 자료 + 포장용기 자료(변경 시) |
|
||
| **중요공정(발효공정 포함 핵심중간체 생성공정 및 그 이후) 변경, 제조단위 규모 10배 이상 변경** | **변경등록** | 제조방법 자료 + 변경 전후 동등성 입증자료(불순물 profile 통계분석[최소 3배치], UV/IR/X-선 회절 등) + 시험성적 3배치(근거 1배치) + 불순물 고찰 |
|
||
| 초기단계 공정변경 등 핵심중간체 이전 공정 변경 | **변경보고** (핵심중간체 동일·불순물 정보 동일 조건) | |
|
||
|
||
**취하**: 「민원 처리에 관한 법률 시행령」 제25조제3항에 따른 **자진취하 수리**([붙임9] 민원 신청 자진취하 수리 양식). 또한 자료공유허여서 관련: *"등록 취하 신청 시 자료공유허여서를 제출한 모든 등록자를 대상으로 품목 양도·양수 여부(양수업체와 원제조소간의 계약여부 확인 포함) 또는 취하여부를 확인하고 그 결과를 제출할 것"*.
|
||
|
||
### 5.5 공고에 실제로 찍히는 "등록구분" 값 (해설서 Q60 실측)
|
||
|
||
> ※ 인터넷 공고 예시
|
||
> • **(주간 공고)** 등록, 변경등록, 연차보고, 연계 등록 원료의약품(사전등록), 연계 등록 원료의약품(사전등록) 전환, 연계 등록 원료의약품(사전등록) 변경
|
||
> • **(총 등록 공고)** '등록구분'항 신설 ⇒ 연계 등록 원료의약품(사전등록)
|
||
|
||
즉 **주간 공고의 등록구분은 6종**이다. 사전등록 품목도 등록증이 발급되고 등록번호가 부여되며, 등록증·인터넷 공고에 **"사전등록"으로 표시**된다.
|
||
|
||
> **설계 반영**: `상태` 컬럼의 도메인은 `{신규, 변경, 취하, 연차보고, 사전등록, 사전등록전환, 사전등록변경}` 7값. 대시보드 KPI 는 앞의 3+워치리스트로 압축하고, `오늘 변경분` 시트에서는 7값 전부를 보여준다.
|
||
|
||
### 5.6 처리기간·수수료 (MFDS 지침서 GRP-MaPP-허가업무-04, 2025.2.20 6개정 — PDF 실측)
|
||
|
||
| 대상 | 처리기간 |
|
||
|---|---|
|
||
| 원료의약품 등록 신청 (기본) | **20일** |
|
||
| 신약의 원료의약품 | **90일** |
|
||
| 「의약품 등의 안전에 관한 규칙」제15조제1항제1호 가목 단서에 따른 등록 | (동 지침 표 참조) |
|
||
| 원료의약품 변경등록 신청 (기본, 양도양수 포함) | **20일** |
|
||
|
||
| 항목 | 수수료 |
|
||
|---|---|
|
||
| 등록대상 원료의약품 등록 신청 | **802,000원 / 887,000원** |
|
||
| 등록대상 원료의약품 변경등록 신청 | **401,000원 / 443,000원** |
|
||
|
||
공고 절차(지침서 4.8):
|
||
|
||
> 식품의약품안전처장은 관련 규정에 따라 원료의약품 **등록, 변경등록, 변경보고 현황**을 우리처 홈페이지에 공고한다. * 「의약품 통합정보시스템」에서 식약처 담당자가 원료의약품 민원 처리 완료시 **의약품안전나라(https://nedrug.mfds.go.kr)에 등록, 변경등록, 변경보고, 현황이 공고됨** ※ 관련 규정 : 의약품 등의 안전에 관한 규칙 제16조, 제17조
|
||
|
||
> **크롤러 설계 함의**: 공고는 **민원 처리 완료 시점에 즉시** 반영된다(주간 배치가 아니라). 따라서 **일 1회 06:00 폴링이 유효**하며, 주간 공고 게시글은 보조 확인용이다.
|
||
|
||
### 5.7 OpenAPI 필드 (data.go.kr 15057075 — WebFetch 확인)
|
||
|
||
**엔드포인트**: `https://apis.data.go.kr/1471000/MdcDmfInfoService01/getMdcDmfList01`
|
||
**API 명**: 식품의약품안전처_원료의약품등록(DMF)현황
|
||
**갱신주기**: 문서에 명시 없음 ⚠️
|
||
|
||
출력 필드:
|
||
|
||
| 한글 | 영문 필드명 | 설명 |
|
||
|---|---|---|
|
||
| 등록번호 | `DMF_PERMIT_NO` | Registration number |
|
||
| 성분명 | `INGR_KOR_NAME` | Ingredient name |
|
||
| 업체명 | `ENTP_NAME` | Company name |
|
||
| 제조소명 | `MNFCTR_NAME` | Manufacturing facility name |
|
||
| 제조소 소재지 | `MNFCTR_PLACE` | Manufacturing facility location |
|
||
| 제조국가명 | `MANUF_COUNTRY_CODE_NM` | Manufacturing country |
|
||
| 발급일자 | `DMF_PERMIT_DATE` | Issue date |
|
||
|
||
요청 파라미터:
|
||
|
||
| 한글 | 파라미터 | 필수 | 설명 |
|
||
|---|---|---|---|
|
||
| 인증키 | `serviceKey` | ✅ | URL 인코딩된 인증키 |
|
||
| 업체명 | `entp_name` | ✖ | |
|
||
| 성분명 | `ingr_kor_name` | ✖ | |
|
||
| 페이지 번호 | `pageNo` | ✖ | |
|
||
| 한 페이지 결과 수 | `numOfRows` | ✖ | |
|
||
| 데이터포맷 | `type` | ✖ | xml / json |
|
||
|
||
> **주의(작성 당시 판단 — 반증됨)**: OpenAPI 는 **7필드만** 준다. `최종변경일자`, `취소/취하구분`, `연계심사문서번호` 가 없다 → **diff 판정에 부족**. 따라서 **nedrug CCBAC03 이 주 소스, OpenAPI 는 건수 교차검증용**으로 쓴다(§8.9 메타 시트의 "건수 검증").
|
||
>
|
||
> ⚠️ **정정**(`design/00b-baseline-data-analysis.md` 실측, 2026-09-02): 위 판단은 **반증됐다.** `발급일자`(`DMF_PERMIT_DATE`)가 정적인 최초 등록일이 아니라 **최종 갱신일로 동작**하고(등록번호 앞 8자리와 44.5% 불일치, 전부 괄호 갱신 건), API 응답에서 등록번호가 **소멸하면 취하**로 판정할 수 있다(웹 9,840건 vs API 9,084건, 756건 차이). 즉 `최종변경일자`·`취소/취하구분`이 없어도 **`발급일자`의 이동 + 레코드 소멸 관찰로 같은 정보를 얻을 수 있다.** 상세 판정 규칙은 §8.0.
|
||
|
||
### 5.8 시장 통계 (대시보드 벤치마크 수치 — 데일리팜 실측)
|
||
|
||
| 지표 | 값 | 출처 |
|
||
|---|---|---|
|
||
| 2025년 상반기 DMF 등록 | **653건** | [데일리팜 ID=324241](https://www.dailypharm.com/Users/News/NewsView.html?ID=324241) |
|
||
| 2024년 상반기 | **256건** (전년 대비 **2.8배**) | 동일 |
|
||
| 2024년 전체 | **545건** | 동일 |
|
||
| 종전 역대 최고(상반기) | 2021년 상반기 **537건** — 2025 상반기가 이를 초과 | 동일 |
|
||
| 규제완화 | 원료의약품 등록 시 **GMP 평가를 GMP 증명서로 대체 가능**(2025년부터) | 동일 |
|
||
| 2023년 전체 DMF | **249건**, 그중 **233건이 해외업체** | [데일리팜 ID=307819](http://m.dailypharm.com/newsView.html?ID=307819) |
|
||
| 2023년 제조국별 | 인도 **120건**, 중국 **60건**, 이탈리아 **10건**, 일본 **6건** | 동일 |
|
||
| 2023년 국산 | **16건 = 6.4%** (2005년 제도 본격 시행 이후 최저, 최초로 10% 미만) | 동일 |
|
||
| 인도+중국 합계 비중 | **72.3%** (180/249) — **최초로 70% 돌파** | 동일 |
|
||
| 인도 단독 비중 | **48.2%** (2018년 36.8% → 상승) | 동일 |
|
||
| 인도+중국 추이 | 2018년 **59.7%** → 2023년 **72.3%** | 동일 |
|
||
| 원료 자급률 | 2021년 **24.4%** → 2022년 **11.9%** | 동일 |
|
||
| 가격 요인 | 인도·중국산이 국산보다 **20~30% 저렴** | 동일 |
|
||
| 그 외 | 국가필수의약품 및 공급중단 보고대상 의약품에 **DMF 유예** 허용 | [데일리팜 32332](https://m.dailypharm.com/user/news/32332) |
|
||
| 그 외 | DMF 등록 원료도 **해외제조소 등록 대상**이며 제조소 이전 시 **신규 등록** 필요 | [데일리팜 67688](https://dailypharm.com/user/news/67688) |
|
||
|
||
DMF 해설서에 실린 **연도별 등록 공고 현황**(‘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(...)
|
||
```
|
||
|
||
> **대시보드 반영**: 제조국 Top 8 가로 막대는 이 통계 축(인도/중국/이탈리아/일본/한국/…)과 정확히 맞물린다. `업체별` 시트에 `국산여부` 플래그 컬럼을 두고, `대시보드` 하단에 **국산 비중 %** 를 보조 지표로 표시한다.
|
||
|
||
---
|
||
|
||
## 6. 색 접근성: 팔레트·상태 매핑·대비비
|
||
|
||
### 6.1 Okabe-Ito 팔레트 (기준 팔레트)
|
||
|
||
Masataka Okabe & Kei Ito, Color Universal Design(CUD) 제정. **Nature Methods 가 명시적으로 권장**하고, Claus Wilke 의 *Fundamentals of Data Visualization* 의 **기본 범주형 스케일**이다.
|
||
|
||
| # | 이름 | HEX | RGB | 흰 배경 대비비 | 검정 텍스트 대비비 | 본 프로젝트 용도 |
|
||
|---|---|---|---|---|---|---|
|
||
| 1 | Orange | `#E69F00` | 230,159,0 | 2.25 | 9.32 | **변경** |
|
||
| 2 | Sky Blue | `#56B4E9` | 86,180,233 | 2.31 | — | 보조/연차보고 차트 계열 |
|
||
| 3 | Bluish Green | `#009E73` | 0,158,115 | 3.42 | — | **신규** |
|
||
| 4 | Yellow | `#F0E442` | 240,228,66 | 1.32 | 15.88 | 사용 금지(흰 배경 대비 부족) |
|
||
| 5 | Blue | `#0072B2` | 0,114,178 | **5.19** | — | **강조색 1개 / 링크 / 합계** |
|
||
| 6 | Vermillion | `#D55E00` | 213,94,0 | 3.87 | — | **취하** |
|
||
| 7 | Reddish Purple | `#CC79A7` | 204,121,167 | 3.06 | — | **워치리스트 히트** |
|
||
| 8 | Black | `#000000` | 0,0,0 | 21.0 | — | 사용 안 함(대신 `#1F2933`) |
|
||
|
||
핵심 설계 사상(검색 #4 원문):
|
||
|
||
> The Okabe-Ito palette **avoids pure red and pure green entirely**. Instead it uses: Orange (#E69F00) — perceived as yellow-orange even in protanopia, and Bluish Green/Teal (#009E73) — which contains enough blue channel to survive the red-green merge.
|
||
|
||
### 6.2 Paul Tol 팔레트 (대안 / 차트 다계열용)
|
||
|
||
출처: [sronpersonalpages.nl/~pault/](https://sronpersonalpages.nl/~pault/) (WebFetch 성공. 참고: `personal.sron.nl/~pault/` 는 DNS 실패 `getaddrinfo ENOTFOUND` — 옛 주소이므로 **`sronpersonalpages.nl` 를 쓸 것**)
|
||
|
||
| 스킴 | HEX 목록 | 흰 배경 대비비(순서대로) | 설명(원문) |
|
||
|---|---|---|---|
|
||
| **bright** | `#4477AA` `#EE6677` `#228833` `#CCBB44` `#66CCEE` `#AA3377` `#BBBBBB` | 4.70 / 3.09 / 4.53 / 1.95 / 1.84 / 6.09 / 1.92 | "colour-blind safe", 범주형 데이터의 1순위 권장 |
|
||
| **vibrant** | `#EE7733` `#0077BB` `#33BBEE` `#EE3377` `#CC3311` `#009988` `#BBBBBB` | — | "equally colour-blind safe", TensorBoard 용으로 설계 |
|
||
| **high-contrast** | `#004488` `#DDAA33` `#BB5566` | **9.62 / 2.13 / 4.56** | "colour-blind safe and optimized for contrast" — **모노크롬 시각·흑백 인쇄에서도 잘 작동** |
|
||
| bad data grey | `#888888` | 3.54 | 결측/무효 데이터 표준 색 |
|
||
|
||
> **본 프로젝트 선택**: 상태색은 **Okabe-Ito**(3색 의미 고정), 결측·무효는 **Paul Tol `#888888`**, 흑백 인쇄를 대비해야 하는 요약표는 **high-contrast 3색**을 예비로 둔다.
|
||
|
||
### 6.3 Color Universal Design 원칙 (jfly.uni-koeln.de/color/ — WebFetch 확인)
|
||
|
||
CUD 8색: Vermilion(순수 red 아님, RGB 100%/32%/0% = `#FF2000`), Orange, Yellow, Bluish Green, Blue, Sky Blue, Reddish Purple(violet 아님), Black.
|
||
|
||
규칙(원문 요지):
|
||
|
||
1. **중복 코딩(Redundant Coding)**: *"Use not only different colors but also a combination of different shapes, positions, line types and coloring patterns."*
|
||
2. **금지 조합**: 차트/형광 이미징에서 **red 와 green 을 절대 함께 쓰지 말 것**. green 과 짝지어야 한다면 red 대신 **magenta(red+blue)** 를 쓸 것.
|
||
3. **구현**: 얇은 선·가는 폰트 대신 **굵은 선과 볼드 폰트(Arial, Helvetica)**. 색 범례 대신 **요소에 직접 라벨링**. 해칭 패턴·실선/점선·다른 기호 사용. 객체와 배경 사이의 **명도·채도 대비**를 충분히.
|
||
4. **텍스트**: *"Avoid indicating objects only by names"* — 색만이 아니라 위치·모양도 함께 서술.
|
||
|
||
### 6.4 Claus Wilke — 색 함정 (clauswilke.com/dataviz/color-pitfalls.html — WebFetch 확인)
|
||
|
||
1. **정보 과다 인코딩**: 범주형 색 스케일은 **3~5개 카테고리가 최선**, **8~10색을 넘으면** 독자의 매칭 부담이 급증. *"Use direct labeling instead of colors when you need to distinguish between more than about eight categorical items."*
|
||
2. **목적 없는 채색**: 무지개 막대 차트처럼 미적 이유만의 색은 시각 공간 낭비.
|
||
3. **비단조 스케일**: 레인보우는 순환적(시작/끝 색이 유사)이고 명도 변화가 불균일 → 그레이스케일 변환 시 결함 노출.
|
||
4. **CVD**: 순차형(sequential)은 명암 그라데이션 유지로 대체로 안전. **발산형(diverging)은 위험** — red-green 은 deutan/protan 에게, blue-green 은 tritan 에게 구별 불가. 범주형은 Okabe-Ito 로.
|
||
5. **색 요소의 크기**: *"Colors are much easier to distinguish when they are applied to large areas than to small ones or thin lines."* CVD 시뮬레이션에서 이 격차가 더 벌어진다.
|
||
6. **권고**: "안전하다는" 팔레트를 믿지 말고 **CVD 시뮬레이터로 실제 검증**하라.
|
||
|
||
> **DMF 리포트 반영**: 상태색은 **셀 배경 전체**(큰 면적)에 칠하고 폰트색만 바꾸지 않는다. 제조국 Top 8 막대는 **단색 `#0072B2`** 로 하고 색으로 국가를 구분하지 않는다(카테고리 8개 = 위험 구간). 국가 구분은 축 라벨(직접 라벨링)로 한다.
|
||
|
||
### 6.5 WCAG 1.4.1 — 색 단독 전달 금지
|
||
|
||
출처: 검색 #12, [accessibility.chat — When Color Coding Fails](https://www.accessibility.chat/articles/when-color-coding-fails-why-status-indicators-need-more-than-pretty-colors) (WebFetch 확인)
|
||
|
||
- 흔한 실수: *"a common mistake is to color-code cells (e.g., red for 'Overdue,' green for 'On Time'), but a user who is color-blind will miss this information."*
|
||
- 규정: 색은 **정보 전달의 유일한 시각 수단이 되어서는 안 된다.** 최소 1개의 다른 시각 지시자를 병행.
|
||
- 엑셀 모범: **인접 컬럼에 텍스트 라벨**("Overdue"/"On Time"), **보편적 아이콘**(✓ 성공, ✕ 오류, ! 경고)을 색과 함께.
|
||
- 유병률: **남성 약 1/12(8%), 여성 약 1/200(0.5%)**.
|
||
- accessibility.chat 권장 다중 채널:
|
||
- **텍스트 라벨**: "Online", "Degraded", "Offline"
|
||
- **아이콘**: ✓, ⚠, ✕
|
||
- **패턴**: 활성=solid fill, 경고=hatched, 오류=stippled
|
||
- **형태**: 정상=원, 경고=사각, 치명=삼각
|
||
- 핵심: *"successful implementations pair visual indicators with multiple channels of information"* — 명확성은 단일 채널의 완벽함이 아니라 **채널 간 중복 신호**에서 온다. 디자인 시스템 컴포넌트 자체에 이 요건을 박아 넣어 **색 단독 패턴을 만들 수 없게** 하라.
|
||
|
||
### 6.6 상태색 매핑표 (구현 상수, 제안)
|
||
|
||
> ⚠️ **팔레트 충돌**: research/06 §7.1 이 같은 상태에 다른 HEX 값을 제시한다. 최종 정본은 design/03-xlsx-report-spec.md 가 확정한다. 이 문서의 값은 WCAG 대비비 실측 근거가 있으므로 정본 후보로 제안한다.
|
||
|
||
**모든 상태는 [배경색 + 폰트색 + 텍스트 라벨 + 기호] 4중 코딩.** 대비비는 계산값(WCAG 2.x 상대휘도 공식).
|
||
|
||
| 상태 | 라벨 | 기호 | 배경 HEX | 폰트 HEX | 대비비 | 등급 | 액센트(차트/타일 숫자) HEX | 액센트 흰배경 대비 |
|
||
|---|---|---|---|---|---|---|---|---|
|
||
| 신규 | `신규` | `+` | `#E3F3EA` | `#005A32` | **7.28** | AAA | `#009E73` | 3.42 |
|
||
| 변경 | `변경` | `◆` | `#FDF0DC` | `#7A3E00` | **7.42** | AAA | `#E69F00` | 2.25 |
|
||
| 취하/취소 | `취하` | `✕` | `#FBE5DC` | `#8A1D00` | **7.69** | AAA | `#D55E00` | 3.87 |
|
||
| 연차보고 | `연차보고` | `↻` | `#E5F1F8` | `#00456B` | **8.85** | AAA | `#0072B2` | 5.19 |
|
||
| 사전등록 계열 | `사전등록` | `◇` | `#F7E9F0` | `#7A2E55` | **7.61** | AAA | `#CC79A7` | 3.06 |
|
||
| 워치리스트 히트 | `★` | `★` | `#F7E9F0` | `#7A2E55` | 7.61 | AAA | `#CC79A7` | 3.06 |
|
||
| 변동 없음 | `-` | `·` | `#FFFFFF` | `#5B6770` | **5.80** | AA | `#888888` | 3.54 |
|
||
| 오류/결측 | `?` | `⚠` | `#EFEFEF` | `#3A3A3A` | **9.89** | AAA | `#888888` | 3.54 |
|
||
|
||
보조 색(구조 요소):
|
||
|
||
| 용도 | HEX | 대비비 |
|
||
|---|---|---|
|
||
| 본문 텍스트 | `#1F2933` on `#FFFFFF` | **14.76** (AAA) |
|
||
| 본문 텍스트 on 연회색 | `#1F2933` on `#F7F7F7` | **13.77** (AAA) |
|
||
| 보조 텍스트 | `#5B6770` on `#FFFFFF` | 5.80 (AA) |
|
||
| 표 헤더 | `#FFFFFF` on `#1F2933` | **14.76** (AAA) |
|
||
| 시트 배경 | `#FFFFFF` | — |
|
||
| 타일 배경 | `#F7F7F7` | — |
|
||
| 구분선/테두리 | `#D0D7DE` | 1.45 (장식용, 텍스트 아님) |
|
||
| 강조(액센트) | `#0072B2` | 5.19 (AA) |
|
||
|
||
> **참고 — 엑셀 기본 조건부서식 색과의 비교**: XlsxWriter 문서 예제의 기본색은 `{"bg_color": "#FFC7CE", "font_color": "#9C0006"}`(대비 5.92)와 `{"bg_color": "#C6EFCE", "font_color": "#006100"}`(대비 6.14)이다. **둘 다 red/green 쌍이라 CUD 금지 조합**이고 대비도 우리 팔레트보다 낮다 → **사용하지 않는다.**
|
||
|
||
### 6.7 아이콘셋 사용 지침
|
||
|
||
XlsxWriter 가 지원하는 `icon_style` 전체 목록(문서 실측):
|
||
`3_arrows`, `3_arrows_gray`, `3_flags`, `3_signs`, `3_symbols`, `3_symbols_circled`, `3_traffic_lights`, `3_traffic_lights_rimmed`, `4_arrows`, `4_arrows_gray`, `4_ratings`, `4_red_to_black`, `4_traffic_lights`, `5_arrows`, `5_arrows_gray`, `5_quarters`, `5_ratings`
|
||
|
||
**본 프로젝트 선택**: `3_symbols_circled` (○ / ! / ✕ 형태 — 모양이 다르므로 CVD 안전) 또는 `3_arrows_gray`(무채색 화살표, 방향만 전달).
|
||
**금지**: `3_traffic_lights`, `3_traffic_lights_rimmed`, `4_traffic_lights` — 신호등은 **red/green 조합 + 동일 형태(원)** 이라 CVD 에서 정보가 소실된다.
|
||
|
||
```python
|
||
# 전일 대비 델타 컬럼(예: 성분별 시트 K열)에 방향만 표시. 값은 그대로 보이게 icons_only=False
|
||
worksheet.conditional_format('K3:K200', {
|
||
'type': 'icon_set',
|
||
'icon_style': '3_arrows_gray',
|
||
'icons': [{'criteria': '>=', 'type': 'number', 'value': 1},
|
||
{'criteria': '<', 'type': 'number', 'value': 1}],
|
||
})
|
||
```
|
||
|
||
`icons` 파라미터 규칙(문서 실측): `criteria` 는 `>=` 또는 `<` 만 가능(기본 `>=`), `type` 은 `number` / `percentile` / `percent` / `formula`(기본 `percent`), **값은 가장 높은 것부터 내림차순**으로 나열해야 한다. 부가 옵션: `reverse_icons: True`, `icons_only: True`.
|
||
|
||
---
|
||
|
||
## 7. 공개 데이터(FDA/EMA/식약처) 컬럼·시트 구성에서 배울 점
|
||
|
||
### 7.1 FDA DMF 목록 (List of Drug Master Files)
|
||
|
||
- **파일 형식**: Excel 로 다운로드 제공. **분기(quarterly) 갱신**.
|
||
- **컬럼 5개**: `DMF Number`, `Type`, `Status`(활동상태), `Holder`(보유자명), `Subject`(제목).
|
||
- **상태 코드**: `A` = active, `I` = inactive. 문서 원문: *"changes to the DMF activity status (A=active ; I=inactive), DMF type, holder name and subject (title) are tracked in the published list."*
|
||
- **컷오프 명시**: *"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."* → **"수령 기준일"과 "acknowledgment letter 발송 기준일"을 둘 다 명시**한다.
|
||
- BDO 가이드(WebFetch 확인) 원문: *"CDER maintains a database containing a publicly available list of DMFs filed with CDER. The list contains information about the DMF number, activity status (active or inactive), type, holder name and subject (title)."*
|
||
|
||
**DMF Type 정의** (Wikipedia 실측 + BDO):
|
||
|
||
| Type | 내용 |
|
||
|---|---|
|
||
| Type I | "Manufacturing Site, Facilities, Operating Procedures and Personnel" |
|
||
| Type II | 원료의약품(Drug substances), 중간체, 제조용 물질 |
|
||
| Type III | 포장재(Packaging materials), 용기·마개 시스템 |
|
||
| Type IV | 부형제(Excipients), 착색제, 향료 |
|
||
| Type V | Type II~IV 에 해당하지 않는 FDA 수용 참조정보(시설·공정·임상 데이터 등) |
|
||
|
||
지역 변형: 유럽 = **EDMF / ASMF**(Active Substance Master File), 미국 = **US-DMF**. Wikipedia 문서에는 **한국(KDMF) 관련 서술이 없다** (⚠️ 미검증 영역).
|
||
|
||
**John Snow Labs 재가공판 필드**(WebFetch 확인) — 정형화 참고:
|
||
|
||
| 필드 | 타입 |
|
||
|---|---|
|
||
| `DMF_Id` | integer |
|
||
| `Is_DMF_Status_Active` | boolean |
|
||
| `Type_of_DMF` | string |
|
||
| `Submission_Date` | date |
|
||
| `DMF_Holder_Name` | string |
|
||
| `DMF_Subject` | string |
|
||
| 갱신주기 | Quarterly |
|
||
| 기간 커버리지 | 1939–2022 |
|
||
|
||
> **배울 점 1 — 상태를 boolean 이 아니라 코드+라벨로**: FDA 는 `A`/`I` 코드를 쓰고, John Snow Labs 는 `Is_DMF_Status_Active` boolean 으로 바꿨다. 우리는 사람이 읽는 파일이므로 **라벨(`정상`/`취하`)을 1급으로 두고 코드는 히든 컬럼**으로 둔다.
|
||
> **배울 점 2 — 컷오프 날짜를 명시**: `메타` 시트에 "수집 기준 시각"과 "공고 반영 기준일"을 분리해 기록한다.
|
||
> **배울 점 3 — 5컬럼의 절제**: FDA 공개 목록은 5컬럼뿐이다. 우리의 `대시보드` 요약 표도 5컬럼을 넘기지 않는다.
|
||
|
||
⚠️ **미검증**: `fda.gov/drugs/drug-master-files-dmfs/list-drug-master-files-dmfs` 와 `fda.gov/drugs/forms-submission-requirements/drug-master-files-dmfs` 는 WebFetch 시 **HTTP 404**(2026-09-02). 실제 xlsx 다운로드 URL(`fda.gov/media/...`)은 확인하지 못했다.
|
||
|
||
### 7.2 FDA Orange Book 데이터 파일
|
||
|
||
- 파일 3종: `products.txt`, `patent.txt`, `exclusivity.txt`.
|
||
- `products.txt` 컬럼 14개(2019-03 기준, 36,771행): `Ingredient`, `DF;Route`, `Trade_Name`, `Applicant`, `Strength`, `Appl_Type`, `Appl_No`, `Product_No`, `TE_Code`, `Approval_Date`, `RLD`, `RS`, `Type`, `Applicant_Full_Name`.
|
||
- 컬럼 의미: `Appl_No` 형식은 `nnnnnn`. `Applicant` 는 **최대 20자 고유 문자열로 압축**된 회사명, `Applicant_Full_Name` 은 전체명. `Appl_Type` 은 `A`(ANDA, 제네릭) 또는 `N`(NDA) **두 값 배타**. `TE_Code` 는 제네릭 대 오리지널의 치료적 동등성 등급. `RLD` 는 FD&C Act 505(c) 승인 대조약 표시.
|
||
- 구분자: paracelsus 문서에 **명시 없음** ⚠️. 공식 필드 설명은 `https://www.fda.gov/drugs/informationondrugs/ucm129689.htm#descriptions`.
|
||
|
||
> **배울 점 4 — 축약명 + 전체명을 둘 다 준다**: `Applicant`(20자) / `Applicant_Full_Name`. 우리도 `신청인_표시`(30자 축약, 표 폭 유지용)와 `신청인_전체`(히든) 를 둔다. 업체명이 길어 표가 무너지는 문제를 이 방식으로 해결한다.
|
||
> **배울 점 5 — 배타적 코드 컬럼**: `Appl_Type` 처럼 값 도메인이 좁은 컬럼은 **데이터 유효성 검사(drop-down)** 를 걸어 사용자가 오염시키지 못하게 한다(`워치리스트` 시트에 적용).
|
||
|
||
### 7.3 Drugs@FDA 데이터 파일
|
||
|
||
- 배포: `drugsatfda.zip` 1개 → **12개 텍스트 테이블**로 압축 해제. **전 필드 탭 구분(tab-delimited)**.
|
||
- 테이블 예: `ActionTypes_Lookup`, `ApplicationDocs`, `Applications` 등. 각 테이블마다 **primary key, data type, field length, nulls 를 문서화**하고 **ERD 를 제공**(2025-01-10 기준).
|
||
- 갱신: **매일 아침, 월~금**. data.gov 메타데이터의 accrual periodicity = `R/P1D`(일 단위), 최종 갱신 2025-07-16. 라이선스 ODbL.
|
||
- openFDA API(`open.fda.gov/apis/drug/drugsfda/`) 필드(WebFetch 확인): `application_number`, `sponsor_name`, `openfda`(조화된 식별자), `products`(`product_number`, `brand_name`, `active_ingredients`, `marketing_status`, `dosage_form`, `route`), `submissions`(`submission_type`, `submission_number`, `submission_status`, `submission_status_date`, `submission_classification_code`, `review_priority`). 갱신 **Daily (Monday-Friday)**, 커버리지 1939~현재, 1998년 이후 승인건은 문서 완비.
|
||
|
||
> **배울 점 6 — 룩업 테이블 분리**: `ActionTypes_Lookup` 처럼 코드→라벨 매핑을 별 테이블로 뺀다. 우리는 `메타` 시트 하단에 **코드북(상태코드·국가코드·시행군 알파벳)** 을 둔다.
|
||
> **배울 점 7 — 제출 이력(submissions)이 별 테이블**: 하나의 신청번호에 여러 제출 이벤트가 붙는 구조. 우리의 `등록번호 1개 : 이벤트 N개` 구조와 동일 → `스냅샷·로그` 시트가 이 역할.
|
||
> **배울 점 8 — ERD 와 필드 정의를 함께 배포**: `메타` 시트에 컬럼 정의표를 넣어, 파일만 받아도 이해 가능하게 한다.
|
||
|
||
⚠️ **미검증**: `fda.gov/drugs/drug-approvals-and-databases/drugsfda-data-files`, `.../orange-book-data-files` 모두 WebFetch **HTTP 404**(2026-09-02).
|
||
|
||
### 7.4 EDQM CEP 데이터베이스
|
||
|
||
출처: 검색 #24 (원 페이지 `edqm.eu/en/-/deployment-of-a-new-edqm-cep-database-feature-full-download-of-cep-data` 는 **HTTP 403** ⚠️)
|
||
|
||
- 전체 다운로드에 포함되는 주요 정보: **CEP number, holder, status, European Pharmacopoeia monograph number, substance name**.
|
||
- 추가 컬럼: holder 의 **SPOR ORG 및 LOC-ID**(있을 경우), **renewal date**(아직 갱신되지 않은 경우), **마지막 절차의 closure date**.
|
||
- 검색 기준: substance name, CEP number, holder name. 부여된 CEP 목록·type·물질명·전체 CEP 번호·발급일·유효 상태로 검색 가능. **Certification Database 는 매일 갱신**.
|
||
- 절차 이력: 선택한 CEP 의 **revision type, 완료된 절차의 결과와 closure date** 를 표 형태로 조회·인쇄 가능.
|
||
|
||
> **배울 점 9 — 조직 식별자를 별도 컬럼으로**: SPOR ORG / LOC-ID 처럼 **회사·소재지의 표준 ID**를 둔다. 한국 DMF 에는 그런 공식 ID 가 없으므로 **정규화 키를 직접 생성**한다: `업체키 = 신청인 문자열 정규화(공백/괄호/(주)/㈜/Co.,Ltd. 제거 + 소문자화 + NFKC)`. 이 키가 있어야 업체별 집계와 워치리스트 매칭이 안정된다.
|
||
> **배울 점 10 — "절차 이력" 뷰**: CEP 는 인증서 1건의 revision 이력을 표로 준다. 우리도 `전체 누적` 시트에서 등록번호를 클릭하면 `스냅샷·로그` 의 해당 이력으로 점프하는 내부 링크를 건다.
|
||
|
||
### 7.5 식약처 통계연보 / 공공데이터
|
||
|
||
- 식품의약품통계연보: [목록](https://www.mfds.go.kr/brd/m_371/list.do). 최신은 **2023년 제25호**(2022년말 기준). 제24호(2022년), 제22호(2020년)도 열람 가능.
|
||
- 식의약 통계 정보 서비스 파일데이터: [data.go.kr/data/15047676](https://www.data.go.kr/data/15047676/fileData.do)
|
||
- 통계 안내: [mfds.go.kr/pgm/m_538/statInformation.do](https://www.mfds.go.kr/pgm/m_538/statInformation.do)
|
||
|
||
> **배울 점 11 — 연보 스타일의 "기준일" 표기**: "2023년 제25호(2022년말 기준)"처럼 **간행 시점과 데이터 기준 시점을 분리 표기**. `메타` 시트에서 그대로 채택.
|
||
|
||
---
|
||
|
||
## 8. 시트 구성 제안 (구현 명세)
|
||
|
||
### 8.0 API 필드 충분성 — 정정
|
||
|
||
**기존 주장과 그것이 왜 틀렸는가**
|
||
|
||
이 문서는 처음에 §5.7 에서 다음과 같이 판단했다: *"OpenAPI 는 7필드만 준다. `최종변경일자`, `취소/취하구분`, `연계심사문서번호` 가 없다 → diff 판정에 부족. 따라서 nedrug CCBAC03 이 주 소스, OpenAPI 는 건수 교차검증용으로 쓴다."* 근거는 "웹 화면에 있는 컬럼이 API 에 없으면 그 정보를 알 수 없다"는 가정이었다.
|
||
|
||
`design/00b-baseline-data-analysis.md` 가 기존 운영 산출물 `DMF_현황.xlsx`(API 7필드만으로 만들어진 파일)를 전수 분석한 결과, 이 가정은 틀렸다. API 응답 자체에 **간접 신호**가 이미 들어 있었다.
|
||
|
||
| 근거 | 실측 | 의미 |
|
||
|---|---|---|
|
||
| 건수 차이 | 웹 화면 9,840건 vs API 9,084건 = **756건** | API 는 취하·취소 건을 제외하고 정상 건만 반환한다 |
|
||
| `발급일자` 이동 | 등록번호 앞 8자리(최초 등록일)와 `발급일자`가 **44.5% 불일치**, 불일치 건은 전부 괄호 붙은 갱신 건 | `발급일자`(`DMF_PERMIT_DATE`)는 정적 최초 등록일이 아니라 **최종 갱신일**로 동작 |
|
||
| 등록번호 유일성 | 9,084건 중 중복 **0건** | diff 의 기준 키로 그대로 쓸 수 있는 완전한 자연 키 |
|
||
|
||
**세 이벤트 판정 규칙** (`00b` §6.3 을 그대로 채택)
|
||
|
||
| 판정 | 규칙 | 근거 |
|
||
|---|---|---|
|
||
| **신규** | 오늘 스냅샷에 등록번호가 있고, 어제 스냅샷에 없음 | 등록번호가 유일 키 |
|
||
| **변경** | 등록번호 동일 + `발급일자`가 어제보다 최신 | `발급일자`가 최종 갱신일로 동작 |
|
||
| **변경(보조 신호)** | 등록번호 동일 + 성분명·업체명·제조소명·제조소소재지·제조국가명 중 하나 이상이 어제와 다름 | 내용 diff |
|
||
| **취하** | 어제 스냅샷에 등록번호가 있고, 오늘 스냅샷에 없음 | API 가 정상 건만 반환 |
|
||
|
||
이로써 §5.7 의 "API 단독으로는 변경·취하 탐지 요구를 충족할 수 없다"는 결론은 **반증된다.** nedrug CCBAC03 스크래핑은 더 이상 diff 판정의 필수 전제가 아니다.
|
||
|
||
> ⚠️ **아직 완전한 증명은 아니다.** `00b` 도 명시하듯 이틀 이상 연속 수집해 실제로 레코드가 사라지는 것을 관찰해야 확정된다. 그때까지 `design/00-DATA-SOURCE-DECISION.md` §7.2 의 안전장치(건수 급감 시 diff 중단)를 유지한다.
|
||
|
||
**API 로 여전히 놓치는 것 — 정직하게 기록한다** (`00b` §6.4 근거)
|
||
|
||
| 놓치는 것 | 이유 | 영향 | 대응 |
|
||
|---|---|---|---|
|
||
| 연차보고 | `최종연차보고년도` 가 API 에 없고, 연차보고 시 `발급일자`가 갱신되는지도 불명 | 중간. 연차보고는 매년 1월 말에 몰림 | 리포트에서 별도 이벤트로 다루지 않는다. 실측 필요(부록 B) |
|
||
| 변경 사유·유형 | 어떤 항목이 왜 바뀌었는지 API 에 없음 | 낮음 | 각 행에 nedrug 검색 링크를 붙여 사람이 직접 확인 |
|
||
| 취하 vs 취소 구분 | 소멸했다는 사실만 알고 사유는 모름 | 낮음 | "목록에서 사라짐"으로만 표기 |
|
||
| 취하 일자 | 실제 취하일이 아니라 소멸을 감지한 날짜만 앎 | 낮음 | `최종변경감지일`로 기록하되 근사치임을 명시 |
|
||
| `문서번호` · `연계심사문서번호` | API 미제공. `00b` 도 다루지 않아 대체 가능성이 확인되지 않음 | 미평가 | §8.3·§8.4 컬럼에서 제외. 재조사 필요(부록 B) |
|
||
|
||
이 표가 §8.3(오늘 변경분)·§8.4(전체 누적) 컬럼 재설계의 기준이다. 정정 전 원안(구 컬럼표)은 각 절 하단에 `<details>` 로 보존했다.
|
||
|
||
### 8.1 공통 규칙
|
||
|
||
| 항목 | 규칙 |
|
||
|---|---|
|
||
| 워크북 파일명 | `DMF_Daily_YYYYMMDD.xlsx` (예: `DMF_Daily_20260902.xlsx`) |
|
||
| 시트 순서 | 대시보드 → 오늘 변경분 → 전체 누적 → 성분별 → 업체별 → 워치리스트 → 스냅샷·로그 → 메타 |
|
||
| 첫 활성 시트 | `대시보드` (`worksheet.activate()`) |
|
||
| 기본 폰트 | 맑은 고딕 10pt |
|
||
| 눈금선 | 데이터 시트는 유지, **`대시보드`만 `hide_gridlines(2)`** |
|
||
| 확대 | 데이터 시트 100%, `대시보드` **`set_zoom(90)`** |
|
||
| 헤더 행 | 모든 데이터 시트 1행 = 제목/역링크, **2행 = 컬럼 헤더**, 3행부터 데이터 |
|
||
| 틀 고정 | 데이터 시트 전부 `freeze_panes(2, 2)` 이상 (헤더 2행 + 좌측 열 고정) |
|
||
| 자동 필터 | `add_table()` 사용 시 자동 부여. 표를 안 쓰는 시트는 `autofilter()` 명시 |
|
||
| 표 스타일 | `'Table Style Light 11'` (줄무늬 약함, 데이터 잉크 비율 우선) |
|
||
| 인쇄 | `set_landscape()`, `fit_to_pages(1, 0)`, `repeat_rows(0, 1)` |
|
||
| 셀 주석 | 컬럼 헤더에 `write_comment()` 로 정의 툴팁 |
|
||
| 값 없음 | 빈 문자열이 아니라 `-` (정렬 일관성) |
|
||
|
||
**표 스타일 근거** — XlsxWriter `add_table()` 옵션(문서 실측): `style` 기본값 `'Table Style Medium 9'`, `None` 으로 해제 가능. `header_row`(기본 True), `autofilter`(기본 True, header_row 필요), `banded_rows`(기본 True), `banded_columns`(기본 False), `first_column`(기본 False), `total_row`(기본 False), `name`(기본 Table1, Table2…). 컬럼 옵션: `header`, `header_format`, `formula`, `format`, `total_function`(sum/average/count/max/min/std_dev/var/count_nums), `total_string`, `total_value`.
|
||
|
||
⚠️ **주의**: 문서 원문 — *"tables aren't available in XlsxWriter when Workbook 'constant_memory' mode is enabled."* → **`constant_memory` 를 켜지 말 것.**
|
||
|
||
### 8.2 시트 (1): `대시보드` — 탭색 `#0072B2`
|
||
|
||
- **역할**: 5초 안에 오늘 상황 파악. 스크롤 없음.
|
||
- 눈금선 숨김, 줌 90%, 인쇄 1페이지 가로. 레이아웃은 §9 와이어프레임 참조.
|
||
|
||
| 셀/범위 | 내용 | 서식 |
|
||
|---|---|---|
|
||
| `B2:R2` (merge) | `DMF 일일 모니터링 리포트 — 2026-09-02(화)` | 18pt bold, `#1F2933`, 좌측정렬, 행높이 30 |
|
||
| `B3:R3` (merge) | 자동 생성 헤드라인 문장(§2.6) | 11pt, `#5B6770`, 행높이 18 |
|
||
| `B5:C5` … `Q5:R5` | KPI 라벨 6개 | 10pt bold, `#5B6770`, 가운데, 배경 `#F7F7F7`, 상단 테두리 굵게(상태색) |
|
||
| `B6:C6` … `Q6:R6` | KPI 값 6개 | **28pt bold**, 상태색, 가운데, `#F7F7F7`, `#,##0` |
|
||
| `B7:C7` … `Q7:R7` | 전일 대비 델타 | 10pt bold, `▲ #,##0;▼ -#,##0;– 0` |
|
||
| `B8:C8` … `Q8:R8` | 30일 스파크라인 | `add_sparkline`, 행높이 14 |
|
||
| `B10` | 차트1 앵커: 최근 30일 일별 신규/변경/취하 (누적 세로 막대) | `insert_chart('B10', ch1, {'x_scale':1.65,'y_scale':1.05})` |
|
||
| `K10` | 차트2 앵커: 제조국 Top 8 (가로 막대) | `insert_chart('K10', ch2, {'x_scale':1.65,'y_scale':1.05})` |
|
||
| `B26:F26` | Top 10 성분 표 헤더: `순위 / 성분명 / 오늘 / 30일 / 누적` | 표 |
|
||
| `H26:L26` | Top 10 업체 표 헤더: `순위 / 신청인 / 오늘 / 30일 / 누적` | 표 |
|
||
| `N26:R26` | 워치리스트 히트 표 헤더: `키워드 / 매칭 / 성분명 / 신청인 / 상태` | 표 |
|
||
| `B42:R42` | 내비게이션 링크 6개 (내부 하이퍼링크) | `write_url('internal:...')` |
|
||
| `B48:R48` | 출처·면책 문구 | 9pt `#5B6770` |
|
||
|
||
**시트 간 링크 방향**: `대시보드` → 나머지 7시트(단방향 발신). 각 데이터 시트 `A1` 에는 `← 대시보드` 역링크(수신).
|
||
|
||
### 8.3 시트 (2): `오늘 변경분` — 탭색 `#D55E00`
|
||
|
||
- **역할**: 전일 스냅샷 대비 diff 결과만. **이 시트가 리포트의 본체.**
|
||
- 정렬: `상태` 커스텀 순서(취하 → 신규 → 변경) → `워치리스트히트` 내림차순 → `성분명` 오름차순. (구안의 사전등록·연차보고 세분류는 API 단독으로 구분할 수 없어 제외 — §8.0)
|
||
- 커스텀 정렬은 엑셀 사용자 정렬로 보존되지 않으므로 **히든 `정렬키` 숫자 컬럼(A열)** 을 만들어 파이썬에서 미리 정렬하고, 사용자가 다시 정렬해도 복구 가능하게 한다. (검색 #35 원문: *"A numeric helper column approach (1, 2, 3) can match your desired sort order, and this approach offers portability across files."*)
|
||
- `freeze_panes(2, 3)` — 헤더 2행 + `A,B,C` 고정.
|
||
|
||
**컬럼표 (정정: API 7필드 + 파생 필드 기준, §8.0 근거)**
|
||
|
||
| 열 | 컬럼명 | 출처 | 타입 | 열너비 | 정렬 | 표시형식 | 조건부서식 | 비고 |
|
||
|---|---|---|---|---|---|---|---|---|
|
||
| A | `정렬키` | 파생 | 정수 | 4 | 가운데 | `0` | — | 히든. 상태 정렬 순서용 |
|
||
| B | `기호` | 파생(상태 기반) | 문자 | 4 | 가운데 | `@` | 상태별 폰트색(§6.6) | `+ ◆ ✕` |
|
||
| C | `상태` | 파생(diff 결과) | 문자열 | 10 | 가운데 | `@` | **행 전체 규칙의 판정 열** | 도메인 3종: 신규/변경/취하. 판정 규칙은 §8.0 표. 구 7종 도메인의 연차보고·사전등록 세분류는 API 로 구분 불가 |
|
||
| D | `등록번호` | API 원본 `DMF_PERMIT_NO` | 문자열 | 24 | 왼쪽 | `@` | 내부 링크 → `전체 누적` | 자연키, 중복 0건 실측(`00b` §5.1) |
|
||
| E | `성분명` | API 원본 `INGR_KOR_NAME` | 문자열 | 30 | 왼쪽 | `@` | 워치 매칭 시 bold | 표기 흔들림 21그룹 실측(`00b` §8.2) |
|
||
| F | `업체명` | API 원본 `ENTP_NAME` | 문자열 | 26 | 왼쪽 | `@` | 워치 매칭 시 bold | nedrug 화면 표기는 "신청인"(`00b` §3) |
|
||
| G | `제조소명` | API 원본 `MNFCTR_NAME` | 문자열 | 26 | 왼쪽 | `@` | | 형식 오류 42건 실측(`00b` §8.4). 콤마로 다중 제조소가 이어붙지만 분해하지 않음(`Co., Ltd.` 자체에 콤마 포함) |
|
||
| H | `제조소소재지` | API 원본 `MNFCTR_PLACE` | 문자열 | 40(상한) | 왼쪽 | `@`, 자동 줄바꿈 | | **최대 473자 실측(`00b` §8.6)**. 열너비 40 상한 고정 + `text_wrap` + 행높이 고정. 표시는 앞 60자만 노출하고 전체는 셀 메모(`write_comment`)로 |
|
||
| I | `제조국가명` | API 원본 `MANUF_COUNTRY_CODE_NM` | 문자열 | 16 | 가운데 | `@` | `대한민국` 포함 시 `#E3F3EA` | 콤마 다중값, 같은 국가 반복 표기 496건 실측(`00b` §8.1) |
|
||
| J | `정규화국가` | 파생 | 문자열 | 16 | 가운데 | `@` | | 히든. 콤마 분해 → 중복 제거 → 정렬 → 재결합. 정규화 후 49개국(`00b` §7.1/8.1) |
|
||
| K | `발급일자` | API 원본 `DMF_PERMIT_DATE` | 날짜 | 12 | 가운데 | `yyyy-mm-dd` | 오늘 날짜면 bold | **최종 갱신일로 동작**(정적 발급일이 아님). 변경 판정의 1차 신호(`00b` §6) |
|
||
| L | `등록수리일자` | 파생(등록번호 파싱, §5.3) | 날짜 | 12 | 가운데 | `yyyy-mm-dd` | | 등록번호 앞 8자리 = 최초 등록일. `발급일자`와 다르면 갱신 이력이 있는 건(`00b` §6.1) |
|
||
| M | `변경차수` | 파생(등록번호 괄호 파싱) | 문자열 | 10 | 가운데 | `@` | 값 있으면 `#FDF0DC` | 괄호 안 값(`A`, `1`, `18` 등). 표준+괄호 2,973건 실측(`00b` §5.2) |
|
||
| N | `대상의약품구분` | 파생(등록번호 포맷) | 문자열 | 12 | 가운데 | `@` | | `수` 접두어 → 신물질(1,882건 실측), 그 외 → 별표1(`00b` §5.2, D4) |
|
||
| O | `변경필드` | 파생(diff 결과) | 문자열 | 34 | 왼쪽 | `@` | 자동 줄바꿈 | 성분명·업체명·제조소명·소재지·국가 중 어제와 값이 다른 필드명 목록 |
|
||
| P | `변경내용(before→after)` | 파생(diff 결과) | 문자열 | 46 | 왼쪽 | `@`, 9pt | 자동 줄바꿈 | 예: `제조소소재지: A → B` |
|
||
| Q | `워치리스트히트` | 파생 | 정수 | 8 | 가운데 | `0;;-` | `>0` → `#F7E9F0` + bold | 매칭 개수 |
|
||
| R | `워치키워드` | 파생 | 문자열 | 20 | 왼쪽 | `@` | | 쉼표 구분 |
|
||
| S | `원문 확인` | 파생(URL 생성) | 문자열 | 12 | 가운데 | `@` | `write_url(url, string='nedrug 검색')` | nedrug CCBAC03 검색 URL(등록번호 파라미터로 조립). **수집 대상 아님 — 사람이 직접 확인하는 보조 링크**(변경 사유·취하 사유 확인용, `00b` §6.4 대응) |
|
||
| T | `최초수집일` | 파생 | 날짜 | 12 | 가운데 | `yyyy-mm-dd` | | 이 시스템이 처음 이 레코드를 본 날(`00b` §3, D9) |
|
||
| U | `최종변경감지일` | 파생 | 날짜 | 12 | 가운데 | `yyyy-mm-dd` | | diff 로 변경/소멸을 감지한 날. 실제 취하일이 아니라 **감지일 근사치**임을 명시(`00b` §6.4) |
|
||
| V | `수집시각` | 파생 | 일시 | 18 | 가운데 | `yyyy-mm-dd hh:mm` | | 실행 타임스탬프 |
|
||
|
||
**제외된 컬럼**: `문서번호`, `연계심사문서번호` — OpenAPI 미제공이고 `00b` 실측도 대체 가능성을 확인하지 못했다(§8.0). 대안이 나올 때까지 이 시트에 넣지 않는다. `최종연차보고년도`, `취소/취하구분`, `취소/취하일자`(nedrug 원본)는 위 파생 필드(`상태`, `최종변경감지일`)로 대체됐다.
|
||
|
||
**조건부서식 (행 전체, 적용 범위 `B3:V1000`)**
|
||
|
||
```python
|
||
rules = [
|
||
("신규", {"bg_color": "#E3F3EA", "font_color": "#005A32"}),
|
||
("변경", {"bg_color": "#FDF0DC", "font_color": "#7A3E00"}),
|
||
("취하", {"bg_color": "#FBE5DC", "font_color": "#8A1D00", "font_strikeout": True}),
|
||
]
|
||
for label, props in rules:
|
||
fmt = workbook.add_format(props)
|
||
ws.conditional_format("B3:V1000", {
|
||
"type": "formula",
|
||
# 열은 절대($C), 행은 상대(3 = 적용범위의 첫 행)
|
||
"criteria": '=$C3="' + label + '"',
|
||
"format": fmt,
|
||
})
|
||
|
||
star = workbook.add_format({"bold": True, "font_color": "#7A2E55"})
|
||
ws.conditional_format("Q3:Q1000",
|
||
{"type": "cell", "criteria": ">", "value": 0, "format": star})
|
||
```
|
||
|
||
> **중요**: XlsxWriter 조건부서식 수식은 **US 스타일 구분자(세미콜론이 아니라 쉼표)** 를 쓰고 범위는 절대 표기를 권장한다(문서 명시). openpyxl 문서도 동일 경고: *"the formula uses an **absolute** reference to the column; but a **relative** row number."*
|
||
|
||
<details>
|
||
<summary>정정 전 원안 — API 미제공 컬럼(<code>최종변경일자</code>, <code>취소/취하구분</code>, <code>문서번호</code> 등)을 원본 컬럼으로 전제했던 구 컬럼표</summary>
|
||
|
||
| 열 | 컬럼명 | 폭 | 정렬 | 표시형식 | 조건부서식 / 수식 | 비고 |
|
||
|---|---|---|---|---|---|---|
|
||
| A | `정렬키` | 4 | 가운데 | `0` | — | **히든** `{'hidden':1}` |
|
||
| B | `기호` | 4 | 가운데 | `@` | 상태별 폰트색(§6.6) | `+ ◆ ✕ ↻ ◇` |
|
||
| C | `상태` | 10 | 가운데 | `@` | **행 전체 규칙의 판정 열** | 값 도메인 7종 |
|
||
| D | `등록번호` | 24 | 왼쪽 | `@` | 내부 링크 → `전체 누적` | 하이퍼링크 |
|
||
| E | `성분명` | 30 | 왼쪽 | `@` | 워치 매칭 시 bold | |
|
||
| F | `신청인_표시` | 26 | 왼쪽 | `@` | 워치 매칭 시 bold | 30자 축약 |
|
||
| G | `제조소명` | 26 | 왼쪽 | `@` | | |
|
||
| H | `제조국가` | 12 | 가운데 | `@` | `국내`/`대한민국` → `#E3F3EA` | |
|
||
| I | `대상의약품` | 12 | 가운데 | `@` | | `별표1` / `신물질` 등 |
|
||
| J | `최초등록일자` | 12 | 가운데 | `yyyy-mm-dd` | | |
|
||
| K | `최종변경일자` | 12 | 가운데 | `yyyy-mm-dd` | 오늘이면 bold | |
|
||
| L | `취소/취하구분` | 10 | 가운데 | `@` | `취하`면 취소선 | |
|
||
| M | `취소/취하일자` | 12 | 가운데 | `yyyy-mm-dd` | | |
|
||
| N | `변경필드` | 34 | 왼쪽 | `@` | 자동 줄바꿈 | diff 로 잡힌 필드명 목록 |
|
||
| O | `변경내용(before→after)` | 46 | 왼쪽 | `@` | 자동 줄바꿈, 9pt | `제조소소재지: A → B` |
|
||
| P | `워치리스트히트` | 8 | 가운데 | `0;;-` | `>0` → `#F7E9F0` + bold | 매칭 개수 |
|
||
| Q | `워치키워드` | 20 | 왼쪽 | `@` | | 쉼표 구분 |
|
||
| R | `문서번호` | 16 | 왼쪽 | `@` | | |
|
||
| S | `연계심사문서번호` | 18 | 왼쪽 | `@` | 값 있으면 `#E5F1F8` | |
|
||
| T | `원문링크` | 12 | 가운데 | `@` | `write_url(url, string='공고 보기')` | 외부 링크 |
|
||
| U | `수집시각` | 18 | 가운데 | `yyyy-mm-dd hh:mm` | | |
|
||
|
||
이 원안은 `L`(취소/취하구분), `M`(취소/취하일자), `R`(문서번호), `S`(연계심사문서번호), `I`(대상의약품, nedrug 원본), `J`(최초등록일자, nedrug 원본)를 **nedrug CCBAC03 스크래핑이 있어야만** 채울 수 있다고 전제했다. §8.0 의 정정으로 이 전제가 필요 없어졌다.
|
||
|
||
</details>
|
||
|
||
### 8.4 시트 (3): `전체 누적` — 탭색 `#5B6770`
|
||
|
||
- **역할**: 유효 등록 전체 목록. 필터·정렬·틀고정으로 사용자가 직접 파고드는 시트.
|
||
- `add_table()` 로 감싸 자동 필터 부여. `freeze_panes(2, 3)`. 정렬은 `등록수리일자` 내림차순.
|
||
- 행 수 수천~수만(실측 기준선 9,084행, `00b` §7.1) → **행 높이 고정(15), 자동 줄바꿈 없음**.
|
||
- **정정**: API 는 애초에 취하·취소 건을 제외한 정상 건만 반환한다(`00b` §4). 따라서 이 시트에 나타나는 모든 행은 이미 유효 등록이며, 별도의 `취소/취하구분='정상'` 필터가 필요 없다. 취하는 "행이 다음날 사라짐"으로 나타나므로 이력은 `스냅샷·로그` 시트에서 확인한다.
|
||
|
||
**컬럼표 (정정: API 7필드 + 파생 필드 기준, §8.0 근거)**
|
||
|
||
| 열 | 컬럼명 | 출처 | 타입 | 열너비 | 정렬 | 표시형식 | 조건부서식 | 비고 |
|
||
|---|---|---|---|---|---|---|---|---|
|
||
| A | `등록번호` | API 원본 `DMF_PERMIT_NO` | 문자열 | 24 | 왼쪽 | `@` | 중복값 강조 `{'type':'duplicate'}`(이상 탐지용, 정상 시 0건) | 자연키, 중복 0건 실측(`00b` §5.1) |
|
||
| B | `성분명` | API 원본 `INGR_KOR_NAME` | 문자열 | 30 | 왼쪽 | `@` | 워치 매칭 시 `#F7E9F0` | |
|
||
| C | `업체명` | API 원본 `ENTP_NAME` | 문자열 | 26 | 왼쪽 | `@` | 워치 매칭 시 `#F7E9F0` | nedrug 화면 표기는 "신청인" |
|
||
| D | `제조소명` | API 원본 `MNFCTR_NAME` | 문자열 | 26 | 왼쪽 | `@` | | |
|
||
| E | `제조소소재지` | API 원본 `MNFCTR_PLACE` | 문자열 | 40(상한) | 왼쪽 | `@`, 자동 줄바꿈 | 헤더에 주석(툴팁) | **최대 473자 실측(`00b` §8.6)**. 열너비 40 상한 + `text_wrap` + 행높이 고정 필수 |
|
||
| F | `제조국가명` | API 원본 `MANUF_COUNTRY_CODE_NM` | 문자열 | 16 | 가운데 | `@` | `text containing '대한민국'` → `#E3F3EA` | 원문 그대로(콤마 다중값). 국가 중복 표기 496건 실측(`00b` §8.1) |
|
||
| G | `정규화국가` | 파생 | 문자열 | 16 | 가운데 | `@` | | 히든. 정규화 후 49개국(`00b` §7.1/8.1) |
|
||
| H | `상태` | 파생(diff 결과) | 문자열 | 10 | 가운데 | `@` | 상태색(§6.6) | 도메인: 신규/변경/정상(무변동). 취하는 레코드 자체가 사라지므로 이 시트에는 값으로 남지 않는다 |
|
||
| I | `발급일자` | API 원본 `DMF_PERMIT_DATE` | 날짜 | 12 | 가운데 | `yyyy-mm-dd` | **오늘 = `#FDF0DC`**(전일 대비 diff 강조) | **최종 갱신일로 동작**(`00b` §6) |
|
||
| J | `등록수리일자` | 파생(등록번호 파싱, §5.3) | 날짜 | 12 | 가운데 | `yyyy-mm-dd` | 데이터바 `#0072B2` | 등록번호 앞 8자리 = 최초 등록일 |
|
||
| K | `변경차수` | 파생(등록번호 괄호 파싱) | 문자열 | 10 | 가운데 | `@` | 값 있으면 옅은 강조 | 괄호 안 값. 표준+괄호 2,973건 실측(`00b` §5.2) |
|
||
| L | `대상의약품구분` | 파생(등록번호 포맷) | 문자열 | 12 | 가운데 | `@` | | `수` 접두어 → 신물질(1,882건), 그 외 → 별표1(`00b` D4) |
|
||
| M | `성분일련번호` | 파생(§5.3 파서) | 정수 | 8 | 가운데 | `0` | **히든** | |
|
||
| N | `시행군` | 파생(§5.3 파서) | 문자 | 6 | 가운데 | `@` | **히든** | |
|
||
| O | `접수일련번호` | 파생(§5.3 파서) | 정수 | 8 | 가운데 | `0` | **히든** | |
|
||
| P | `성분내일련번호` | 파생(§5.3 파서) | 정수 | 8 | 가운데 | `0` | **히든** | |
|
||
| Q | `허여서순번` | 파생(§5.3 파서) | 문자 | 10 | 가운데 | `@` | **히든** | |
|
||
| R | `정규화성분키` | 파생 | 문자열 | 26 | 왼쪽 | `@` | **히든**(정규화 키) | 표기 흔들림 21그룹 정규화(`00b` §8.2) |
|
||
| S | `정규화업체키` | 파생 | 문자열 | 26 | 왼쪽 | `@` | **히든**(정규화 키) | 표기 흔들림 2그룹 정규화(`00b` §8.3) |
|
||
| T | `최초수집일` | 파생 | 날짜 | 12 | 가운데 | `yyyy-mm-dd` | | 크롤러가 처음 본 날 |
|
||
| U | `최종변경감지일` | 파생 | 날짜 | 12 | 가운데 | `yyyy-mm-dd` | | diff 로 변경을 감지한 날 |
|
||
| V | `행해시` | 파생 | 문자열 | 16 | 왼쪽 | `@` | **히든**. diff 판정용 SHA1 앞 12자 |
|
||
|
||
**제외된 컬럼**: `최종연차보고년도`, `취소/취하구분`, `취소/취하일자`, `문서번호`, `연계심사문서번호` — API 미제공(§8.0). `연차보고`는 대체 신호가 없어 실질적 손실로 남는다(`00b` §6.4, 부록 B 확인 필요).
|
||
|
||
**링크**: `A` 열 등록번호 → `스냅샷·로그` 의 해당 등록번호 이력(수신).
|
||
|
||
<details>
|
||
<summary>정정 전 원안 — API 미제공 컬럼(<code>최종연차보고년도</code>, <code>취소/취하구분</code>, <code>문서번호</code> 등)을 원본 컬럼으로 전제했던 구 컬럼표</summary>
|
||
|
||
| 열 | 컬럼명 | 폭 | 정렬 | 표시형식 | 조건부서식 / 수식 |
|
||
|---|---|---|---|---|---|
|
||
| A | `등록번호` | 24 | 왼쪽 | `@` | 중복값 강조 `{'type':'duplicate'}` (동일번호 재공고 탐지) |
|
||
| B | `성분명` | 30 | 왼쪽 | `@` | 워치 매칭 시 `#F7E9F0` |
|
||
| C | `신청인_표시` | 26 | 왼쪽 | `@` | 워치 매칭 시 `#F7E9F0` |
|
||
| D | `제조소명` | 26 | 왼쪽 | `@` | |
|
||
| E | `제조소소재지` | 40 | 왼쪽 | `@` | 헤더에 주석(툴팁) |
|
||
| F | `제조국가` | 12 | 가운데 | `@` | `text containing '대한민국'` → `#E3F3EA` |
|
||
| G | `대상의약품` | 12 | 가운데 | `@` | |
|
||
| H | `등록구분` | 12 | 가운데 | `@` | 상태색(§6.6) |
|
||
| I | `등록수리일자` | 12 | 가운데 | `yyyy-mm-dd` | 데이터바 `#0072B2` |
|
||
| J | `최초등록일자` | 12 | 가운데 | `yyyy-mm-dd` | |
|
||
| K | `최종변경일자` | 12 | 가운데 | `yyyy-mm-dd` | **오늘 = `#FDF0DC`** (전일 대비 diff 강조) |
|
||
| L | `최종연차보고년도` | 12 | 가운데 | `0000` | 3년 이상 경과 → `#EFEFEF` |
|
||
| M | `취소/취하구분` | 10 | 가운데 | `@` | `정상` 아니면 행 전체 `#FBE5DC` + 취소선 |
|
||
| N | `취소/취하일자` | 12 | 가운데 | `yyyy-mm-dd` | |
|
||
| O | `문서번호` | 16 | 왼쪽 | `@` | |
|
||
| P | `연계심사문서번호` | 18 | 왼쪽 | `@` | 값 있으면 `#E5F1F8` |
|
||
| Q | `성분일련번호` | 8 | 가운데 | `0` | **히든**(파생) |
|
||
| R | `시행군` | 6 | 가운데 | `@` | **히든**(파생) |
|
||
| S | `접수일련번호` | 8 | 가운데 | `0` | **히든**(파생) |
|
||
| T | `성분내일련번호` | 8 | 가운데 | `0` | **히든**(파생) |
|
||
| U | `허여서순번` | 10 | 가운데 | `@` | **히든**(파생) |
|
||
| V | `업체키` | 26 | 왼쪽 | `@` | **히든**(정규화 키) |
|
||
| W | `성분키` | 26 | 왼쪽 | `@` | **히든**(정규화 키) |
|
||
| X | `최초관측일` | 12 | 가운데 | `yyyy-mm-dd` | 크롤러가 처음 본 날 |
|
||
| Y | `최종관측일` | 12 | 가운데 | `yyyy-mm-dd` | |
|
||
| Z | `행해시` | 16 | 왼쪽 | `@` | **히든**. diff 판정용 SHA1 앞 12자 |
|
||
|
||
이 원안은 `H`(등록구분), `L`(최종연차보고년도), `M`(취소/취하구분), `N`(취소/취하일자), `O`(문서번호), `P`(연계심사문서번호)를 **nedrug CCBAC03 스크래핑이 있어야만** 채울 수 있다고 전제했다. §8.0 의 정정으로 `등록구분`·`취소/취하구분`·`취소/취하일자`는 API 파생 필드로 대체 가능함이 확인됐고, 나머지 3개는 실질적 손실로 §8.0 표에 남는다.
|
||
|
||
</details>
|
||
|
||
### 8.5 시트 (4): `성분별` — 탭색 `#009E73`
|
||
|
||
- **역할**: 성분(원료명) 기준 정적 집계표(피벗 대체). 정렬: `오늘 합계` 내림차순 → `누적` 내림차순.
|
||
- `add_table()` + `total_row: True`.
|
||
|
||
| 열 | 컬럼명 | 폭 | 정렬 | 표시형식 | 조건부서식 / 수식 |
|
||
|---|---|---|---|---|---|
|
||
| A | `순위` | 6 | 가운데 | `0` | `total_string: '합계'` |
|
||
| B | `성분명` | 34 | 왼쪽 | `@` | 워치 매칭 시 `#F7E9F0` + bold |
|
||
| C | `오늘 신규` | 9 | 가운데 | `#,##0;;-` | `>0` → `#E3F3EA`, `total_function:'sum'` |
|
||
| D | `오늘 변경` | 9 | 가운데 | `#,##0;;-` | `>0` → `#FDF0DC`, `total_function:'sum'` |
|
||
| E | `오늘 취하` | 9 | 가운데 | `#,##0;;-` | `>0` → `#FBE5DC`, `total_function:'sum'` |
|
||
| F | `오늘 합계` | 9 | 가운데 | `#,##0;;-` | `total_function:'sum'` |
|
||
| G | `최근 7일` | 9 | 가운데 | `#,##0;;-` | 데이터바 `#0072B2` |
|
||
| H | `최근 30일` | 9 | 가운데 | `#,##0;;-` | 데이터바 `#0072B2` |
|
||
| I | `누적 유효등록` | 11 | 가운데 | `#,##0` | 데이터바 `#0072B2`, `total_function:'sum'` |
|
||
| J | `등록업체 수` | 9 | 가운데 | `#,##0` | `>=5` → bold (공급처 다변화 신호) |
|
||
| K | `제조국 수` | 9 | 가운데 | `#,##0` | |
|
||
| L | `주요 제조국` | 14 | 가운데 | `@` | 최다 건수 국가 |
|
||
| M | `국산 보유` | 8 | 가운데 | `@` | `Y`/`N`, `Y` → `#E3F3EA` |
|
||
| N | `30일 추이` | 14 | 가운데 | — | **스파크라인**(column), 원천은 `메타!` 히든 영역 |
|
||
| O | `전일 대비` | 9 | 가운데 | `▲ #,##0;▼ -#,##0;– 0` | `icon_set` `3_arrows_gray` |
|
||
| P | `상세` | 8 | 가운데 | `@` | 내부 링크 → `전체 누적` |
|
||
|
||
### 8.6 시트 (5): `업체별` — 탭색 `#009E73`
|
||
|
||
구조는 `성분별`과 대칭.
|
||
|
||
| 열 | 컬럼명 | 폭 | 정렬 | 표시형식 | 조건부서식 / 수식 |
|
||
|---|---|---|---|---|---|
|
||
| A | `순위` | 6 | 가운데 | `0` | `total_string: '합계'` |
|
||
| B | `신청인_표시` | 30 | 왼쪽 | `@` | 워치 매칭 시 `#F7E9F0` + bold |
|
||
| C | `신청인_전체` | 40 | 왼쪽 | `@` | **히든** |
|
||
| D | `업체키` | 26 | 왼쪽 | `@` | **히든** |
|
||
| E | `오늘 신규` | 9 | 가운데 | `#,##0;;-` | `>0` → `#E3F3EA` |
|
||
| F | `오늘 변경` | 9 | 가운데 | `#,##0;;-` | `>0` → `#FDF0DC` |
|
||
| G | `오늘 취하` | 9 | 가운데 | `#,##0;;-` | `>0` → `#FBE5DC` |
|
||
| H | `오늘 합계` | 9 | 가운데 | `#,##0;;-` | `total_function:'sum'` |
|
||
| I | `최근 30일` | 9 | 가운데 | `#,##0;;-` | 데이터바 |
|
||
| J | `누적 유효등록` | 11 | 가운데 | `#,##0` | 데이터바, `total_function:'sum'` |
|
||
| K | `보유 성분 수` | 9 | 가운데 | `#,##0` | |
|
||
| L | `제조소 수` | 9 | 가운데 | `#,##0` | |
|
||
| M | `주요 제조국` | 14 | 가운데 | `@` | |
|
||
| N | `국산여부` | 8 | 가운데 | `@` | `국산`/`해외`, `국산` → `#E3F3EA` |
|
||
| O | `최근 등록일` | 12 | 가운데 | `yyyy-mm-dd` | 90일 이상 경과 → `#EFEFEF` |
|
||
| P | `30일 추이` | 14 | 가운데 | — | 스파크라인(column) |
|
||
| Q | `워치리스트` | 8 | 가운데 | `@` | `★`/`-`, `★` → `#F7E9F0` |
|
||
| R | `상세` | 8 | 가운데 | `@` | 내부 링크 → `전체 누적` |
|
||
|
||
### 8.7 시트 (6): `워치리스트` — 탭색 `#CC79A7`
|
||
|
||
- **역할**: **사용자가 직접 편집하는 유일한 시트.** 다음 실행 시 이 시트를 읽어 매칭한다.
|
||
- `A~G` = 입력 영역(사용자 편집), `I~O` = 결과 영역(프로그램 생성, 시트 보호로 잠금).
|
||
|
||
**입력 영역**
|
||
|
||
| 열 | 컬럼명 | 폭 | 정렬 | 표시형식 | 유효성 / 규칙 |
|
||
|---|---|---|---|---|---|
|
||
| A | `번호` | 6 | 가운데 | `0` | |
|
||
| B | `대상유형` | 12 | 가운데 | `@` | `data_validation` list: `성분명`,`신청인`,`제조소명`,`제조국가`,`등록번호` |
|
||
| C | `키워드` | 30 | 왼쪽 | `@` | 필수. 빈 값이면 `#FBE5DC` |
|
||
| D | `매칭방식` | 12 | 가운데 | `@` | list: `부분일치`,`정확일치`,`정규식` |
|
||
| E | `우선순위` | 8 | 가운데 | `0` | list: `1`,`2`,`3` (1 = 최상) |
|
||
| F | `활성` | 6 | 가운데 | `@` | list: `Y`,`N` |
|
||
| G | `메모` | 26 | 왼쪽 | `@` | 자유 입력 |
|
||
|
||
**결과 영역**
|
||
|
||
| 열 | 컬럼명 | 폭 | 정렬 | 표시형식 | 조건부서식 |
|
||
|---|---|---|---|---|---|
|
||
| I | `오늘 매칭` | 9 | 가운데 | `#,##0;;-` | `>0` → `#F7E9F0` + bold |
|
||
| J | `최근 7일` | 9 | 가운데 | `#,##0;;-` | 데이터바 |
|
||
| K | `최근 30일` | 9 | 가운데 | `#,##0;;-` | 데이터바 |
|
||
| L | `누적 매칭` | 9 | 가운데 | `#,##0` | |
|
||
| M | `마지막 매칭일` | 12 | 가운데 | `yyyy-mm-dd` | 오늘이면 `#F7E9F0` |
|
||
| N | `마지막 매칭 대상` | 34 | 왼쪽 | `@` | |
|
||
| O | `상세` | 8 | 가운데 | `@` | `internal:'오늘 변경분'!A1` |
|
||
|
||
**하단 매칭 상세 표** (`A20` 이하): `키워드 / 상태 / 등록번호 / 성분명 / 신청인 / 제조국가 / 최종변경일자` 7컬럼.
|
||
|
||
> **입력 보존 규칙(구현 필수)**: 매일 새 xlsx 를 만들므로, **직전 파일의 `워치리스트` 시트 `A:G` 영역을 openpyxl 로 읽어 새 파일에 이월**한다. 더 안전한 방식은 `config/watchlist.csv` 를 SSOT 로 두고 엑셀은 미러로 쓰는 것(§11.5).
|
||
|
||
### 8.8 시트 (7): `스냅샷·로그` — 탭색 `#888888`
|
||
|
||
- **역할**: append-only 감사 로그 + 원본 스냅샷 지문. regulatory intelligence 의 "tracker/repository"(§4.2).
|
||
- 정렬: `수집시각` 내림차순. `freeze_panes(2, 1)`.
|
||
|
||
| 열 | 컬럼명 | 폭 | 정렬 | 표시형식 | 조건부서식 / 비고 |
|
||
|---|---|---|---|---|---|
|
||
| A | `수집시각` | 18 | 가운데 | `yyyy-mm-dd hh:mm:ss` | |
|
||
| B | `실행ID` | 14 | 왼쪽 | `@` | UUID 앞 8자 |
|
||
| C | `소스` | 16 | 가운데 | `@` | `nedrug` / `openapi` |
|
||
| D | `소스URL` | 40 | 왼쪽 | `@` | 외부 하이퍼링크 |
|
||
| E | `이벤트` | 12 | 가운데 | `@` | `신규`/`변경`/`취하`/`재공고`/`무변동` |
|
||
| F | `등록번호` | 24 | 왼쪽 | `@` | 내부 링크 → `전체 누적` |
|
||
| G | `필드` | 20 | 왼쪽 | `@` | 변경된 필드명 |
|
||
| H | `이전값` | 34 | 왼쪽 | `@` | 9pt |
|
||
| I | `이후값` | 34 | 왼쪽 | `@` | 9pt, `#FDF0DC` |
|
||
| J | `행해시_이전` | 14 | 왼쪽 | `@` | **히든** |
|
||
| K | `행해시_이후` | 14 | 왼쪽 | `@` | **히든** |
|
||
| L | `스냅샷파일` | 30 | 왼쪽 | `@` | 로컬 경로 |
|
||
| M | `스냅샷SHA256` | 20 | 왼쪽 | `@` | 앞 16자 |
|
||
| N | `HTTP상태` | 8 | 가운데 | `0` | `!=200` → `#FBE5DC` |
|
||
| O | `소요(ms)` | 9 | 가운데 | `#,##0` | `>10000` → `#FDF0DC` |
|
||
| P | `에이전트` | 14 | 가운데 | `@` | `agy -p` / `requests` |
|
||
| Q | `비고` | 30 | 왼쪽 | `@` | 에러 메시지 **원문 그대로** 보존 |
|
||
|
||
> **보존 정책**: 시트 행이 50,000 을 넘으면 `logs/archive/YYYYMM.parquet` 로 롤오프하고 시트에는 최근 90일만 남긴다.
|
||
|
||
### 8.9 시트 (8): `메타` — 탭색 `#888888`
|
||
|
||
**블록 1 — 수집 메타 (`A2:D14`)**
|
||
|
||
| 항목 | 값 예 | 표시형식 |
|
||
|---|---|---|
|
||
| 리포트 기준일 | `2026-09-02` | `yyyy-mm-dd` |
|
||
| 수집 시작 시각 | `2026-09-02 06:00:03` | `yyyy-mm-dd hh:mm:ss` |
|
||
| 수집 종료 시각 | `2026-09-02 06:04:41` | `yyyy-mm-dd hh:mm:ss` |
|
||
| 공고 반영 기준일 | `2026-09-01` | `yyyy-mm-dd` |
|
||
| 실행 ID | `a1b2c3d4` | `@` |
|
||
| 실행 호스트 | `DESKTOP-XXXX` | `@` |
|
||
| 실행 방식 | `Task Scheduler / agy -p` | `@` |
|
||
| 파이썬 버전 | `3.12.x` | `@` |
|
||
| xlsxwriter | `3.2.1` | `@` |
|
||
| openpyxl | `3.1.5` | `@` |
|
||
| pandas | `2.2.3` | `@` |
|
||
| 직전 스냅샷 | `snapshots/20260901.json` | `@` |
|
||
| 직전 리포트 | `out/DMF_Daily_20260901.xlsx` | `@` (외부 링크) |
|
||
|
||
**블록 2 — 소스 목록 (`A17:E24`)**: `소스명 / URL / HTTP상태 / 수집행수 / 비고`. 최소 3행(nedrug 공고 `CCBAC03`, nedrug 게시판 `bbs/117`, OpenAPI `getMdcDmfList01`).
|
||
|
||
**블록 3 — 건수 검증 (`A27:E36`)**
|
||
|
||
| 검증 항목 | 기대 | 실제 | 판정 | 규칙 |
|
||
|---|---|---|---|---|
|
||
| nedrug 총 행수 | 전일 총계 + diff | 실측 | `OK`/`불일치` | 항등식 |
|
||
| OpenAPI totalCount | nedrug 총 행수 ± 허용치 | 실측 | | 교차검증 |
|
||
| 신규+변경+취하 = 오늘 총 변경분 | | | | 항등식 |
|
||
| 등록번호 중복 | 0 | | | |
|
||
| 등록번호 형식 불일치 | 0 | | | `번호형식정상 = False` 개수 |
|
||
| 필수 컬럼 결측 | 0 | | | |
|
||
| 제조국가 미매핑 | 0 | | | 코드북에 없는 국가명 |
|
||
| 워치리스트 유효 항목 | | | | `활성=Y` 개수 |
|
||
|
||
판정 셀 조건부서식: `text containing 'OK'` → `#E3F3EA`/`#005A32`; `'불일치'` → `#FBE5DC`/`#8A1D00`.
|
||
|
||
**블록 4 — 코드북 (`A39:C80`)**: `구분 / 코드 / 설명`. 상태 7종, 시행군 알파벳 A~K(§5.3 표 그대로), DMF Type I~V(참고), 제조국 표기 정규화 매핑.
|
||
|
||
**히든 데이터 영역 (`AA1:BZ400`)**: 스파크라인·차트가 참조하는 원천 시계열. 컬럼 폭 0 + `{'hidden': 1}`.
|
||
- `AA`: 날짜(최근 30일)
|
||
- `AB`: 일별 신규 / `AC`: 일별 변경 / `AD`: 일별 취하 / `AE`: 일별 합계 / `AF`: 워치 히트(1/0, win_loss 용)
|
||
- `AH:AI`: 제조국 Top 8 (국가명, 건수)
|
||
- `AK:BZ`: 성분별·업체별 30일 시계열(행 = 항목, 열 = 일자)
|
||
|
||
### 8.10 시트 탭색 · 링크 방향 요약
|
||
|
||
| # | 시트명 | 탭색 | 링크 발신 → | 링크 수신 ← |
|
||
|---|---|---|---|---|
|
||
| 1 | `대시보드` | `#0072B2` | 전 시트(7개) | 모든 데이터 시트 `A1` 의 `← 대시보드` |
|
||
| 2 | `오늘 변경분` | `#D55E00` | `전체 누적`(등록번호), 외부 공고 URL | `대시보드`, `워치리스트` |
|
||
| 3 | `전체 누적` | `#5B6770` | `스냅샷·로그`(등록번호 이력) | `대시보드`, `오늘 변경분`, `성분별`, `업체별` |
|
||
| 4 | `성분별` | `#009E73` | `전체 누적` | `대시보드` |
|
||
| 5 | `업체별` | `#009E73` | `전체 누적` | `대시보드` |
|
||
| 6 | `워치리스트` | `#CC79A7` | `오늘 변경분` | `대시보드` |
|
||
| 7 | `스냅샷·로그` | `#888888` | — | `대시보드`, `전체 누적` |
|
||
| 8 | `메타` | `#888888` | 외부 소스 URL | `대시보드` |
|
||
|
||
**내부 링크 문법 (XlsxWriter 문서 실측)**
|
||
|
||
```python
|
||
worksheet.write_url("A1", "internal:Sheet2!A1")
|
||
worksheet.write_url("A3", "internal:'Sales Data'!A1") # 시트명에 공백 → 홑따옴표
|
||
worksheet.write_url("A1", "internal:Sheet2!A1", string="Link to sheet2", tip="Click here")
|
||
```
|
||
|
||
본 프로젝트 시트명에는 공백이 있으므로(`오늘 변경분`, `전체 누적`, `스냅샷·로그`) **반드시 홑따옴표로 감싼다**: `"internal:'오늘 변경분'!A1"`.
|
||
관련 이슈: [XlsxWriter #609 — write_url when internally referencing a sheet (with spaces)](https://github.com/jmcnamara/XlsxWriter/issues/609), [#350 — write_url produces broken hyperlink](https://github.com/jmcnamara/XlsxWriter/issues/350), [#511 — 여러 URL 포함 셀](https://github.com/jmcnamara/XlsxWriter/issues/511).
|
||
|
||
---
|
||
|
||
## 9. 대시보드 레이아웃 텍스트 와이어프레임
|
||
|
||
### 9.1 열 폭 / 행 높이 그리드
|
||
|
||
**열 폭** (`set_column`, 단위 = 문자폭)
|
||
|
||
| 열 | 폭 | 역할 |
|
||
|---|---|---|
|
||
| `A` | 1.5 | 좌 여백 |
|
||
| `B`, `C` | 13, 13 | KPI 타일 1 / 좌측 차트 / Top 성분 표 |
|
||
| `D` | 1.5 | 거터 |
|
||
| `E`, `F` | 13, 13 | KPI 타일 2 |
|
||
| `G` | 1.5 | 거터 |
|
||
| `H`, `I` | 13, 13 | KPI 타일 3 / Top 업체 표 |
|
||
| `J` | 1.5 | 거터 |
|
||
| `K`, `L` | 13, 13 | KPI 타일 4 / 우측 차트 |
|
||
| `M` | 1.5 | 거터 |
|
||
| `N`, `O` | 13, 13 | KPI 타일 5 / 워치 표 |
|
||
| `P` | 1.5 | 거터 |
|
||
| `Q`, `R` | 13, 13 | KPI 타일 6 |
|
||
| `S` | 1.5 | 우 여백 |
|
||
|
||
총 폭 ≈ 1.5 + (13×12) + (1.5×6) + 1.5 = **168 문자폭 ≈ 1,270 px**. 줌 90% 적용 시 ≈ 1,143 px → **1366×768 노트북에서도 가로 스크롤 없음.**
|
||
|
||
**행 높이** (`set_row`)
|
||
|
||
| 행 | 높이 | 역할 |
|
||
|---|---|---|
|
||
| 1 | 6 | 상단 여백 |
|
||
| 2 | 30 | 제목 |
|
||
| 3 | 18 | 헤드라인 문장 |
|
||
| 4 | 8 | 여백 |
|
||
| 5 | 18 | KPI 라벨 |
|
||
| 6 | 34 | KPI 값 (28pt) |
|
||
| 7 | 16 | KPI 델타 |
|
||
| 8 | 14 | KPI 스파크라인 |
|
||
| 9 | 10 | 여백 |
|
||
| 10–24 | 20 (×15) | 차트 영역 (총 300 px) |
|
||
| 25 | 10 | 여백 |
|
||
| 26 | 18 | 표 헤더 |
|
||
| 27–40 | 16 (×14) | 표 본문(10행 + 여유) |
|
||
| 41 | 10 | 여백 |
|
||
| 42 | 20 | 내비게이션 |
|
||
| 43–47 | 8 | 여백 |
|
||
| 48 | 14 | 출처·면책 |
|
||
|
||
총 높이 ≈ 6+30+18+8+18+34+16+14+10+300+10+18+224+10+20+40+14 = **790 px**. 줌 90% ≈ 711 px → **한 화면(768px)에 거의 맞음.** 세로 스크롤 최소.
|
||
|
||
### 9.2 텍스트 와이어프레임
|
||
|
||
```
|
||
A B C D E F G H I J K L M N O P Q R S
|
||
┌───┬───────────────────┬──┬───────────────────┬──┬───────────────────┬──┬───────────────────┬──┬───────────────────┬──┬───────────────────┬───┐
|
||
1│ (상단 여백 h=6) │
|
||
2│ │ B2:R2 ▌DMF 일일 모니터링 리포트 — 2026-09-02(화) 18pt bold #1F2933 │ │
|
||
3│ │ B3:R3 오늘 신규 12건 · 변경 3건 · 취하 1건 — 워치리스트 2건 적중(피타바스타틴칼슘, 다파글리플로진) 11pt #5B6770 │ │
|
||
4│ (여백 h=8) │
|
||
5│ │ B5:C5 │ │ E5:F5 │ │ H5:I5 │ │ K5:L5 │ │ N5:O5 │ │ Q5:R5 │ │
|
||
│ │ 오늘 신규 │ │ 오늘 변경 │ │ 오늘 취하 │ │ 오늘 총 변경분 │ │ 워치리스트 히트 │ │ 전체 누적 등록 │ │
|
||
6│ │ B6:C6 ┏━━━━━┓ │ │ E6:F6 │ │ H6:I6 │ │ K6:L6 │ │ N6:O6 │ │ Q6:R6 │ │
|
||
│ │ 12 │ │ 3 │ │ 1 │ │ 16 │ │ 2 │ │ 9,084 │ │
|
||
│ │ 28pt #009E73 │ │ 28pt #E69F00 │ │ 28pt #D55E00 │ │ 28pt #0072B2 │ │ 28pt #CC79A7 │ │ 28pt #1F2933 │ │
|
||
7│ │ B7:C7 ▲ 5 │ │ E7:F7 ▼ -2 │ │ H7:I7 – 0 │ │ K7:L7 ▲ 3 │ │ N7:O7 ▲ 2 │ │ Q7:R7 ▲ 11 │ │
|
||
8│ │ B8:C8 ▁▂▅▁▃█▂▁ │ │ E8:F8 ▁▁▂▁▁▃▁▁ │ │ H8:I8 ▁▁▁▁█▁▁▁ │ │ K8:L8 ▂▃▅▂▄█▃▂ │ │ N8:O8 ▁▁█▁▁█▁▁ │ │ Q8:R8 ╱╱╱╱╱╱╱╱ │ │
|
||
│ │ sparkline column │ │ sparkline column │ │ sparkline column │ │ sparkline column │ │ sparkline win_loss│ │ sparkline line │ │
|
||
9│ (여백 h=10) │
|
||
10│ ┌──────────────────────────────────────────────────────────┐ ┌──────────────────────────────────────────────────────────┐ │
|
||
│ │ B10:I24 │ │ K10:R24 │ │
|
||
│ │ [차트1] 최근 30일 일별 등록 동향 (누적 세로 막대) │ │ [차트2] 제조국 Top 8 (가로 막대, 단색 #0072B2) │ │
|
||
│ │ x=일자(30), y=건수 │ │ x=건수, y=국가명 (직접 라벨링, 색 구분 없음) │ │
|
||
│ │ series: 신규 #009E73 / 변경 #E69F00 / 취하 #D55E00 │ │ 인도 ████████████████ 3,351 │ │
|
||
24│ │ insert_chart('B10', {'x_scale':1.65,'y_scale':1.05}) │ │ 중국 ███████████ 2,231 대한민국 ████ 845 … │ │
|
||
│ └──────────────────────────────────────────────────────────┘ └──────────────────────────────────────────────────────────┘ │
|
||
25│ (여백 h=10) │
|
||
26│ │ B26:F26 Top 10 성분 │ │ H26:L26 Top 10 업체 │ │ N26:R26 워치리스트 히트 │ │
|
||
│ │ 순위│성분명 │오늘│30일│누적 │ │ 순위│신청인 │오늘│30일│누적 │ │ 키워드│매칭│성분명│신청인│상태 │ │
|
||
27│ │ 1 │히알루론…│ 3 │ 11 │ 104 │ │ 1 │(주)삼오제약│ 4 │ 15 │ 392 │ │ 다파글…│ 1 │… │… │ 신규 │ │
|
||
40│ │ … (10행, 데이터바 #0072B2) │ │ … (10행) │ │ … (최대 10행) │ │
|
||
41│ (여백 h=10) │
|
||
42│ │ B42 [오늘 변경분] │ E42 [전체 누적] │ H42 [성분별] │ K42 [업체별] │ N42 [워치리스트] │ Q42 [스냅샷·로그 / 메타] │
|
||
43│ (여백 h=8 ×5) │
|
||
48│ │ B48:R48 출처: 의약품안전나라 DMF 공고(nedrug.mfds.go.kr/pbp/CCBAC03) · 공공데이터포털 OpenAPI(15057075) │ 수집 2026-09-02 06:00 KST │
|
||
└───┴───────────────────┴──┴───────────────────┴──┴───────────────────┴──┴───────────────────┴──┴───────────────────┴──┴───────────────────┴───┘
|
||
```
|
||
|
||
> **예시 수치 출처**: `전체 누적 등록`(9,084), 제조국 Top 표(인도 3,351 · 중국 2,231 · 대한민국 845), Top 10 성분 1위(히알루론산나트륨, 누적 104), Top 10 업체 1위((주)삼오제약, 누적 392)는 `design/00b-baseline-data-analysis.md` §7 실측값이다. 오늘 신규/변경/취하 등 **일별 델타는 diff 예시일 뿐 실측이 아니다**(00b 는 단일 시점 스냅샷이라 일별 값을 제공하지 않는다). 전체 국가 분포는 00b §7.3 참조.
|
||
|
||
### 9.3 F-패턴 검증
|
||
|
||
5초 규칙 문서의 F-패턴 요구와 대조:
|
||
|
||
| 요구 | 본 레이아웃 |
|
||
|---|---|
|
||
| 좌상단 = KPI Zone (3~4개 핵심) | `B5:R8` 최상단 가로 밴드. 좌측 3개(신규/변경/취하)가 핵심 |
|
||
| 중앙 = Trend Zone (라인 차트) | `B10:R24` 차트 밴드. 좌측이 시계열 |
|
||
| 우하단 = Detail Zone (표) | `B26:R40` 표 밴드, 우측이 워치리스트 상세 |
|
||
| 한 화면, 스크롤 없음 | ✅ 계산상 711 px @ 90% |
|
||
| 최대 6~8 KPI | ✅ 6개 |
|
||
| 색 3개 + 의미 고정 | ✅ 신규/변경/취하 3색 + 강조 `#0072B2` |
|
||
| 범례 읽지 않아도 됨 | ✅ 차트2 는 단색 + 직접 라벨링, 차트1 은 KPI 타일 색과 동일 매핑 |
|
||
|
||
### 9.4 KPI 타일의 셀 단위 구현 (병합 없이)
|
||
|
||
도형(shape) 대신 **테두리 + 배경 + 병합**으로 카드를 만든다.
|
||
|
||
```python
|
||
TILE_BG = "#F7F7F7"
|
||
BORDER = "#D0D7DE"
|
||
|
||
def tile_formats(wb, accent):
|
||
"""KPI 타일 4행(라벨/값/델타/스파크라인)용 서식 세트."""
|
||
common = {"font_name": "맑은 고딕", "bg_color": TILE_BG,
|
||
"align": "center", "valign": "vcenter",
|
||
"left": 1, "right": 1, "left_color": BORDER, "right_color": BORDER}
|
||
label = wb.add_format({**common, "font_size": 10, "bold": True,
|
||
"font_color": "#5B6770",
|
||
"top": 5, "top_color": accent}) # 상단 굵은 액센트 선
|
||
value = wb.add_format({**common, "font_size": 28, "bold": True,
|
||
"font_color": accent, "num_format": "#,##0"})
|
||
delta = wb.add_format({**common, "font_size": 10, "bold": True,
|
||
"font_color": "#5B6770",
|
||
"num_format": "▲ #,##0;▼ -#,##0;– 0"})
|
||
spark = wb.add_format({**common, "bottom": 1, "bottom_color": BORDER})
|
||
return label, value, delta, spark
|
||
|
||
TILES = [
|
||
("오늘 신규", "B", "C", "#009E73"),
|
||
("오늘 변경", "E", "F", "#E69F00"),
|
||
("오늘 취하", "H", "I", "#D55E00"),
|
||
("오늘 총 변경분", "K", "L", "#0072B2"),
|
||
("워치리스트 히트", "N", "O", "#CC79A7"),
|
||
("전체 누적 등록", "Q", "R", "#1F2933"),
|
||
]
|
||
|
||
def write_tiles(wb, ws, values, deltas, spark_ranges):
|
||
"""values/deltas: 길이 6 리스트. spark_ranges: 길이 6 의 '메타'!$AB$2:$AB$31 형태."""
|
||
for i, (label_text, c1, c2, accent) in enumerate(TILES):
|
||
f_label, f_value, f_delta, f_spark = tile_formats(wb, accent)
|
||
ws.merge_range(f"{c1}5:{c2}5", label_text, f_label)
|
||
ws.merge_range(f"{c1}6:{c2}6", values[i], f_value)
|
||
ws.merge_range(f"{c1}7:{c2}7", deltas[i], f_delta)
|
||
ws.merge_range(f"{c1}8:{c2}8", "", f_spark)
|
||
stype = "win_loss" if i == 4 else ("line" if i == 5 else "column")
|
||
ws.add_sparkline(f"{c1}8", {
|
||
"range": spark_ranges[i],
|
||
"type": stype,
|
||
"series_color": accent,
|
||
"high_point": True,
|
||
"last_point": True,
|
||
})
|
||
```
|
||
|
||
**`add_sparkline()` 옵션 전체**(XlsxWriter 문서 실측): `range`, `type`(`line` 기본 / `column` / `win_loss`), `markers`, `high_point`, `low_point`, `first_point`, `last_point`, `negative_points`, `axis`, `style`(1–6, 20), `series_color`(hex), `reverse`, `location`(그룹 스파크라인용 셀 배열).
|
||
|
||
문서 예제(원문 그대로):
|
||
|
||
```python
|
||
import xlsxwriter
|
||
|
||
workbook = xlsxwriter.Workbook("sparklines2.xlsx")
|
||
worksheet1 = workbook.add_worksheet()
|
||
worksheet2 = workbook.add_worksheet()
|
||
|
||
# Line sparkline with markers
|
||
worksheet1.add_sparkline("A6", {"range": "Sheet2!A1:J1", "markers": True})
|
||
|
||
# Column with custom color
|
||
worksheet1.add_sparkline(
|
||
"A18", {"range": "Sheet2!A2:J2", "type": "column", "series_color": "#E965E0"}
|
||
)
|
||
|
||
# Grouped sparklines
|
||
worksheet1.add_sparkline(
|
||
"A27",
|
||
{
|
||
"location": ["A27", "A28", "A29"],
|
||
"range": ["Sheet2!A5:J5", "Sheet2!A6:J6", "Sheet2!A7:J7"],
|
||
"markers": True,
|
||
},
|
||
)
|
||
|
||
workbook.close()
|
||
```
|
||
|
||
> **스파크라인 데이터 포인트 수**: 실무 권고는 **12–24개가 최적**, 100개 이상은 피하라. 우리는 **30개**를 쓰므로 상한에 가깝다 — `type: 'column'` 으로 개별 값을 구분 가능하게 하고, 라인은 누적 KPI 에만 쓴다. (검색 #37)
|
||
|
||
---
|
||
|
||
## 10. 피벗테이블 문제와 대안
|
||
|
||
### 10.1 문제 — 파이썬에서 피벗테이블을 만들 수 없다
|
||
|
||
**openpyxl 공식 문서 원문**(WebFetch 확인, `https://openpyxl.readthedocs.io/en/stable/pivot.html`):
|
||
|
||
> **Creation**: openpyxl cannot create new pivot tables. The docs state: *"it is not intended that client code should be able to create pivot tables."*
|
||
> **Existing Pivot Tables**: 읽기 지원 + 기존 피벗 편집 가능. *"edit and manipulate existing pivot tables, eg. change their ranges or whether they should update automatically settings."*
|
||
> **Access Method**: 관리 API 가 없으므로 워크시트의 `_pivots` 리스트를 직접 순회해야 한다.
|
||
|
||
예제 코드(문서 원문):
|
||
|
||
```python
|
||
from openpyxl import load_workbook
|
||
wb = load_workbook("campaign.xlsx")
|
||
ws = wb["Results"]
|
||
pivot = ws._pivots[0]
|
||
pivot.cache.refreshOnLoad = True
|
||
```
|
||
|
||
**XlsxWriter 도 마찬가지**: 차트 기능은 있으나 **피벗테이블을 처음부터 생성하는 기능은 없다**(검색 #3).
|
||
|
||
근본 원인: 피벗테이블은 `pivotCacheDefinition`, `pivotCacheRecords`, `pivotTableDefinition` 3종 XML 파트와 relationship 을 모두 정합적으로 써야 하는데, 어느 라이브러리도 이 캐시를 생성하는 API 를 제공하지 않는다.
|
||
|
||
### 10.2 대안 3가지 비교
|
||
|
||
| # | 방식 | 장점 | 단점 | 채택 |
|
||
|---|---|---|---|---|
|
||
| **A** | **pandas 집계 → 정적 표 + `add_table()` + `add_chart()`** | 의존성 최소, 크로스플랫폼, 파일 안정, Excel 미설치 PC 에서도 생성 가능, 자동필터·정렬·합계행 모두 제공 | 사용자가 축을 바꿔 드릴다운 불가 | ✅ **기본 채택** |
|
||
| **B** | **피벗이 미리 들어있는 템플릿 xlsx + openpyxl 로 데이터 시트만 교체 + `pivot.cache.refreshOnLoad = True`** | 진짜 피벗(사용자가 축 변경 가능), Excel 미설치 PC 에서도 생성 가능 | 템플릿을 수동으로 1회 제작·유지해야 함. 데이터 행 수가 템플릿 범위를 넘으면 범위 재설정 필요 | 🔶 **2단계 옵션** |
|
||
| **C** | `pywin32` / `xlwings` 로 Excel COM 을 띄워 실제 PivotTable 생성 | 완전 자유 | **Excel 설치 필수**, 06:00 무인 실행에서 COM 이 자주 실패, 좀비 EXCEL.EXE 누수, 세션 0 문제 | ❌ **비채택** (무인 스케줄러 환경과 상극) |
|
||
|
||
### 10.3 방식 A 구현 — 정적 피벗 (채택안)
|
||
|
||
```python
|
||
import pandas as pd
|
||
|
||
def build_ingredient_pivot(df_all: pd.DataFrame, df_today: pd.DataFrame,
|
||
df_30d: pd.DataFrame) -> pd.DataFrame:
|
||
"""성분별 시트용 집계표. pandas 로 '피벗'을 만들고 정적 표로 내보낸다."""
|
||
today = (df_today
|
||
.pivot_table(index="성분명", columns="상태", values="등록번호",
|
||
aggfunc="count", fill_value=0))
|
||
for col in ("신규", "변경", "취하"):
|
||
if col not in today.columns:
|
||
today[col] = 0
|
||
today = today[["신규", "변경", "취하"]]
|
||
today.columns = ["오늘 신규", "오늘 변경", "오늘 취하"]
|
||
today["오늘 합계"] = today.sum(axis=1)
|
||
|
||
d30 = df_30d.groupby("성분명")["등록번호"].count().rename("최근 30일")
|
||
d7 = (df_30d[df_30d["일자"] >= df_30d["일자"].max() - pd.Timedelta(days=6)]
|
||
.groupby("성분명")["등록번호"].count().rename("최근 7일"))
|
||
|
||
# 정정(§8.0): API 는 애초에 정상 건만 반환하므로 별도 '취소/취하구분' 필터가 필요 없다.
|
||
cum = (df_all
|
||
.groupby("성분명")
|
||
.agg(**{"누적 유효등록": ("등록번호", "count"),
|
||
"등록업체 수": ("정규화업체키", "nunique"),
|
||
"제조국 수": ("제조국가명", "nunique")}))
|
||
|
||
main_country = (df_all.groupby(["성분명", "제조국가"])["등록번호"].count()
|
||
.reset_index()
|
||
.sort_values("등록번호", ascending=False)
|
||
.drop_duplicates("성분명")
|
||
.set_index("성분명")["제조국가"].rename("주요 제조국"))
|
||
|
||
has_kr = (df_all.assign(_kr=df_all["제조국가"].isin(["대한민국", "한국", "국내"]))
|
||
.groupby("성분명")["_kr"].any()
|
||
.map({True: "Y", False: "N"}).rename("국산 보유"))
|
||
|
||
out = (cum.join([today, d7, d30, main_country, has_kr], how="left")
|
||
.fillna({"오늘 신규": 0, "오늘 변경": 0, "오늘 취하": 0,
|
||
"오늘 합계": 0, "최근 7일": 0, "최근 30일": 0,
|
||
"주요 제조국": "-", "국산 보유": "N"})
|
||
.astype({"오늘 신규": int, "오늘 변경": int, "오늘 취하": int,
|
||
"오늘 합계": int, "최근 7일": int, "최근 30일": int})
|
||
.sort_values(["오늘 합계", "누적 유효등록"], ascending=False)
|
||
.reset_index())
|
||
out.insert(0, "순위", range(1, len(out) + 1))
|
||
return out[["순위", "성분명", "오늘 신규", "오늘 변경", "오늘 취하", "오늘 합계",
|
||
"최근 7일", "최근 30일", "누적 유효등록", "등록업체 수", "제조국 수",
|
||
"주요 제조국", "국산 보유"]]
|
||
```
|
||
|
||
pandas → XlsxWriter 표 변환(문서 원문 패턴):
|
||
|
||
```python
|
||
# 헤더 없이 1행 아래부터 쓰고, add_table 로 헤더를 만든다
|
||
df.to_excel(writer, sheet_name="성분별", startrow=2, header=False, index=False)
|
||
|
||
column_settings = [{"header": c} for c in df.columns]
|
||
(max_row, max_col) = df.shape
|
||
worksheet.add_table(1, 0, max_row + 1, max_col - 1, {
|
||
"columns": column_settings,
|
||
"style": "Table Style Light 11",
|
||
"name": "TblIngredient",
|
||
})
|
||
```
|
||
|
||
### 10.4 방식 B 구현 — 템플릿 피벗 + 캐시 갱신 (2단계 옵션)
|
||
|
||
1. 사람이 Excel 로 `templates/dmf_pivot_template.xlsx` 를 1회 제작:
|
||
- `RawData` 시트에 헤더만 있는 **테이블(`Table1`)** 을 만들고,
|
||
- `성분피벗`·`업체피벗` 시트에 `Table1` 을 소스로 하는 피벗테이블을 배치.
|
||
2. 파이썬은 매일 `RawData` 시트를 채우고 테이블 `ref` 를 늘린 뒤 캐시를 갱신 표시:
|
||
|
||
```python
|
||
from openpyxl import load_workbook
|
||
from openpyxl.utils import get_column_letter
|
||
|
||
def refresh_template_pivots(template_path, out_path, df):
|
||
wb = load_workbook(template_path)
|
||
ws = wb["RawData"]
|
||
|
||
# 기존 데이터 영역 비우기(헤더 1행 유지)
|
||
if ws.max_row > 1:
|
||
ws.delete_rows(2, ws.max_row - 1)
|
||
for row in df.itertuples(index=False):
|
||
ws.append(list(row))
|
||
|
||
# 테이블 ref 확장
|
||
last = f"{get_column_letter(df.shape[1])}{df.shape[0] + 1}"
|
||
for tbl in ws.tables.values():
|
||
tbl.ref = f"A1:{last}"
|
||
|
||
# 모든 피벗을 열 때 새로고침하도록 표시 (openpyxl 공식 예제 패턴)
|
||
for sheet in wb.worksheets:
|
||
for pivot in getattr(sheet, "_pivots", []):
|
||
pivot.cache.refreshOnLoad = True
|
||
|
||
wb.save(out_path)
|
||
```
|
||
|
||
> **한계**: 피벗 자체는 파이썬이 계산하지 않으므로, **사용자가 파일을 Excel 로 열기 전까지 피벗 셀은 이전 값**이다. 따라서 `대시보드`의 KPI·차트는 절대 피벗에 의존시키지 않는다(방식 A 로 계산된 정적 값만 사용).
|
||
|
||
### 10.5 차트로 피벗을 대체하기 (XlsxWriter)
|
||
|
||
pandas 데이터프레임 범위를 직접 차트 시리즈로 지정한다(문서 원문 패턴):
|
||
|
||
```python
|
||
chart = workbook.add_chart({"type": "column"})
|
||
(max_row, max_col) = df.shape
|
||
chart.add_series({"values": ["Sheet1", 1, 1, max_row, 1]})
|
||
worksheet.insert_chart(1, 3, chart)
|
||
```
|
||
|
||
시리즈 참조는 두 방식 모두 가능:
|
||
- 문자열: `"categories": "=Sheet1!$A$2:$A$7"`, `"values": "=Sheet1!$B$2:$B$7"`
|
||
- 배열: `["Sheet1", first_row, first_col, last_row, last_col]` (0-based)
|
||
|
||
완전한 컬럼 차트 예제(XlsxWriter 문서 원문):
|
||
|
||
```python
|
||
import xlsxwriter
|
||
|
||
workbook = xlsxwriter.Workbook("chart_column.xlsx")
|
||
worksheet = workbook.add_worksheet()
|
||
bold = workbook.add_format({"bold": 1})
|
||
|
||
headings = ["Number", "Batch 1", "Batch 2"]
|
||
data = [
|
||
[2, 3, 4, 5, 6, 7],
|
||
[10, 40, 50, 20, 10, 50],
|
||
[30, 60, 70, 50, 40, 30],
|
||
]
|
||
|
||
worksheet.write_row("A1", headings, bold)
|
||
worksheet.write_column("A2", data[0])
|
||
worksheet.write_column("B2", data[1])
|
||
worksheet.write_column("C2", data[2])
|
||
|
||
# Default column chart
|
||
chart1 = workbook.add_chart({"type": "column"})
|
||
chart1.add_series({
|
||
"name": "=Sheet1!$B$1",
|
||
"categories": "=Sheet1!$A$2:$A$7",
|
||
"values": "=Sheet1!$B$2:$B$7",
|
||
})
|
||
chart1.add_series({
|
||
"name": ["Sheet1", 0, 2],
|
||
"categories": ["Sheet1", 1, 0, 6, 0],
|
||
"values": ["Sheet1", 1, 2, 6, 2],
|
||
})
|
||
chart1.set_title({"name": "Results of sample analysis"})
|
||
chart1.set_x_axis({"name": "Test number"})
|
||
chart1.set_y_axis({"name": "Sample length (mm)"})
|
||
chart1.set_style(11)
|
||
worksheet.insert_chart("D2", chart1, {"x_offset": 25, "y_offset": 10})
|
||
|
||
# Stacked column chart
|
||
chart2 = workbook.add_chart({"type": "column", "subtype": "stacked"})
|
||
chart2.add_series({
|
||
"name": "=Sheet1!$B$1",
|
||
"categories": "=Sheet1!$A$2:$A$7",
|
||
"values": "=Sheet1!$B$2:$B$7",
|
||
})
|
||
chart2.add_series({
|
||
"name": "=Sheet1!$C$1",
|
||
"categories": "=Sheet1!$A$2:$A$7",
|
||
"values": "=Sheet1!$C$2:$C$7",
|
||
})
|
||
chart2.set_title({"name": "Stacked Chart"})
|
||
chart2.set_x_axis({"name": "Test number"})
|
||
chart2.set_y_axis({"name": "Sample length (mm)"})
|
||
chart2.set_style(12)
|
||
worksheet.insert_chart("D18", chart2, {"x_offset": 25, "y_offset": 10})
|
||
|
||
# Percentage stacked column chart
|
||
chart3 = workbook.add_chart({"type": "column", "subtype": "percent_stacked"})
|
||
chart3.add_series({
|
||
"name": "=Sheet1!$B$1",
|
||
"categories": "=Sheet1!$A$2:$A$7",
|
||
"values": "=Sheet1!$B$2:$B$7",
|
||
})
|
||
chart3.add_series({
|
||
"name": "=Sheet1!$C$1",
|
||
"categories": "=Sheet1!$A$2:$A$7",
|
||
"values": "=Sheet1!$C$2:$C$7",
|
||
})
|
||
chart3.set_title({"name": "Percent Stacked Chart"})
|
||
chart3.set_x_axis({"name": "Test number"})
|
||
chart3.set_y_axis({"name": "Sample length (mm)"})
|
||
chart3.set_style(13)
|
||
worksheet.insert_chart("D34", chart3, {"x_offset": 25, "y_offset": 10})
|
||
|
||
workbook.close()
|
||
```
|
||
|
||
크기 조정: `chart.set_size({'x_scale': 2, 'y_scale': 2})` 또는 `insert_chart(..., {'x_scale':…, 'y_scale':…})`.
|
||
|
||
> **본 프로젝트 주의**: `set_style(11)` 같은 내장 스타일은 **엑셀 기본 팔레트를 강제**하므로 쓰지 않는다. 대신 `add_series` 마다 `{'fill': {'color': '#009E73'}}` 로 §6 팔레트를 직접 지정한다.
|
||
|
||
---
|
||
|
||
## 11. 구현 레시피: 동작하는 코드
|
||
|
||
### 11.1 환경 (실측 확인)
|
||
|
||
```
|
||
xlsxwriter 3.2.1 openpyxl 3.1.5 pandas 2.2.3
|
||
```
|
||
|
||
PDF 파싱 보조 라이브러리도 확인됨: `pypdf 4.3.1`, `pdfplumber`, `pymupdf(fitz)`.
|
||
|
||
### 11.2 워크북 뼈대
|
||
|
||
```python
|
||
import pandas as pd
|
||
import xlsxwriter
|
||
|
||
SHEETS = [
|
||
("대시보드", "#0072B2"),
|
||
("오늘 변경분", "#D55E00"),
|
||
("전체 누적", "#5B6770"),
|
||
("성분별", "#009E73"),
|
||
("업체별", "#009E73"),
|
||
("워치리스트", "#CC79A7"),
|
||
("스냅샷·로그", "#888888"),
|
||
("메타", "#888888"),
|
||
]
|
||
|
||
def build_workbook(path, ctx):
|
||
# constant_memory 는 절대 켜지 말 것 (add_table 불가)
|
||
wb = xlsxwriter.Workbook(path, {"default_date_format": "yyyy-mm-dd",
|
||
"constant_memory": False})
|
||
wb.set_size(1600, 900)
|
||
ws = {}
|
||
for name, tab in SHEETS:
|
||
ws[name] = wb.add_worksheet(name)
|
||
ws[name].set_tab_color(tab)
|
||
|
||
dash = ws["대시보드"]
|
||
dash.hide_gridlines(2) # 화면 + 인쇄 모두 숨김
|
||
dash.set_zoom(90)
|
||
dash.activate() # 파일 열면 대시보드가 먼저 보이게
|
||
dash.set_landscape()
|
||
dash.fit_to_pages(1, 1)
|
||
|
||
for name, _ in SHEETS[1:]:
|
||
ws[name].freeze_panes(2, 2)
|
||
ws[name].set_landscape()
|
||
ws[name].fit_to_pages(1, 0)
|
||
ws[name].repeat_rows(0, 1)
|
||
# 역링크
|
||
ws[name].write_url("A1", "internal:'대시보드'!A1",
|
||
string="← 대시보드", tip="대시보드로 이동")
|
||
return wb, ws
|
||
```
|
||
|
||
**메서드 시그니처 참조**(XlsxWriter `worksheet.html` 실측):
|
||
|
||
| 메서드 | 시그니처 | 예 |
|
||
|---|---|---|
|
||
| `freeze_panes` | `freeze_panes(row, col[, top_row, left_col])` | `worksheet.freeze_panes(1, 0)` |
|
||
| `autofilter` | `autofilter(first_row, first_col, last_row, last_col)` | `worksheet.autofilter('A1:D10')` |
|
||
| `set_column` | `set_column(first_col, last_col, width[, cell_format, options])` | `worksheet.set_column('A:A', 20)` / `worksheet.set_column('D:D', 20, None, {'hidden': 1})` / `worksheet.set_column('B:G', None, None, {'level': 1})` |
|
||
| `set_tab_color` | `set_tab_color(color)` | `worksheet.set_tab_color('#FF0000')` |
|
||
| `activate` | `activate()` | `worksheet3.activate()` |
|
||
| `hide_gridlines` | `hide_gridlines([option])` | `worksheet.hide_gridlines()` |
|
||
| `set_zoom` | `set_zoom(scale)` | `worksheet.set_zoom(150)` |
|
||
| `set_row` | `set_row(row, height[, cell_format, options])` | `worksheet.set_row(0, 20)` |
|
||
| `write_url` | `write_url(row, col, url[, cell_format, string, tip])` | `worksheet.write_url('A1', 'internal:Sheet2!A1')` |
|
||
| `data_validation` | `data_validation(first_row, first_col, last_row, last_col, options)` | `worksheet.data_validation('B13', {'validate': 'list', 'source': ['open', 'high', 'close']})` |
|
||
|
||
### 11.3 pandas → 서식 있는 시트 (문서 원문 패턴)
|
||
|
||
```python
|
||
import pandas as pd
|
||
|
||
df = pd.DataFrame({"Data": [10, 20, 30, 20, 15, 30, 45]})
|
||
writer = pd.ExcelWriter("pandas_simple.xlsx", engine="xlsxwriter")
|
||
df.to_excel(writer, sheet_name="Sheet1")
|
||
|
||
workbook = writer.book
|
||
worksheet = writer.sheets["Sheet1"]
|
||
|
||
format1 = workbook.add_format({"num_format": "#,##0.00"})
|
||
format2 = workbook.add_format({"num_format": "0%"})
|
||
worksheet.set_column(1, 1, 18, format1) # 폭 + 서식
|
||
worksheet.set_column(2, 2, None, format2) # 서식만
|
||
```
|
||
|
||
`to_excel()` 은 `freeze_panes` 파라미터를 지원한다: `freeze_panes=(1, 1)` = 첫 행 + 첫 열 고정. 자동필터는 `ws.autofilter(0, 0, df.shape[0] - 1, df.shape[1] - 1)`.
|
||
|
||
### 11.4 전일 대비 diff — 스냅샷 비교
|
||
|
||
접근법 출처: [matthewkudija.com — Excel Diff](https://matthewkudija.com/blog/2018/07/21/excel-diff/) (WebFetch 확인). 원문은 셀 단위 `A-->B` 마킹 후 텍스트 조건부서식으로 강조하는 방식:
|
||
|
||
```python
|
||
import pandas as pd
|
||
|
||
df_OLD = pd.read_excel(path_OLD).fillna(0)
|
||
df_NEW = pd.read_excel(path_NEW).fillna(0)
|
||
|
||
dfDiff = df_OLD.copy()
|
||
for row in range(dfDiff.shape[0]):
|
||
for col in range(dfDiff.shape[1]):
|
||
value_OLD = df_OLD.iloc[row,col]
|
||
try:
|
||
value_NEW = df_NEW.iloc[row,col]
|
||
if value_OLD==value_NEW:
|
||
dfDiff.iloc[row,col] = df_NEW.iloc[row,col]
|
||
else:
|
||
dfDiff.iloc[row,col] = ('{}-->{}').format(value_OLD,value_NEW)
|
||
except:
|
||
dfDiff.iloc[row,col] = ('{}-->{}').format(value_OLD, 'NaN')
|
||
```
|
||
|
||
강조 서식(원문):
|
||
|
||
```python
|
||
writer = pd.ExcelWriter(fname, engine='xlsxwriter')
|
||
workbook = writer.book
|
||
worksheet = writer.sheets['DIFF']
|
||
|
||
highlight_fmt = workbook.add_format({'font_color': '#FF0000', 'bg_color':'#B1B3B3'})
|
||
grey_fmt = workbook.add_format({'font_color': '#E0E0E0'})
|
||
|
||
worksheet.conditional_format('A1:ZZ1000', {'type': 'text',
|
||
'criteria': 'containing',
|
||
'value':'→',
|
||
'format': highlight_fmt})
|
||
```
|
||
|
||
2019 업데이트판은 **위치 인덱싱 대신 `df.loc[]` + 인덱스 컬럼**으로 행 추가/삭제를 추적하며, **변경=빨강 / 신규행=초록 / 삭제행=회색**으로 색을 나눈다.
|
||
|
||
> **본 프로젝트 변형 (중요)**: 위 원문은 ① O(rows×cols) 파이썬 루프라 수만 행에서 느리고, ② **red/green 조합이라 CUD 위반**, ③ 셀 안에 `A-->B` 를 넣으면 정렬·필터가 깨진다.
|
||
> → **키 기반 머지 + 행 해시**로 바꾸고, 변경 내용은 **별도 `변경내용` 컬럼**에 담는다.
|
||
|
||
> **정정**(§8.0 반영): 아래 `DIFF_FIELDS` 는 원래 nedrug 원본 13필드로 짜여 있었다. API 7필드 설계에서는 이 중 실제로 diff 가능한 필드만 남는다 — **`발급일자`를 반드시 포함**해야 한다(내용이 같아도 `발급일자`만 갱신되는 재확인 건을 "변경"으로 잡기 위해, `00b` §6).
|
||
|
||
```python
|
||
import hashlib
|
||
import pandas as pd
|
||
|
||
DIFF_FIELDS = ["성분명", "업체명", "제조소명", "제조소소재지", "제조국가명", "발급일자"]
|
||
|
||
def row_hash(row) -> str:
|
||
raw = "\x1f".join(str(row.get(f, "")) for f in DIFF_FIELDS)
|
||
return hashlib.sha1(raw.encode("utf-8")).hexdigest()[:12]
|
||
|
||
def diff_snapshots(prev: pd.DataFrame, curr: pd.DataFrame) -> pd.DataFrame:
|
||
"""등록번호를 키로 신규/변경/취하를 판정하고 변경 상세를 문자열로 만든다."""
|
||
prev = prev.set_index("등록번호", drop=False)
|
||
curr = curr.set_index("등록번호", drop=False)
|
||
prev["행해시"] = prev.apply(row_hash, axis=1)
|
||
curr["행해시"] = curr.apply(row_hash, axis=1)
|
||
|
||
new_keys = curr.index.difference(prev.index)
|
||
gone_keys = prev.index.difference(curr.index)
|
||
common_keys = curr.index.intersection(prev.index)
|
||
changed_keys = [k for k in common_keys
|
||
if curr.at[k, "행해시"] != prev.at[k, "행해시"]]
|
||
|
||
rows = []
|
||
for k in new_keys:
|
||
r = curr.loc[k].to_dict()
|
||
r.update({"상태": "신규", "기호": "+", "정렬키": 2,
|
||
"변경필드": "-", "변경내용(before→after)": "-"})
|
||
rows.append(r)
|
||
|
||
for k in changed_keys:
|
||
r = curr.loc[k].to_dict()
|
||
fields, details = [], []
|
||
for f in DIFF_FIELDS:
|
||
a, b = prev.at[k, f], curr.at[k, f]
|
||
if str(a) != str(b):
|
||
fields.append(f)
|
||
details.append(f"{f}: {a} → {b}")
|
||
# 정정(§8.0): 취하는 gone_keys(레코드 소멸)로만 판정한다.
|
||
# API 에는 '취소/취하구분' 필드 자체가 없으므로 여기서 승격 판정을 하지 않는다.
|
||
r.update({
|
||
"상태": "변경",
|
||
"기호": "◆",
|
||
"정렬키": 3,
|
||
"변경필드": ", ".join(fields),
|
||
"변경내용(before→after)": " | ".join(details),
|
||
})
|
||
rows.append(r)
|
||
|
||
for k in gone_keys: # 목록에서 사라진 경우 = 삭제/취하 의심
|
||
r = prev.loc[k].to_dict()
|
||
r.update({"상태": "취하", "기호": "✕", "정렬키": 1,
|
||
"변경필드": "목록에서 제거",
|
||
"변경내용(before→after)": "이전 스냅샷에 있었으나 오늘 목록에 없음"})
|
||
rows.append(r)
|
||
|
||
out = pd.DataFrame(rows)
|
||
if out.empty:
|
||
return out
|
||
return out.sort_values(["정렬키", "워치리스트히트", "성분명"],
|
||
ascending=[True, False, True],
|
||
na_position="last").reset_index(drop=True)
|
||
```
|
||
|
||
`전체 누적` 시트에서 "오늘 변경된 행"을 강조하는 조건부서식(전일 대비 diff 강조):
|
||
|
||
```python
|
||
today_fmt = workbook.add_format({"bg_color": "#FDF0DC", "font_color": "#7A3E00"})
|
||
ws_all.conditional_format("A3:Z50000", {
|
||
"type": "formula",
|
||
"criteria": '=$I3=TODAY()', # I = 발급일자(§8.4) — 최종 갱신일로 동작(00b §6)
|
||
"format": today_fmt,
|
||
})
|
||
```
|
||
|
||
중복 등록번호(재공고) 탐지 — XlsxWriter `duplicate` 규칙(문서 원문):
|
||
|
||
```python
|
||
worksheet.conditional_format('A1:F10', {'type': 'duplicate',
|
||
'format': format1})
|
||
```
|
||
|
||
문자열 규칙(문서 원문). `criteria` 옵션은 `'containing'`, `'not containing'`, `'begins with'`, `'ends with'`:
|
||
|
||
```python
|
||
worksheet.conditional_format('A1:A4', {'type': 'text',
|
||
'criteria': 'containing',
|
||
'value': 'foo',
|
||
'format': format1})
|
||
```
|
||
|
||
수식 규칙(문서 원문):
|
||
|
||
```python
|
||
worksheet.conditional_format('A1:A4', {'type': 'formula',
|
||
'criteria': '=$A$1>5',
|
||
'format': format1})
|
||
```
|
||
|
||
`cell` 타입 예제(문서 실측 — 색상값 원문): 포맷 `{"bg_color": "#FFC7CE", "font_color": "#9C0006"}` / `{"bg_color": "#C6EFCE", "font_color": "#006100"}`, 범위 `"B3:K12"`, 기준 `">="`/`"<"` 50, `"between"`(30~70)/`"not between"`, 비연속 범위는 `"multi_range": "B3:K6 B9:K12"`.
|
||
→ **이 기본 색쌍은 red/green 이라 본 프로젝트에서는 §6.6 팔레트로 대체한다.**
|
||
|
||
### 11.5 워치리스트 매칭
|
||
|
||
```python
|
||
import re
|
||
import unicodedata
|
||
|
||
CORP_NOISE = re.compile(r"\(주\)|㈜|주식회사|\bco\.,?\s*ltd\.?|\binc\.?|\bltd\.?|\bcorp\.?|\s+")
|
||
|
||
def norm_key(s: str) -> str:
|
||
s = unicodedata.normalize("NFKC", str(s or "")).lower()
|
||
return CORP_NOISE.sub("", s)
|
||
|
||
def match_watchlist(df: pd.DataFrame, watch: pd.DataFrame) -> pd.DataFrame:
|
||
"""df 에 워치리스트히트/워치키워드 컬럼을 채워 반환."""
|
||
field_map = {"성분명": "성분명", "신청인": "업체명",
|
||
"제조소명": "제조소명", "제조국가": "제조국가명",
|
||
"등록번호": "등록번호"}
|
||
hits = [0] * len(df)
|
||
kws = [[] for _ in range(len(df))]
|
||
|
||
active = watch[watch["활성"].astype(str).str.upper() == "Y"]
|
||
for _, w in active.iterrows():
|
||
col = field_map.get(str(w["대상유형"]).strip())
|
||
if col is None or col not in df.columns:
|
||
continue
|
||
kw, mode = str(w["키워드"]).strip(), str(w["매칭방식"]).strip()
|
||
if not kw:
|
||
continue
|
||
series = df[col].astype(str)
|
||
if mode == "정확일치":
|
||
mask = series.map(norm_key) == norm_key(kw)
|
||
elif mode == "정규식":
|
||
try:
|
||
mask = series.str.contains(kw, regex=True, na=False)
|
||
except re.error:
|
||
continue # 잘못된 정규식은 조용히 건너뛰고 메타에 기록
|
||
else: # 부분일치(기본)
|
||
nk = norm_key(kw)
|
||
mask = series.map(norm_key).str.contains(re.escape(nk), na=False)
|
||
for i in df.index[mask]:
|
||
pos = df.index.get_loc(i)
|
||
hits[pos] += 1
|
||
kws[pos].append(kw)
|
||
|
||
df = df.copy()
|
||
df["워치리스트히트"] = hits
|
||
df["워치키워드"] = [", ".join(k) if k else "" for k in kws]
|
||
return df
|
||
```
|
||
|
||
**워치리스트 이월** — 직전 xlsx 에서 사용자 입력 영역만 읽어온다:
|
||
|
||
```python
|
||
from openpyxl import load_workbook
|
||
|
||
def carry_over_watchlist(prev_xlsx_path):
|
||
"""직전 리포트의 워치리스트 A:G 를 DataFrame 으로 회수. 없으면 빈 DF."""
|
||
cols = ["번호", "대상유형", "키워드", "매칭방식", "우선순위", "활성", "메모"]
|
||
try:
|
||
wb = load_workbook(prev_xlsx_path, read_only=True, data_only=True)
|
||
ws = wb["워치리스트"]
|
||
except Exception:
|
||
return pd.DataFrame(columns=cols)
|
||
rows = []
|
||
for r in ws.iter_rows(min_row=3, min_col=1, max_col=7, values_only=True):
|
||
if r[2] in (None, ""): # 키워드가 비면 종료
|
||
continue
|
||
rows.append(dict(zip(cols, r)))
|
||
return pd.DataFrame(rows, columns=cols)
|
||
```
|
||
|
||
**데이터 유효성 검사(드롭다운)**:
|
||
|
||
```python
|
||
ws_watch.data_validation(2, 1, 500, 1,
|
||
{"validate": "list", "source": ["성분명", "신청인", "제조소명", "제조국가", "등록번호"]})
|
||
ws_watch.data_validation(2, 3, 500, 3,
|
||
{"validate": "list", "source": ["부분일치", "정확일치", "정규식"]})
|
||
ws_watch.data_validation(2, 4, 500, 4,
|
||
{"validate": "list", "source": [1, 2, 3]})
|
||
ws_watch.data_validation(2, 5, 500, 5,
|
||
{"validate": "list", "source": ["Y", "N"]})
|
||
```
|
||
|
||
### 11.6 대시보드 차트 2종 (팔레트 고정)
|
||
|
||
```python
|
||
STATUS_COLORS = {"신규": "#009E73", "변경": "#E69F00", "취하": "#D55E00"}
|
||
|
||
def add_trend_chart(wb, dash, meta_sheet="메타", n=30):
|
||
ch = wb.add_chart({"type": "column", "subtype": "stacked"})
|
||
for i, (label, color) in enumerate(STATUS_COLORS.items()):
|
||
col = 27 + i # AB=27, AC=28, AD=29 (0-based)
|
||
ch.add_series({
|
||
"name": label,
|
||
"categories": [meta_sheet, 1, 26, n, 26], # AA2:AA31 = 날짜
|
||
"values": [meta_sheet, 1, col, n, col],
|
||
"fill": {"color": color},
|
||
"border": {"none": True},
|
||
"gap": 40,
|
||
})
|
||
ch.set_title({"name": "최근 30일 일별 등록 동향",
|
||
"name_font": {"name": "맑은 고딕", "size": 11, "bold": True,
|
||
"color": "#1F2933"}})
|
||
ch.set_x_axis({"num_font": {"name": "맑은 고딕", "size": 8, "color": "#5B6770"},
|
||
"line": {"color": "#D0D7DE"}, "major_tick_mark": "none"})
|
||
ch.set_y_axis({"num_font": {"name": "맑은 고딕", "size": 8, "color": "#5B6770"},
|
||
"major_gridlines": {"visible": True,
|
||
"line": {"color": "#EFEFEF", "width": 0.75}},
|
||
"line": {"none": True}, "major_tick_mark": "none"})
|
||
ch.set_legend({"position": "top", "font": {"name": "맑은 고딕", "size": 9}})
|
||
ch.set_chartarea({"border": {"color": "#D0D7DE"}, "fill": {"color": "#FFFFFF"}})
|
||
ch.set_plotarea({"border": {"none": True}, "fill": {"none": True}})
|
||
dash.insert_chart("B10", ch, {"x_scale": 1.65, "y_scale": 1.05})
|
||
|
||
def add_country_chart(wb, dash, meta_sheet="메타", n=8):
|
||
ch = wb.add_chart({"type": "bar"})
|
||
ch.add_series({
|
||
"name": "건수",
|
||
"categories": [meta_sheet, 1, 33, n, 33], # AH2:AH9 = 국가명
|
||
"values": [meta_sheet, 1, 34, n, 34], # AI2:AI9 = 건수
|
||
"fill": {"color": "#0072B2"}, # 단색: 카테고리 8개는 색 구분 금지
|
||
"border": {"none": True},
|
||
"data_labels": {"value": True, "position": "outside_end",
|
||
"font": {"name": "맑은 고딕", "size": 8, "color": "#1F2933"}},
|
||
"gap": 50,
|
||
})
|
||
ch.set_title({"name": "제조국 Top 8 (최근 30일)",
|
||
"name_font": {"name": "맑은 고딕", "size": 11, "bold": True,
|
||
"color": "#1F2933"}})
|
||
ch.set_x_axis({"visible": False}) # 데이터 라벨이 있으므로 축 제거(데이터 잉크)
|
||
ch.set_y_axis({"num_font": {"name": "맑은 고딕", "size": 9, "color": "#1F2933"},
|
||
"line": {"color": "#D0D7DE"}, "major_tick_mark": "none",
|
||
"reverse": True}) # 1위가 위로
|
||
ch.set_legend({"none": True})
|
||
ch.set_chartarea({"border": {"color": "#D0D7DE"}, "fill": {"color": "#FFFFFF"}})
|
||
ch.set_plotarea({"border": {"none": True}, "fill": {"none": True}})
|
||
dash.insert_chart("K10", ch, {"x_scale": 1.65, "y_scale": 1.05})
|
||
```
|
||
|
||
### 11.7 openpyxl 조건부서식 (템플릿 경로를 쓸 때만)
|
||
|
||
openpyxl 문서 원문 예제:
|
||
|
||
```python
|
||
# FormulaRule
|
||
ws.conditional_formatting.add('E1:E10',
|
||
FormulaRule(formula=['ISBLANK(E1)'], stopIfTrue=True, fill=redFill))
|
||
|
||
ws.conditional_formatting.add('E1:E10',
|
||
FormulaRule(formula=['E1=0'], font=myFont, border=myBorder, fill=redFill))
|
||
|
||
# CellIsRule
|
||
ws.conditional_formatting.add('C2:C10',
|
||
CellIsRule(operator='lessThan', formula=['C$1'], stopIfTrue=True, fill=redFill))
|
||
|
||
ws.conditional_formatting.add('D2:D10',
|
||
CellIsRule(operator='between', formula=['1','5'], stopIfTrue=True, fill=redFill))
|
||
|
||
# ColorScaleRule
|
||
rule = ColorScaleRule(start_type='percentile', start_value=10, start_color='FFAA0000',
|
||
mid_type='percentile', mid_value=50, mid_color='FF0000AA',
|
||
end_type='percentile', end_value=90, end_color='FF00AA00')
|
||
|
||
# IconSetRule
|
||
rule = IconSetRule('5Arrows', 'percent', [10, 20, 30, 40, 50],
|
||
showValue=None, percent=None, reverse=None)
|
||
|
||
# DataBarRule
|
||
rule = DataBarRule(start_type='percentile', start_value=10,
|
||
end_type='percentile', end_value='90',
|
||
color="FF638EC6", showValue="None", minLength=None, maxLength=None)
|
||
|
||
# 문자열 규칙 (containsText)
|
||
rule = Rule(type="containsText", operator="containsText", text="highlight", dxf=dxf)
|
||
rule.formula = ['NOT(ISERROR(SEARCH("highlight",A1)))']
|
||
```
|
||
|
||
openpyxl `Table` + `TableStyleInfo` 예제(문서 원문):
|
||
|
||
```python
|
||
from openpyxl import Workbook
|
||
from openpyxl.worksheet.table import Table, TableStyleInfo
|
||
|
||
wb = Workbook()
|
||
ws = wb.active
|
||
|
||
data = [
|
||
['Apples', 10000, 5000, 8000, 6000],
|
||
['Pears', 2000, 3000, 4000, 5000],
|
||
['Bananas', 6000, 6000, 6500, 6000],
|
||
['Oranges', 500, 300, 200, 700],
|
||
]
|
||
|
||
ws.append(["Fruit", "2011", "2012", "2013", "2014"])
|
||
for row in data:
|
||
ws.append(row)
|
||
|
||
tab = Table(displayName="Table1", ref="A1:E5")
|
||
style = TableStyleInfo(name="TableStyleMedium9", showFirstColumn=False,
|
||
showLastColumn=False, showRowStripes=True, showColumnStripes=True)
|
||
tab.tableStyleInfo = style
|
||
ws.add_table(tab)
|
||
wb.save("table.xlsx")
|
||
```
|
||
|
||
openpyxl 제약(문서 원문):
|
||
- *"Table headings and column headings must always contain strings."* (write-only 모드에서는 헤더 문자열이 셀 값과 정확히 일치해야 함)
|
||
- *"Filters will be added automatically to tables that contain header rows. It is not possible to create tables with header rows without filters."*
|
||
- `ws.add_table()` 를 써야 워크북 전체에서 이름 유일성이 보장된다.
|
||
- 틀 고정: `sheet.freeze_panes = 'A2'`.
|
||
|
||
---
|
||
|
||
## 12. 한국 제약 실무자가 실제로 보고 싶어하는 것
|
||
|
||
### 12.1 근거 — 국내 매체가 DMF 를 다루는 방식
|
||
|
||
| 기사/자료 | 실제로 쓴 집계 축 | 강조한 숫자 |
|
||
|---|---|---|
|
||
| [데일리팜 — 규제완화 여파…원료약 등록 1년새 237→653건](https://www.dailypharm.com/Users/News/NewsView.html?ID=324241) | **연도별(반기별) 총 건수**, 전년 동기 배수 | 653 / 256 / 545 / 537, 2.8배, 역대 최고 |
|
||
| [데일리팜 — 인도·중국산 등록비중 70% 돌파…외면받는 국산 원료약](http://m.dailypharm.com/newsView.html?ID=307819) | **제조국별 건수와 비중**, 국산 비중, 연도별 추이 | 249건 중 233건 해외 / 인도 120·중국 60·이탈리아 10·일본 6 / 국산 16건(6.4%) / 인·중 72.3% / 인도 48.2% / 2018년 59.7% → 2023년 72.3% / 자급률 24.4%→11.9% / 인·중산 20~30% 저렴 |
|
||
| [데일리팜 — 필수약·공급중단 보고대상 의약품 'DMF 등록' 유예](https://m.dailypharm.com/user/news/32332) | **제도 변화(유예)** | 국가필수의약품·공급중단 보고대상 |
|
||
| [데일리팜 — 의약품 해외제조소 등록 안하면 2차 위반시 허가취소](https://dailypharm.com/user/news/67688) | **제조소 규제 연계** | DMF 등록 원료도 해외제조소 등록 대상, 제조소 이전 시 신규 등록 |
|
||
| [의학신문 — 식약처, 원료의약품 등록 제도(DMF) 해설서 발간](http://www.bosa.co.kr/news/articleView.html?idxno=2141146) | 제도 해설 | 연계심사 절차 반영 |
|
||
| [KHIDI 제약산업정보포털 — 등록대상 원료의약품(DMF) 등록 공고(7월 둘째주)](https://www.khidi.or.kr/board/view?pageNum=48&rowCnt=10&menuId=MENU01872&maxIndex=00487793189998&minIndex=00487441479998&schType=0&upDown=0&no1=912&linkId=26604564) | **주간 공고 원문 재배포** | 첨부 표 컬럼: 등록번호 / 성분명 / 등록업체 / 제조원 / 제조국 / **등록구분(신규·변경·취하)** |
|
||
|
||
> **결론**: 한국 실무자가 보는 축은 정확히 **① 총 건수와 전년 대비 ② 제조국(특히 인도·중국 vs 국산) ③ 등록구분(신규/변경/취하) ④ 성분 ⑤ 업체·제조원** 다섯이다. 본 설계의 시트·차트가 이 다섯을 모두 커버한다.
|
||
|
||
### 12.2 규제 인텔리전스 뉴스레터가 담는 항목 (국제 실무)
|
||
|
||
검색 #34 종합: FDA 데이터셋 기대치 명확화, **라벨링 업데이트**, **품목허가 신청(marketing authorization applications)**, **허가 후 변경(post-approval submissions)**, 품질·비임상·유효성·안전성 **과학적 가이던스**.
|
||
형식: *"a daily synthesis of complex policy—helping you understand what's changing, why it matters, and what comes next."* → **무엇이 바뀌었는가 / 왜 중요한가 / 다음에 무엇이 오는가** 3단.
|
||
|
||
> **DMF 리포트에 적용**: `오늘 변경분` 시트의 `변경내용(before→after)` 컬럼이 "무엇이 바뀌었나"를, `워치리스트히트`가 "왜 중요한가"를 담당한다. "다음에 무엇이 오는가"는 `최종연차보고년도` 3년 경과 경고와 `취소/취하` 예고로 대체한다.
|
||
|
||
### 12.3 실무자 체크리스트 → 시트 매핑
|
||
|
||
| 실무 질문 | 답을 주는 위치 |
|
||
|---|---|
|
||
| "어제 오늘 사이에 뭐가 새로 등록됐나?" | `대시보드` KPI 1 + `오늘 변경분` (상태=신규) |
|
||
| "우리가 쓰는 원료가 변경등록됐나?" | `워치리스트` 결과 영역 + `오늘 변경분` `워치리스트히트` |
|
||
| "경쟁사 ○○제약이 뭘 등록했나?" | `업체별` 시트 + 워치리스트(`대상유형=신청인`) |
|
||
| "이 성분 국내 공급처가 몇 곳인가?" | `성분별` `등록업체 수` / `제조국 수` / `국산 보유` |
|
||
| "제조소가 중국에서 인도로 바뀌었나?" | `오늘 변경분` `변경필드`에 `제조국가`·`제조소명` 포함된 행 |
|
||
| "우리 원료 DMF 가 취하됐나?" (**공급 리스크**) | `대시보드` KPI 3 + `오늘 변경분` (상태=취하, 취소선) |
|
||
| "이 등록번호 언제 등록됐고 몇 번 바뀌었나?" | `전체 누적` 5개 날짜 컬럼 → `스냅샷·로그` 이력 |
|
||
| "국산 원료 비중이 계속 떨어지나?" | `대시보드` 제조국 Top 8 + `업체별` `국산여부` |
|
||
| "연차보고 3년째 안 한 등록이 있나?" | `전체 누적` `최종연차보고년도` 조건부서식 |
|
||
| "이 숫자 믿어도 되나? 크롤링 실패는 없었나?" | `메타` 블록 3(건수 검증) + `스냅샷·로그` `HTTP상태` |
|
||
|
||
### 12.4 한국 특유의 주의사항
|
||
|
||
1. **성분명 표기 흔들림**: 한글 성분명은 띄어쓰기·괄호·염 표기가 제각각(`아토르바스타틴칼슘삼수화물` vs `아토르바스타틴 칼슘 삼수화물`). → `성분키` 정규화 컬럼 필수.
|
||
2. **업체명 접두/접미**: `(주)`, `㈜`, `주식회사`, `Co., Ltd.`, 영문/한글 혼용. → `업체키` 정규화 필수(§11.5 `norm_key`).
|
||
3. **제조국가 표기**: `대한민국`/`한국`/`국내`, `중국`/`중화인민공화국`, `인도`/`India` 혼용 가능. → `메타` 코드북에 매핑표를 두고 미매핑 값은 검증 블록에 카운트.
|
||
4. **"사전등록(연계 등록 원료의약품)"**: 완제 허가와 연계된 별도 상태. 일반 신규와 혼동하면 KPI 가 부풀려진다. → 상태 도메인 7종을 분리 유지(§5.5).
|
||
5. **연차보고는 "변경"이 아니다**: 연차보고(변경보고)는 경미한 변경이므로 `오늘 변경` KPI 에 넣지 않는다. 별도 상태로만 표시.
|
||
6. **재공고**: DMF 해설서 통계표에 `( )의 숫자는 재공고된 숫자를 의미함` 이라는 주석이 있다. → 동일 등록번호가 다시 나타나는 경우를 `재공고` 이벤트로 `스냅샷·로그`에 기록하고, KPI 신규에서 제외.
|
||
7. **hwp/hwpx 첨부**: 주간 공고 게시글의 첨부는 hwp 계열일 수 있다(원료의약품 등록에 관한 규정 최신판은 `.hwpx`). → 첨부 파싱은 2단계 과제로 남기고 1단계는 HTML 표만 쓴다.
|
||
|
||
---
|
||
|
||
## 부록 A. 출처 목록
|
||
|
||
**범례** — 확인여부: `✅ FETCH` = WebFetch 로 본문 확인 / `⚠️ 403` = 접근 거부 / `⚠️ 404` = 없음 / `⚠️ DNS` = 도메인 해석 실패 / `🔍 검색` = 검색 결과 링크로만 등장(본문 미확인) / `📄 PDF` = PDF 로 내려받아 텍스트 추출 성공
|
||
|
||
### A.1 엑셀 대시보드 설계 원칙
|
||
|
||
| 제목 | URL | 확인여부 |
|
||
|---|---|---|
|
||
| Dashboard design 5 seconds rule — Den Otter Solutions | https://denottersolutions.com/en/data-insights/dashboard-design-5-seconds-rule/ | ✅ FETCH |
|
||
| 12 Best Practices in Dashboard Design — GoSkills | https://www.goskills.com/excel/resources/best-practices-in-dashboard-design | ✅ FETCH |
|
||
| How to Create a Dashboard in Excel: A Step-by-Step Tutorial — DataCamp | https://www.datacamp.com/tutorial/create-excel-dashboards-tutorial | ✅ FETCH |
|
||
| Excel Dashboard Templates: Free Downloads + How to Build One (2026) — DataHub Pro | https://www.datahubpro.co.uk/tutorials/excel-dashboard-templates | ✅ FETCH |
|
||
| from dashboard to story — storytelling with data | https://www.storytellingwithdata.com/blog/from-dashboard-to-story | ✅ FETCH |
|
||
| Dashboard Confusion (Stephen Few, 2004-03-20) — Perceptual Edge | https://www.perceptualedge.com/articles/ie/dashboard_confusion.pdf | 📄 PDF (4p, 10,305자 추출) |
|
||
| Build an Interactive Excel Dashboard with Pivot Tables — Excel Campus | https://www.excelcampus.com/charts/excel-dahsboard-tutorial-full-buildout/ | ✅ FETCH |
|
||
| Excel Dashboard Design Principles: Make the Answer Obvious — Big Excel Energy | https://bigexcelenergy.com/excel-dashboard-design-principles/ | ⚠️ 403 |
|
||
| Excel Dashboards — Templates, Tutorials, Downloads and Examples — Chandoo.org | https://chandoo.org/wp/excel-dashboards/ | ⚠️ 403 |
|
||
| Making a Simple KPI Dashboard using MS Excel — Chandoo.org | https://chandoo.org/wp/2011/08/25/simple-kpi-dashboard-using-excel/ | ⚠️ 403 |
|
||
| kpi dashboards Archives — Chandoo.org | https://chandoo.org/wp/tag/kpi-dashboards/ | 🔍 검색 |
|
||
| Dashboard Design Concepts — Chandoo.org Forums | https://chandoo.org/forum/threads/dashboard-design-concepts.19536/ | 🔍 검색 |
|
||
| Dashboard Ideas — Chandoo.org Forums | https://chandoo.org/forum/threads/dashboard-ideas.21929/ | 🔍 검색 |
|
||
| How to design the dashboard — Chandoo.org Forums | https://chandoo.org/forum/threads/how-to-design-the-dashboard.12296/ | 🔍 검색 |
|
||
| What is the best Excel Background Design that WOWed you...? — Chandoo.org Forums | https://chandoo.org/forum/threads/what-is-the-best-excel-background-design-that-wowed-you.29586/ | 🔍 검색 |
|
||
| Learn how to create these 11 amazing dashboards — Chandoo.org | https://chandoo.org/wp/2014/02/14/dashboard-training-course/ | 🔍 검색 |
|
||
| 18 Tips to Make you an Excel Formatting Pro — Chandoo.org | http://chandoo.org/wp/2011/12/05/excel-formatting-tips/ | 🔍 검색 |
|
||
| How to create a fully interactive Project Dashboard with Excel — Chandoo.org | https://chandoo.org/wp/interactive-project-dashboard-with-excel/ | 🔍 검색 |
|
||
| Introducing Excel Dashboard Templates from Chandoo.org | https://chandoo.org/wp/introducing-excel-dashboard-templates/ | 🔍 검색 |
|
||
| How to highlight overdue items, coming up and completed in Excel? — Chandoo.org | https://chandoo.org/wp/highlight-due-dates-excel/ | 🔍 검색 |
|
||
| Excel Campus (홈) | https://www.excelcampus.com/ | 🔍 검색 |
|
||
| Use AI to Design Excel Dashboards Fast — Excel Campus | https://www.excelcampus.com/charts/use-ai-to-design-excel-dashboard/ | 🔍 검색 |
|
||
| 7 Excel Dashboard Tools & Tips for the Quick Access Toolbar — Excel Campus | https://www.excelcampus.com/charts/7-excel-dashboard-tips-qat/ | 🔍 검색 |
|
||
| Introduction to Pivot Tables and Dashboards [Video 1 of 3] — Excel Campus | https://www.excelcampus.com/pivot-tables/pivot-tables-dashboards-part-1/ | 🔍 검색 |
|
||
| About Jon Acampora — Excel Campus | https://www.excelcampus.com/about/ | 🔍 검색 |
|
||
| Video Review of the Excel Dashboard Course — Excel Campus | https://www.excelcampus.com/charts/excel-dashboard-course-review/ | 🔍 검색 |
|
||
| Excel Campus - Jon — YouTube | https://www.youtube.com/user/ExcelCampus | 🔍 검색 |
|
||
| Jon Acampora — LinkedIn | https://www.linkedin.com/in/jonacampora/ | 🔍 검색 |
|
||
| Dashboard Design Best Practices for Better Data Visualization — Rosalie (Medium) | https://rosalie24.medium.com/dashboard-design-best-practices-for-better-data-visualization-3dec5d71761b | 🔍 검색 |
|
||
| Dashboard Design Best Practices: Layouts & Examples — Domo | https://www.domo.com/learn/article/dashboard-design-examples-best-practices | 🔍 검색 |
|
||
| 10 Key Dashboard Design Principles — Yellowfin | https://www.yellowfinbi.com/blog/key-dashboard-design-principles-analytics-best-practice | 🔍 검색 |
|
||
| Five Rules for Building Impressive Excel Dashboards — Plum Solutions | https://plumsolutions.com.au/five-rules-for-building-impressive-excel-dashboards/ | 🔍 검색 |
|
||
| 8 Dashboard Design Best Practices for Insight-Led Decisions — Vizule | https://vizule.io/dashboard-design-best-practices/ | 🔍 검색 |
|
||
| Effective Dashboard Color Schemes — insightsoftware | https://insightsoftware.com/blog/effective-color-schemes-for-analytics-dashboards/ | 🔍 검색 |
|
||
| Best Admin Dashboard Color Schemes: A Developer's Guide — AdminLTE | https://adminlte.io/blog/best-admin-dashboard-color-schemes/ | 🔍 검색 |
|
||
| 8 Excel Dashboard Palettes + 4 Chart Color Sets — Big Excel Energy | https://bigexcelenergy.com/excel-dashboard-color-palette/ | 🔍 검색 |
|
||
| Best Hex Code Palettes for Professional Spreadsheets — GSheets | https://www.gsheets.com/blog/beautiful-spreadsheet-color-palettes | 🔍 검색 |
|
||
| What are the most effective UI/UX design principles for an Excel-based Sales Dashboard? — iCert Global | https://www.icertglobal.com/community/excel-dashboard-ui-ux-design-tips | 🔍 검색 |
|
||
| Incredible dashboard design principles that make data land — Forsta | https://www.forsta.com/resources/blog/dashboard-design-principles-land/ | 🔍 검색 |
|
||
| Design for Data Storytelling: Less Clutter, More Clarity — Locus IT | https://locusit.com/learning/business-applications/design-for-data-storytelling-less-clutter-more-clarity/ | 🔍 검색 |
|
||
|
||
### A.2 데이터 스토리텔링 · 데이터 잉크 비율
|
||
|
||
| 제목 | URL | 확인여부 |
|
||
|---|---|---|
|
||
| Declutter your Chart with Data-Ink Ratio — Sudharsan Asaithambi (Medium) | https://sudharsanasai.medium.com/declutter-your-chart-with-data-ink-ratio-6f6908727842 | 🔍 검색 |
|
||
| The Art and Science of Data-Ink Ratios For Effective Data Storytelling — John Loewen (Medium) | https://medium.com/data-storytelling-corner/the-art-and-science-of-simple-data-visual-design-for-effective-storytelling-1d7667c1bfd8 | 🔍 검색 |
|
||
| Data Storytelling in Data Visualisation (CHI 2024) — ACM DL | https://dl.acm.org/doi/10.1145/3613904.3643022 | 🔍 검색 |
|
||
| Visualization 101: 9 essential principles of data storytelling — Bunker DB | https://bunkerdb.com/en/blog/data-visualization-101-storytelling-principles | 🔍 검색 |
|
||
| Master Data Storytelling — Techcanvass | https://businessanalyst.techcanvass.com/data-storytelling/ | 🔍 검색 |
|
||
| Telling your Data Story in Seconds - Part 2: Color — DataSelf | https://www.dataself.com/telling-story-seconds-part-2-color/ | 🔍 검색 |
|
||
| 3.4 Storytelling with data visualisation — Alan Turing Institute RDS Course | https://alan-turing-institute.github.io/rds-course/modules/m3/3.4-StoryTelling.html | 🔍 검색 |
|
||
| Ten practical tips for effective data visualization — School of Cities | https://schoolofcities.github.io/urban-data-storytelling/urban-data-visualization/data-visualization-practical-tips/data-visualization-practical-tips.html | 🔍 검색 |
|
||
| storytelling with data (홈) | https://www.storytellingwithdata.com/ | 🔍 검색 |
|
||
| storytelling with data — makeovers | https://www.storytellingwithdata.com/makeovers | 🔍 검색 |
|
||
| storytelling with data — podcast | https://www.storytellingwithdata.com/podcast | 🔍 검색 |
|
||
| storytelling with data — podcast archive | https://www.storytellingwithdata.com/podcast/archive | 🔍 검색 |
|
||
| Ep 1 | the art of feedback — SWD podcast | https://www.storytellingwithdata.com/podcast/feedback | 🔍 검색 |
|
||
| storytelling with data — books | https://www.storytellingwithdata.com/books | 🔍 검색 |
|
||
| SWD community — Aug 2021 dashboard stories challenge | https://community.storytellingwithdata.com/challenges/aug-2021-dashboard-stories/are-we-undercharging-for-data-visualization-work | 🔍 검색 |
|
||
| Storytelling with Data (Cole Nussbaumer Knaflic) — Wiley | https://www.wiley.com/en-us/Storytelling+with+Data:+A+Data+Visualization+Guide+for+Business+Professionals-p-9781119002253 | 🔍 검색 |
|
||
| Storytelling with Data — Amazon | https://www.amazon.com/Storytelling-Data-Visualization-Business-Professionals/dp/1119002257 | 🔍 검색 |
|
||
| Storytelling with data: What a book! — Sam Campitiello (Medium) | https://medium.com/@sam.campitiello/storytelling-with-data-what-a-book-1cea585ca7c0 | 🔍 검색 |
|
||
| Book Summary - Storytelling with Data — Readingraphics | https://readingraphics.com/book-summary-storytelling-with-data/ | 🔍 검색 |
|
||
| Storytelling with data (PDF) — Vizalyst | https://vizalyst.com/wp-content/uploads/2025/03/Cole-Knaflic-Storytelling-with-data-a-data-visualization-guide-for-business-professionals-Knaflic-Cole-Nussbaumer-Storytelling-with-data-a-dat.pdf | 🔍 검색 |
|
||
| Storytelling with Data — eBay 리스팅 | https://www.ebay.de/itm/116667462617 | 🔍 검색 |
|
||
|
||
### A.3 색 접근성 · 팔레트
|
||
|
||
| 제목 | URL | 확인여부 |
|
||
|---|---|---|
|
||
| Color Universal Design (Okabe & Ito) — jfly | https://jfly.uni-koeln.de/color/ | ✅ FETCH |
|
||
| Paul Tol — Colour Schemes (bright / vibrant / high-contrast, bad-data grey) | https://sronpersonalpages.nl/~pault/ | ✅ FETCH |
|
||
| Paul Tol — 구 주소 (DNS 실패) | https://personal.sron.nl/~pault/ | ⚠️ DNS (`getaddrinfo ENOTFOUND`) |
|
||
| Fundamentals of Data Visualization — Color Pitfalls (Claus Wilke) | https://clauswilke.com/dataviz/color-pitfalls.html | ✅ FETCH |
|
||
| Okabe-Ito Palette Hex Codes for Python, R & CSS — ConceptViz | https://conceptviz.app/blog/okabe-ito-palette-hex-codes-complete-reference | ✅ FETCH |
|
||
| When Color Coding Fails: Why Status Indicators Need More Than Pretty Colors — accessibility.chat | https://www.accessibility.chat/articles/when-color-coding-fails-why-status-indicators-need-more-than-pretty-colors | ✅ FETCH |
|
||
| Okabe-Ito Colorblind-Safe Palette: Copy HEX + Download — sci-draw | https://sci-draw.com/blog/colorblind-safe-palettes-okabe-ito-reference | 🔍 검색 |
|
||
| Best Scientific Color Palettes: Hex Codes & Examples — ConceptViz | https://conceptviz.app/blog/scientific-color-palette-for-research-papers-and-posters | 🔍 검색 |
|
||
| Okabe-Ito Palette Hex Codes: Full 8-Color Reference [2026] — Figviz | https://figviz.com/blog/okabe-ito-palette-hex-codes-full-8-color-reference-with-code-examples-2026-wlt9th1n | 🔍 검색 |
|
||
| Colorblind-Safe Palette Generator — Okabe-Ito Set | https://www.generatorcollection.com/en/generators/colors/colorblind-safe-palette-generator/ | 🔍 검색 |
|
||
| Okabe-Ito Palette: Gold Standard for Science — Vizcept | https://vizcept.com/blog/okabe-ito-palette-guide | 🔍 검색 |
|
||
| Color scales — Claus Wilke SDS375 slides | https://wilkelab.org/SDS375/slides/color-scales.html | 🔍 검색 |
|
||
| Fundamentals of Data Visualization — Google Books | https://books.google.com/books/about/Fundamentals_of_Data_Visualization.html?id=L3ajtgEACAAJ | 🔍 검색 |
|
||
| Fundamentals of Data Visualization — O'Reilly | https://www.oreilly.com/library/view/fundamentals-of-data/9781492031079/ | 🔍 검색 |
|
||
| Fundamentals of Data Visualization — Amazon | https://www.amazon.com/Fundamentals-Data-Visualization-Informative-Compelling/dp/1492031089 | 🔍 검색 |
|
||
| Fundamentals of Data Visualization — Goodreads | https://www.goodreads.com/book/show/40014286-fundamentals-of-data-visualization | 🔍 검색 |
|
||
| Book Summary: Fundamentals of Data Visualization — Lucky Bookshelf | https://www.luckybookshelf.com/fundamentals-of-data-visualization-by-claus-o-wilke/ | 🔍 검색 |
|
||
| Fundamentals of Data Visualization (전문) — dokumen.pub | https://dokumen.pub/fundamentals-of-data-visualization-a-primer-on-making-informative-and-compelling-figures-1nbsped-1492031089-9781492031086.html | 🔍 검색 |
|
||
| Creating Accessible Excel Spreadsheets: A Comprehensive Guide — Benetech | https://benetech.org/creating-accessible-excel-spreadsheets-a-comprehensive-guide/ | 🔍 검색 |
|
||
| Excel Accessibility: How to Make Excel Documents Accessible — Continual Engine | https://www.continualengine.com/blog/excel-accessibility/ | 🔍 검색 |
|
||
| WCAG 1.4.1: Don't rely solely on color — wcag.dock.codes | https://wcag.dock.codes/documentation/wcag141/ | 🔍 검색 |
|
||
| Understanding WCAG SC 1.4.1 Use of Color — DigitalA11Y | https://www.digitala11y.com/understanding-sc-1-4-1-use-of-color/ | 🔍 검색 |
|
||
| 1.4.1 Use of Color — AAArdvark | https://aaardvarkaccessibility.com/wcag-plain-english/1-4-1-use-of-color/ | 🔍 검색 |
|
||
| WCAG 1.4.1 Use of Color - Complete Guide — Accessibility.build | https://accessibility.build/wcag/1-4-1 | 🔍 검색 |
|
||
| WCAG 1.4.1 - Use of Color | Fix It Series — Accessibility Checker | https://accessibilitychecker.com/news/wcag-fix-it-1-4-1-use-of-color/ | 🔍 검색 |
|
||
|
||
### A.4 KPI 카드 · 스파크라인
|
||
|
||
| 제목 | URL | 확인여부 |
|
||
|---|---|---|
|
||
| Create Amazing Key Performance Indicator Data Cards In Excel — How To Excel | https://www.howtoexcel.org/kpi-data-cards/ | ✅ FETCH |
|
||
| Flooring Installation KPI Scorecard in Excel — PK: An Excel Expert | https://www.pk-anexcelexpert.com/flooring-installation-kpi-scorecard-in-excel/ | ✅ FETCH |
|
||
| Excel KPI Card Design: Build Clean Metric Cards — Big Excel Energy | https://bigexcelenergy.com/excel-kpi-card-design/ | ⚠️ 403 |
|
||
| Excel Sparklines - A Complete Guide with Examples — TrumpExcel | https://trumpexcel.com/sparklines/ | ✅ FETCH |
|
||
| Creating a Big Numbers KPI Dashboard in Tableau — Senturus | https://senturus.com/blog/creating-a-big-numbers-kpi-dashboard-in-tableau/ | 🔍 검색 |
|
||
| How To Build Dynamic KPI Dashboards in Excel — Vena Solutions | https://www.venasolutions.com/blog/build-kpi-dashboard-excel | 🔍 검색 |
|
||
| Card Visualization: The Tile Visualization or Large Number Tiles — Dashboard Builder | https://dashboardbuilder.net/card-visualization | 🔍 검색 |
|
||
| Spark Line Chart: What It Is and When to Use It — Domo | https://www.domo.com/learn/charts/sparkline-chart | 🔍 검색 |
|
||
| Win/Loss Sparklines — MyExcelOnline | https://www.myexcelonline.com/blog/winloss-sparklines-in-excel/ | 🔍 검색 |
|
||
| How to Create Sparkline Dashboard in Excel — ElyxAI | https://getelyxai.com/en/how-to/create-sparkline-dashboard | 🔍 검색 |
|
||
| Column Sparkline — ElyxAI Excel Glossary | https://getelyxai.com/en/excel-glossary/column-sparkline | 🔍 검색 |
|
||
| Win/Loss Sparkline — ElyxAI Excel Glossary | https://getelyxai.com/en/excel-glossary/win-loss-sparkline | 🔍 검색 |
|
||
| Create Win-Loss Sparklines In Excel — Simple Sheets | https://www.simplesheets.co/blog/complete-guide-for-create-win-loss-sparklines-in-excel | 🔍 검색 |
|
||
| Mastering Sparklines in Excel: Win-Loss Sparklines (Part II) — Vishal CPA Prep | https://vishalcpaprep.com/blogs/news/mastering-sparklines-in-excel-win-loss-sparklines-part-ii | 🔍 검색 |
|
||
| Win/Loss Sparkline for Streaks — DFW Excel | https://dfwexcel.com/excel-formulas/win-loss-sparkline/ | 🔍 검색 |
|
||
|
||
### A.5 XlsxWriter / openpyxl / pandas
|
||
|
||
| 제목 | URL | 확인여부 |
|
||
|---|---|---|
|
||
| Working with Conditional Formatting — XlsxWriter | https://xlsxwriter.readthedocs.io/working_with_conditional_formats.html | ✅ FETCH |
|
||
| Example: Sparklines (Advanced) — XlsxWriter | https://xlsxwriter.readthedocs.io/example_sparklines2.html | ✅ FETCH |
|
||
| Working with Pandas and XlsxWriter | https://xlsxwriter.readthedocs.io/working_with_pandas.html | ✅ FETCH |
|
||
| Working with Worksheet Tables — XlsxWriter | https://xlsxwriter.readthedocs.io/working_with_tables.html | ✅ FETCH |
|
||
| Example: Column Chart — XlsxWriter | https://xlsxwriter.readthedocs.io/example_chart_column.html | ✅ FETCH |
|
||
| Example: Adding hyperlinks — XlsxWriter | https://xlsxwriter.readthedocs.io/example_hyperlink.html | ✅ FETCH |
|
||
| The Worksheet Class — XlsxWriter | https://xlsxwriter.readthedocs.io/worksheet.html | ✅ FETCH |
|
||
| Example: Conditional Formatting — XlsxWriter | https://xlsxwriter.readthedocs.io/example_conditional_format.html | ✅ FETCH |
|
||
| Example: Sparklines (Simple) — XlsxWriter | https://xlsxwriter.readthedocs.io/example_sparklines1.html | 🔍 검색 |
|
||
| Example: Freeze Panes and Split Panes — XlsxWriter | https://xlsxwriter.readthedocs.io/example_panes.html | 🔍 검색 |
|
||
| Example: Charts with Data Tables — XlsxWriter | https://xlsxwriter.readthedocs.io/example_chart_data_table.html | 🔍 검색 |
|
||
| Contents — XlsxWriter | https://xlsxwriter.readthedocs.io/contents.html | 🔍 검색 |
|
||
| XlsxWriter — working_with_conditional_formats.rst (GitHub) | https://github.com/jmcnamara/XlsxWriter/blob/master/dev/docs/source/working_with_conditional_formats.rst | 🔍 검색 |
|
||
| XlsxWriter — worksheet.rst (GitHub) | https://github.com/jmcnamara/XlsxWriter/blob/main/dev/docs/source/worksheet.rst | 🔍 검색 |
|
||
| XlsxWriter — working_with_pandas.rst (GitHub) | https://github.com/jmcnamara/XlsxWriter/blob/main/dev/docs/source/working_with_pandas.rst | 🔍 검색 |
|
||
| XlsxWriter — examples/chart_column.py (GitHub) | https://github.com/jmcnamara/XlsxWriter/blob/master/examples/chart_column.py | 🔍 검색 |
|
||
| XlsxWriter Issue #609 — write_url internal sheet reference with spaces | https://github.com/jmcnamara/XlsxWriter/issues/609 | 🔍 검색 |
|
||
| XlsxWriter Issue #350 — write_url produces broken hyperlink | https://github.com/jmcnamara/XlsxWriter/issues/350 | 🔍 검색 |
|
||
| XlsxWriter Issue #511 — hyperlinking cells containing multiple URLs | https://github.com/jmcnamara/XlsxWriter/issues/511 | 🔍 검색 |
|
||
| libxlsxwriter — hyperlinks.c 예제 | https://libxlsxwriter.github.io/hyperlinks_8c-example.html | 🔍 검색 |
|
||
| xlsxwriter.lua — Adding hyperlinks | https://xlsxwriterlua.readthedocs.io/example_hyperlink.html | 🔍 검색 |
|
||
| XlsxWriter 0.5.8 — PyPI | https://pypi.org/project/XlsxWriter/0.5.8 | 🔍 검색 |
|
||
| Python XlsxWriter - Sparklines — TutorialsPoint | https://www.tutorialspoint.com/python_xlsxwriter/python_xlsxwriter_sparklines.htm | 🔍 검색 |
|
||
| Python XlsxWriter - Conditional Formatting — TutorialsPoint | https://www.tutorialspoint.com/python_xlsxwriter/python_xlsxwriter_conditional_formatting.htm | 🔍 검색 |
|
||
| Python XlsxWriter - Adding Charts — TutorialsPoint | https://www.tutorialspoint.com/python_xlsxwriter/python_xlsxwriter_adding_charts.htm | 🔍 검색 |
|
||
| Python XlsxWriter - Hyperlinks — TutorialsPoint | https://www.tutorialspoint.com/python_xlsxwriter/python_xlsxwriter_hyperlinks.htm | 🔍 검색 |
|
||
| Python | Plotting column charts in excel sheet using XlsxWriter — GeeksforGeeks | https://www.geeksforgeeks.org/python/python-plotting-column-charts-in-excel-sheet-using-xlsxwriter-module/ | 🔍 검색 |
|
||
| Create excel charts with xlsxwriter — riptutorial | https://riptutorial.com/python/example/17392/create-excel-charts-with-xlsxwriter | 🔍 검색 |
|
||
| Python and Excel: Writing Data Using XlsxWriter — Charlie (Medium) | https://medium.com/@charliepython/python-and-excel-writing-data-using-xlsxwriter-78d602ca5048 | 🔍 검색 |
|
||
| Pivot Tables — openpyxl (stable) | https://openpyxl.readthedocs.io/en/stable/pivot.html | ✅ FETCH |
|
||
| Conditional Formatting — openpyxl (stable) | https://openpyxl.readthedocs.io/en/stable/formatting.html | ✅ FETCH |
|
||
| Worksheet Tables — openpyxl (stable) | https://openpyxl.readthedocs.io/en/stable/worksheet_tables.html | ✅ FETCH |
|
||
| Pivot Tables — openpyxl 2.5.15 | https://openpyxl.readthedocs.io/en/2.5/pivot.html | 🔍 검색 |
|
||
| Pivot Tables — openpyxl 3.0.10 | https://openpyxl.readthedocs.io/en/3.0/pivot.html | 🔍 검색 |
|
||
| Pivot Tables — openpyxl 3.1.3 | https://openpyxl.readthedocs.io/en/3.1/pivot.html | 🔍 검색 |
|
||
| Pivot Tables — openpyxl latest | https://openpyxl.readthedocs.io/en/latest/pivot.html | 🔍 검색 |
|
||
| openpyxl.pivot.cache module — 3.1.1 | https://openpyxl.readthedocs.io/en/3.1.1/api/openpyxl.pivot.cache.html | 🔍 검색 |
|
||
| openpyxl.pivot.cache module — stable | https://openpyxl.readthedocs.io/en/stable/api/openpyxl.pivot.cache.html | 🔍 검색 |
|
||
| openpyxl.pivot.cache module — latest | https://openpyxl.readthedocs.io/en/latest/api/openpyxl.pivot.cache.html | 🔍 검색 |
|
||
| openpyxl.pivot.table module — stable | https://openpyxl.readthedocs.io/en/stable/api/openpyxl.pivot.table.html | 🔍 검색 |
|
||
| openpyxl.formatting.rule module — 3.0.10 | https://openpyxl.readthedocs.io/en/3.0/api/openpyxl.formatting.rule.html | 🔍 검색 |
|
||
| openpyxl.formatting.rule module — latest | https://openpyxl.readthedocs.io/en/latest/api/openpyxl.formatting.rule.html | 🔍 검색 |
|
||
| openpyxl.formatting.rule module — stable | https://openpyxl.readthedocs.io/en/stable/api/openpyxl.formatting.rule.html | 🔍 검색 |
|
||
| openpyxl.formatting.rule module — 3.1.1 | https://openpyxl.readthedocs.io/en/3.1.1/api/openpyxl.formatting.rule.html | 🔍 검색 |
|
||
| Conditional Formatting — openpyxl 3.1.2 | https://openpyxl.readthedocs.io/en/3.1.2/formatting.html | 🔍 검색 |
|
||
| Conditional Formatting — openpyxl 3.1.1 | https://openpyxl.readthedocs.io/en/3.1.1/formatting.html | 🔍 검색 |
|
||
| Conditional Formatting — openpyxl 3.1 | https://openpyxl.readthedocs.io/en/3.1/formatting.html | 🔍 검색 |
|
||
| openpyxl.worksheet.table module — stable | https://openpyxl.readthedocs.io/en/stable/api/openpyxl.worksheet.table.html | 🔍 검색 |
|
||
| Worksheet Tables — openpyxl 3.1 | https://openpyxl.readthedocs.io/en/3.1/worksheet_tables.html | 🔍 검색 |
|
||
| Worksheet Tables — openpyxl 2.4.11 | https://openpyxl.readthedocs.io/en/2.4/worksheet_tables.html | 🔍 검색 |
|
||
| openpyxl.worksheet.table — 2.5.2 (미러) | https://ssopenpyxl.readthedocs.io/en/2.5.2/openpyxl.worksheet.table.html | 🔍 검색 |
|
||
| Using filters and sorts — openpyxl | https://openpyxl.readthedocs.io/en/stable/filters.html | 🔍 검색 |
|
||
| Conditional Format — Master OpenPyXL | https://www.openpyxl.com/conditional-format.html | 🔍 검색 |
|
||
| Adding Conditional Formatting to Excel Using Python Openpyxl — GeeksforGeeks | https://www.geeksforgeeks.org/python/adding-conditional-formatting-to-excel-using-python-openpyxl/ | 🔍 검색 |
|
||
| Adding Conditional Formatting (Video) — Real Python | https://realpython.com/lessons/conditional-formatting/ | 🔍 검색 |
|
||
| How to Implement Excel-Style Conditional Logic with Openpyxl — Statology | https://www.statology.org/how-to-implement-excel-style-conditional-logic-with-openpyxl-in-python/ | 🔍 검색 |
|
||
| openpyxl Part 13 – Conditional Styling — Prospero Coder | https://prosperocoder.com/science/openpyxl-part-13-conditional-styling/ | 🔍 검색 |
|
||
| Python openpyxl Conditional Formatting Guide — PyTutorial | https://pytutorial.com/python-openpyxl-conditional-formatting-guide/ | 🔍 검색 |
|
||
| Chapter 13 – Working with Excel Spreadsheets — Automate the Boring Stuff | https://automatetheboringstuff.com/2e/chapter13/ | 🔍 검색 |
|
||
| Diff Two Excel Files with Python — Matthew Kudija | https://matthewkudija.com/blog/2018/07/21/excel-diff/ | ✅ FETCH |
|
||
| pandas.DataFrame.to_excel — pandas 3.0.5 | https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_excel.html | 🔍 검색 |
|
||
| Enhance Your Pandas Excel Output: Freezing Panes, Filtering, Adjusting Column Widths — velog | https://velog.io/@sacross93/Enhance-Your-Pandas-Excel-Output-Freezing-Panes-Filtering-and-Adjusting-Column-Widths | 🔍 검색 |
|
||
| Pandas DataFrame.to_excel() Function — Delft Stack | https://www.delftstack.com/api/python-pandas/pandas-dataframe-dataframe.to_excel-function/ | 🔍 검색 |
|
||
| pyspark.pandas.DataFrame.to_excel — Databricks | https://api-docs.databricks.com/python/pyspark/latest/pyspark.pandas/api/pyspark.pandas.DataFrame.to_excel.html | 🔍 검색 |
|
||
| pandas PR #62994 (diff) | https://github.com/pandas-dev/pandas/pull/62994.diff | 🔍 검색 |
|
||
| Excel Tips — Scribd | https://www.scribd.com/document/44201206/Excel-Tips | 🔍 검색 |
|
||
|
||
### A.6 엑셀 정렬 · 커스텀 리스트
|
||
|
||
| 제목 | URL | 확인여부 |
|
||
|---|---|---|
|
||
| Sort data using a custom list — Microsoft Support | https://support.microsoft.com/en-us/office/sort-data-using-a-custom-list-cba3d67a-c5cb-406f-9b14-a02205834d72 | 🔍 검색 |
|
||
| Excel SORTBY function - custom sort with formula — Ablebits | https://www.ablebits.com/office-addins-blog/excel-sortby-function-custom-sort-formula/ | 🔍 검색 |
|
||
| Custom Sort Order in Excel - Made Simple — Excel Easy | https://www.excel-easy.com/examples/custom-sort-order.html | 🔍 검색 |
|
||
| How to Create Custom Sort Order in Excel — ElyxAI | https://getelyxai.com/en/how-to/create-custom-sort-order | 🔍 검색 |
|
||
| How to Sort Data in Excel – Custom Order, Filters & Pro Tips — Great Learning | https://www.mygreatlearning.com/blog/sort-data-in-excel/ | 🔍 검색 |
|
||
| Custom Sorting with SORT & MATCH — Excel Cookbook | https://www.excel-cookbook.com/recipes/sort-by-custom-order-match | 🔍 검색 |
|
||
| Organise Your Data Like a Pro: Custom Sort Order in Excel — techmemore | https://techmemore.substack.com/p/organise-your-data-like-a-pro-how | 🔍 검색 |
|
||
| Custom Sort — MrExcel Message Board | https://www.mrexcel.com/board/threads/custom-sort.471612/ | 🔍 검색 |
|
||
| Excel custom sorting using Office scripts — Microsoft Learn Q&A | https://learn.microsoft.com/en-us/answers/questions/1527424/excel-custom-sorting-using-office-scripts | 🔍 검색 |
|
||
| OfficeTalk: Bringing Improvements to Tables in Excel 2007 (Part 5 of 6) — Microsoft Learn | https://learn.microsoft.com/en-us/previous-versions/office/developer/officetalk2007/bb963879(v=office.11) | 🔍 검색 |
|
||
|
||
### A.7 한국 DMF (식약처 · 법령 · 업계)
|
||
|
||
| 제목 | URL | 확인여부 |
|
||
|---|---|---|
|
||
| 의약품안전나라 > 원료의약품등록(DMF) 공고 | https://nedrug.mfds.go.kr/pbp/CCBAC03 | ✅ FETCH (컬럼 15개 확인) |
|
||
| 의약품안전나라 > 원료의약품등록(DMF) 정보 (게시판) | https://nedrug.mfds.go.kr/bbs/117 | ✅ FETCH (총 710건, 주/격주 공고) |
|
||
| 공공데이터포털 — 식품의약품안전처_원료의약품등록(DMF)현황 OpenAPI | https://www.data.go.kr/data/15057075/openapi.do | ✅ FETCH (필드·파라미터 확인) |
|
||
| OpenAPI 엔드포인트 | https://apis.data.go.kr/1471000/MdcDmfInfoService01/getMdcDmfList01 | (문서 기재) |
|
||
| KHIDI — 등록대상 원료의약품(DMF) 등록 공고(7월 둘째주) | https://www.khidi.or.kr/board/view?pageNum=48&rowCnt=10&menuId=MENU01872&maxIndex=00487793189998&minIndex=00487441479998&schType=0&upDown=0&no1=912&linkId=26604564 | ✅ FETCH (주간 공고 컬럼 6종) |
|
||
| 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 | ✅ FETCH (2017-03-30, HWP 223.5KB) |
|
||
| KHIDI — 「원료의약품 등록에 관한 규정」고시 개정 알림 | https://www.khidi.or.kr/board/view?pageNum=48&rowCnt=10&no1=3447&linkId=48923038&menuId=MENU01872&maxIndex=&minIndex=&schType=0&schText=&schStartDate=&schEndDate=&boardStyle=&categoryId=&continent=&country= | 🔍 검색 |
|
||
| KPBMA — 원료의약품 등록 제도(DMF) 해설서 제5개정판 (PDF) | https://www.kpbma.or.kr/api/info/law/policy/download/211364 | 📄 PDF (118p, 113,549자 추출 — 등록번호 체계 Q51, 변경등록/변경보고 비교표) |
|
||
| MFDS — 원료의약품 등록 및 변경등록 업무 (지침서 GRP-MaPP-허가업무-04, 2025.2.20 6개정, PDF) | https://www.mfds.go.kr/brd/m_1059/down.do?brd_id=data0010&seq=15144&data_tp=A&file_seq=2 | 📄 PDF (35p, 23,925자 추출 — 처리기간·수수료·공고절차) |
|
||
| 국가법령정보센터 — 원료의약품 등록에 관한 규정 (admRulSeq=2100000198136) | https://law.go.kr/LSW/admRulLsInfoP.do?admRulSeq=2100000198136 | 🔍 검색 |
|
||
| 국가법령정보센터 — 원료의약품 등록에 관한 규정 (admRulSeq=2100000150569) | https://www.law.go.kr/LSW/admRulLsInfoP.do?admRulSeq=2100000150569 | 🔍 검색 |
|
||
| 국가법령정보센터 — 원료의약품 등록에 관한 규정 (admRulSeq=2100000252602) | https://law.go.kr/LSW/admRulLsInfoP.do?admRulSeq=2100000252602 | 🔍 검색 |
|
||
| 국가법령정보센터 — 원료의약품등록에관한규정 (행정규칙 직접 링크) | 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 | ✅ FETCH (요약만) |
|
||
| 국가법령정보센터 — 원료의약품 등록에 관한 규정 (조문 팝업) | 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 | 🔍 검색 |
|
||
| MFDS — 원료의약품 등록에 관한 규정 (seq=14569) | https://www.mfds.go.kr/brd/m_211/view.do?seq=14569&srchFr=&srchTo=&srchWord=&srchTp=&itm_seq_1=0&itm_seq_2=0&multi_itm_seq=0&company_cd=&company_nm=&page=1 | 🔍 검색 |
|
||
| MFDS — 「원료의약품 등록에 관한 규정」고시전문 (seq=14870) | 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 | 🔍 검색 |
|
||
| KPBMA — 「원료의약품 등록에 관한 규정」(식약처 고시) 일부개정 알림 | https://www.kpbma.or.kr/info/law/statute/select/210036 | 🔍 검색 |
|
||
| KPBMA — 등록대상 원료의약품(DMF) 사전등록제 세부 운영방안 알림 | https://www.kpbma.or.kr/inform/notice/notice/select/244214 | 🔍 검색 |
|
||
| 정부24 — 원료의약품(등록신청, 등록사항 변경등록신청) 민원안내 | https://www.gov.kr/mw/AA020InfoCappView.do?HighCtgCD=A09006&CappBizCD=14700000458&tp_seq= | ✅ FETCH (처리기간 20~90일, 콜센터 1588-2188) |
|
||
| KDI 경제교육·정보센터 — 원료의약품 등록 제도(DMF) 꼼꼼히 알기! | https://eiec.kdi.re.kr/policy/materialView.do?num=209012&topic=&pp=20&datecount=&recommend=&pg= | ✅ FETCH (2020-12-29 해설서 발간) |
|
||
| KDI 경제교육·정보센터 — 원료의약품 등록제도(DMF), 궁금하면 클릭하세요! | https://eiec.kdi.re.kr/policy/materialView.do?num=196213&topic=L&pp=20 | 🔍 검색 |
|
||
| MFDS — 원료의약품 등록제도(DMF), 궁금하면 클릭하세요! (PDF) | https://www.mfds.go.kr/brd/m_99/down.do?brd_id=ntc0021&seq=43886&data_tp=A&file_seq=2 | 🔍 검색 |
|
||
| MFDS — 원료등록(DMF) 기간 대폭 단축! (PDF) | https://www.mfds.go.kr/brd/m_99/down.do?brd_id=ntc0021&seq=48245&data_tp=A&file_seq=2 | 🔍 검색 |
|
||
| 식품의약품안전평가원 — 사업소개>의약품>허가심사>KDMF | https://www.nifds.go.kr/brd/m_87/list.do | 🔍 검색 |
|
||
| MFDS — 식품의약품 통계연보 목록 | https://www.mfds.go.kr/brd/m_371/list.do | 🔍 검색 |
|
||
| MFDS — 2023년 식품의약품통계연보(제25호) | https://www.mfds.go.kr/brd/m_371/view.do?seq=30722&itm_seq_1=0&itm_seq_2=0&multi_itm_seq=0&page=1 | 🔍 검색 |
|
||
| MFDS — 2022년 식품의약품통계연보(제24호) | https://www.mfds.go.kr/brd/m_371/view.do?seq=30721 | 🔍 검색 |
|
||
| MFDS — 2020년 식품의약품통계연보(제22호) | https://www.mfds.go.kr/brd/m_371/view.do?seq=30719&srchFr=&srchTo=&srchWord=&srchTp=&itm_seq_1=0&itm_seq_2=0&multi_itm_seq=0&company_cd=&company_nm=&page=1 | 🔍 검색 |
|
||
| MFDS — 통계안내 | https://www.mfds.go.kr/pgm/m_538/statInformation.do | 🔍 검색 |
|
||
| MFDS — 국가별 규제정보 (seq=40464) | https://www.mfds.go.kr/brd/m_617/view.do?seq=40464&srchFr=&srchTo=&srchWord=&srchTp=&itm_seq_1=0&itm_seq_2=0&multi_itm_seq=0&company_cd=&company_nm=&page=1 | 🔍 검색 |
|
||
| 공공데이터포털 — 식의약 통계 정보 서비스 파일데이터 | https://www.data.go.kr/data/15047676/fileData.do | 🔍 검색 |
|
||
| 의약품안전나라 (홈) | https://nedrug.mfds.go.kr/ | 🔍 검색 |
|
||
| 의약품안전나라 — 의약품등 검색 | https://nedrug.mfds.go.kr/searchDrug | 🔍 검색 |
|
||
| 의약품안전나라 — 고시/공고/알림 > 변경명령 | https://nedrug.mfds.go.kr/CCBAR01F012/getList/getItem?infoNo=20230089&infoClassCode=3 | 🔍 검색 |
|
||
| 의약품안전나라 — 고시/공고/알림 > (구)변경지시 | https://nedrug.mfds.go.kr/pbp/CCBAQ03/getItem?totalPages=1257&limit=10&page=2&bbsYn=Y&orderNo=&bbscttNo=4652 | 🔍 검색 |
|
||
| 의약품안전나라 — 고시/공고/알림 > 품목허가현황 | https://nedrug.mfds.go.kr/pbp/CCBAE01 | 🔍 검색 |
|
||
| 데일리팜 — 규제완화 여파…원료약 등록 1년새 237→653건 | https://www.dailypharm.com/Users/News/NewsView.html?ID=324241 | ✅ FETCH |
|
||
| 데일리팜 — 인도·중국산 등록비중 70% 돌파…외면받는 국산 원료약 | http://m.dailypharm.com/newsView.html?ID=307819 | ✅ FETCH |
|
||
| 데일리팜 — 필수약·공급중단 보고대상 의약품 'DMF 등록' 유예 | https://m.dailypharm.com/user/news/32332 | 🔍 검색 |
|
||
| 데일리팜 — 의약품 해외제조소 등록 안하면 2차 위반시 허가취소 | https://dailypharm.com/user/news/67688 | 🔍 검색 |
|
||
| 의학신문 — 식약처, 원료의약품 등록 제도(DMF) 해설서 발간 | http://www.bosa.co.kr/news/articleView.html?idxno=2141146 | 🔍 검색 |
|
||
| 히트뉴스 — 원료의약품 품질심사, 완제약 허가신청 돼야 동시 진행 | http://www.hitnews.co.kr/news/articleView.html?idxno=17459 | 🔍 검색 |
|
||
| 시넥스 — 원료의약품 등록 제도(DMF) | https://www.synex.co.kr/popup/pop_view.asp?bbs_code=2&bbs_idx=192 | 🔍 검색 |
|
||
|
||
### A.8 FDA / EMA / 국제 DMF 데이터
|
||
|
||
| 제목 | URL | 확인여부 |
|
||
|---|---|---|
|
||
| A Guide to Drug Master Files — BDO | https://www.bdo.com/insights/industries/life-sciences/a-guide-to-drug-master-files | ✅ FETCH |
|
||
| Drug Master File — Wikipedia | https://en.wikipedia.org/wiki/Drug_Master_File | ✅ FETCH |
|
||
| FDA Drug Master Files Directory — John Snow Labs | https://www.johnsnowlabs.com/marketplace/fda-drug-master-files-directory/ | ✅ FETCH |
|
||
| Programmatic access and formatting of FDA Orange Book data — paracelsus | https://manuelxduval.github.io/paracelsus/ | ✅ FETCH |
|
||
| openFDA — Drugs@FDA API | https://open.fda.gov/apis/drug/drugsfda/ | ✅ FETCH |
|
||
| openFDA — Downloads | https://open.fda.gov/data/downloads/ | ✅ FETCH |
|
||
| Drugs@FDA Database — catalog.data.gov | https://catalog.data.gov/dataset/drugsfda-database | ✅ FETCH |
|
||
| List of Drug Master Files (DMFs) — FDA | https://www.fda.gov/drugs/drug-master-files-dmfs/list-drug-master-files-dmfs | ⚠️ 404 |
|
||
| Drug Master Files (DMFs) — FDA | https://www.fda.gov/drugs/forms-submission-requirements/drug-master-files-dmfs | ⚠️ 404 |
|
||
| Orange Book Data Files — FDA | https://www.fda.gov/drugs/drug-approvals-and-databases/orange-book-data-files | ⚠️ 404 |
|
||
| Drugs@FDA Data Files — FDA | https://www.fda.gov/drugs/drug-approvals-and-databases/drugsfda-data-files | ⚠️ 404 |
|
||
| Guideline for Drug Master Files (DMF) — FDA | https://www.fda.gov/drugs/drug-master-files-dmfs/guideline-drug-master-files-dmf | 🔍 검색 |
|
||
| Types of Drug Master Files (DMFs) — FDA | https://www.fda.gov/drugs/drug-master-files-dmfs/types-drug-master-files-dmfs | 🔍 검색 |
|
||
| Drug Master Files Guidance for Industry (DRAFT) — FDA | https://www.fda.gov/media/131861/download | 🔍 검색 |
|
||
| FDA — Orange Book 필드 설명 (ucm129689#descriptions) | https://www.fda.gov/drugs/informationondrugs/ucm129689.htm#descriptions | 🔍 검색 |
|
||
| FDA — ucm079750 (Drugs@FDA 관련) | https://www.fda.gov/drugs/informationondrugs/ucm079750.htm | 🔍 검색 |
|
||
| FDA — media/103879/download (Orange Book) | https://www.fda.gov/media/103879/download | 🔍 검색 |
|
||
| Frequently Asked Questions on The Orange Book — FDA | https://www.fda.gov/drugs/drug-approvals-and-databases/frequently-asked-questions-orange-book | 🔍 검색 |
|
||
| Search Databases — FDA | https://www.fda.gov/industry/fda-basics-industry/search-databases | 🔍 검색 |
|
||
| What's New in Drugs@FDA — FDA | https://www.fda.gov/drugs/drug-approvals-and-databases/whats-new-drugsfda | 🔍 검색 |
|
||
| Orange Book (regulations.gov PDF, FDA-2019-P-1318-0003) | https://downloads.regulations.gov/FDA-2019-P-1318-0003/content.pdf | 🔍 검색 |
|
||
| Orange Book: Approved Drug Products with TE (FDA-2017-P-2044-0004) | https://downloads.regulations.gov/FDA-2017-P-2044-0004/content.pdf | 🔍 검색 |
|
||
| FDA Orange Book Drug Data — Kaggle | https://www.kaggle.com/datasets/thedevastator/fda-orange-book-drug-data | 🔍 검색 |
|
||
| Orange Book 101 — Fish & Richardson | https://www.fr.com/insights/ip-law-essentials/orange-book-101/ | 🔍 검색 |
|
||
| FDA Orange Book: Search Patents and TE Codes — Assyro AI | https://www.assyro.com/blog/fda-orange-book-guide | 🔍 검색 |
|
||
| FDA Drugs Database — Databricks Marketplace (John Snow Labs) | https://marketplace.databricks.com/details/cf63f2b8-da42-4ce5-a89b-9644cff0ef26/John-Snow-Labs_FDA-Drugs-Database | 🔍 검색 |
|
||
| FDA Approved Drug Products — NBER | https://www.nber.org/research/data/fda-approved-drug-products | 🔍 검색 |
|
||
| Drugs@FDA Database — HealthData.gov | https://healthdata.gov/FDA/Drugs-FDA-Database/8m24-qzcq | 🔍 검색 |
|
||
| Dmf List – FDA Quarterly Drug Master Files Spreadsheet | https://dmf-list.backgroundscheck.info/ | 🔍 검색 |
|
||
| Complete Guide to DMF Submissions — Celegence | https://www.celegence.com/guide-to-drug-master-files-dmf-submission-tips-best-practices/ | 🔍 검색 |
|
||
| Excipient DMF List — Pharma Excipients | https://www.pharmaexcipients.com/excipient-sources/excipient-dmf-list/ | 🔍 검색 |
|
||
| Drug Master File - US DMF FDA Guidance and Submission — FDA Pals | https://fdapals.com/services/dmf-fda-guidance/ | 🔍 검색 |
|
||
| FDA's Drug Master Files — Duke Medical Center Library | https://mclibrary.duke.edu/about/blog/2014-12-04/fda-drug-master-files | 🔍 검색 |
|
||
| What Does DMF Mean? Drug Master Files Explained (2026) — Tianming Pharm | https://www.tianmingpharm.com/what-does-dmf-mean/ | 🔍 검색 |
|
||
| US Drug Master Files (DMF) Database — PharmaCompass | https://www.pharmacompass.com/us-drug-master-files-dmfs | ⚠️ 403 |
|
||
| API Manufacturers | Suppliers | DMF | CEP — PharmaCompass | https://www.pharmacompass.com/manufacturers-suppliers-exporters | 🔍 검색 |
|
||
| PDMS API Manufacturers — PharmaCompass | https://www.pharmacompass.com/manufacturers-suppliers-exporters/pdms | 🔍 검색 |
|
||
| DMCTC API Manufacturers — PharmaCompass | https://www.pharmacompass.com/manufacturers-suppliers-exporters/dmctc | 🔍 검색 |
|
||
| PAS API Manufacturers — PharmaCompass | https://www.pharmacompass.com/manufacturers-suppliers-exporters/pas | 🔍 검색 |
|
||
| LOC API Manufacturers — PharmaCompass | https://www.pharmacompass.com/manufacturers-suppliers-exporters/loc | 🔍 검색 |
|
||
| API - Uses, DMF, Dossier, Manufacturer… — PharmaCompass | https://www.pharmacompass.com/apis-products | 🔍 검색 |
|
||
| Find APIs and FDF Suppliers — PharmaCompass | https://www.pharmacompass.com/find-suppliers | 🔍 검색 |
|
||
| 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 |
|
||
| Databases — EDQM | https://www.edqm.eu/en/databases | ⚠️ 403 |
|
||
| What is the CEP 2.0? — EDQM | https://www.edqm.eu/en/what-is-the-cep-2.0 | 🔍 검색 |
|
||
| User guide for Certification On-Line database (PA/PH/CEP (23) 56, May 2024) — EDQM PDF | https://www.edqm.eu/documents/52006/156629/User%20guide%20for%20Certification%20On-Line%20database%20(PA_PH_CEP%20(23)%2056,%20May%202024).pdf/26264db2-58b5-0cd4-7439-999bb86c4795?t=1718896649552 | 🔍 검색 |
|
||
| CEP 2.0: Changes from September 2023 — EDQM | https://www.edqm.eu/en/-/cep-2.0-changes-from-september-2023-to-information-published-in-certification-online-and-knowledge-databases | 🔍 검색 |
|
||
| Notifications, Revisions, Renewals and Sister Files — EDQM | https://www.edqm.eu/en/notifications-revisions-renewals-and-sister-files | 🔍 검색 |
|
||
| How do I know if I have the latest valid version of a CEP? — EDQM FAQ | https://faq.edqm.eu/pages/viewpage.action?pageId=1377010 | 🔍 검색 |
|
||
| Certification of Suitability (CEP) — EDQM | https://www.edqm.eu/en/certification | 🔍 검색 |
|
||
| Certification Policy Documents & Guidelines — EDQM | https://www.edqm.eu/en/certification-policy-documents-guidelines | 🔍 검색 |
|
||
| KNOWLEDGE Database — EDQM | https://www.edqm.eu/en/knowledge-database | 🔍 검색 |
|
||
|
||
### A.9 규제 인텔리전스 도구 · 뉴스레터
|
||
|
||
| 제목 | URL | 확인여부 |
|
||
|---|---|---|
|
||
| Global Regulatory Intelligence monthly newsletter — ICON plc | https://www.iconplc.com/insights/regulatory-intelligence/regulatory-intelligence-newsletter | ✅ FETCH |
|
||
| A Comprehensive Review of Regulatory Intelligence — PDA Letter | https://www.pda.org/pda-letter-portal/home/full-article/a-comprehensive-review-of-regulatory-intelligence-exploring-tools-and-program-maturities | ⚠️ 403 |
|
||
| FDA Tracker: Free Regulatory Intelligence & 483 Tracker — Leucine | https://leucine.ai/fda-tracker/ | ⚠️ 403 |
|
||
| Thomson Reuters Regulatory Intelligence | https://regintel-content.thomsonreuters.com/ | 🔍 검색 |
|
||
| Regulatory change tracking for compliance management — Ideagen | https://www.ideagen.com/solutions/quality/regulatory-intelligence/regulatory-change-tracking | 🔍 검색 |
|
||
| RegASK | Agentic AI Regulatory Intelligence Platform | https://regask.com/ | 🔍 검색 |
|
||
| AI Regulatory Intelligence — IONI AI | https://ioni.ai/ai-regulatory-intelligence | 🔍 검색 |
|
||
| Regulatory Intelligence Software: 5 Best Tools (2026) — IONI AI | https://ioni.ai/post/best-ai-regulatory-intelligence-tool | 🔍 검색 |
|
||
| Regulatory Intelligence 101: A Beginner's Guide — IONI AI | https://ioni.ai/post/regulatory-intelligence-101-a-beginners-guide-for-the-pharmaceutical-industry | 🔍 검색 |
|
||
| Regulatory Intelligence Software - Law Change Monitoring and Alerts — Visualping | https://visualping.io/regulatory-intelligence | 🔍 검색 |
|
||
| Regulatory Intelligence Tools — Compliance.ai | https://www.compliance.ai/solution/regulatory-intelligence/ | 🔍 검색 |
|
||
| Regulatory Intelligence Database, Software & Tools — Vistaar | https://www.vistaar.ai/blog/regulatory-intelligence-database-software-tools-for-compliance/ | 🔍 검색 |
|
||
| Vistaar (홈) | https://www.vistaar.ai/ | 🔍 검색 |
|
||
| Cortellis Regulatory Intelligence — Clarivate | https://clarivate.com/life-sciences-healthcare/research-development/regulatory-compliance-intelligence/regulatory-intelligence-solutions/ | 🔍 검색 |
|
||
| AgencyIQ by POLITICO | https://www.agencyiq.com/ | 🔍 검색 |
|
||
| Pharma and Biotech Regulatory Intelligence & Guidance Monitoring — DDi | https://www.ddismart.com/pharma-biotech-regulatory-intel/ | 🔍 검색 |
|
||
| Regulatory Intelligence and Updates — PharmaRegulatory.in | https://www.pharmaregulatory.in/regulatory-intelligence-and-updates/ | 🔍 검색 |
|
||
| Harnessing Regulatory Intelligence and Updates in Pharma Strategy — PharmaRegulatory.in | https://www.pharmaregulatory.in/harnessing-regulatory-intelligence-and-updates-in-pharma-strategy | 🔍 검색 |
|
||
|
||
### A.10 기타 (검색 결과에 등장했으나 본 주제와 무관하거나 참고용)
|
||
|
||
| 제목 | URL | 확인여부 |
|
||
|---|---|---|
|
||
| Dimethyl fumarate particles and pharmaceutical compositions thereof (USPTO 11291642) | https://image-ppubs.uspto.gov/dirsearch-public/print/downloadPdf/11291642 | 🔍 검색 (동음이의 DMF — 무관) |
|
||
| Substance of very high concern — Wikipedia | https://en.wikipedia.org/wiki/Substance_of_very_high_concern | 🔍 검색 (무관) |
|
||
| nedrug 통합검색 (깨진 인코딩 URL) | https://nedrug.mfds.go.kr/search?keyword=... | 🔍 검색 (스팸성 결과) |
|
||
| KPI Scorecard 이미지 1 — PK: An Excel Expert | https://www.pk-anexcelexpert.com/wp-content/uploads/2026/08/Flooring-Installation-KPI-Scorecard-2-1210x642.jpg | (이미지) |
|
||
| KPI Scorecard 이미지 2 — NextGen Templates | https://nextgentemplates.com/wp-content/uploads/2026/08/Flooring-Installation-KPI-Scorecard-3-scaled.jpg | (이미지) |
|
||
| KPI Scorecard 이미지 3 — NextGen Templates | https://nextgentemplates.com/wp-content/uploads/2026/08/Flooring-Installation-KPI-Scorecard-4.jpg | (이미지) |
|
||
| KHIDI 주간 공고 — 전체 쿼리스트링 변형 | 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 | 🔍 검색 (A.7 항목과 동일 문서) |
|
||
| KHIDI DMF 처리 절차 — 전체 쿼리스트링 변형 | https://www.khidi.or.kr/board/view?pageNum=1&rowCnt=10&menuId=MENU01872&maxIndex=99999999999999&minIndex=99999999999999&schType=0&schText=&categoryId=&continent=&country=&upDown=0&boardStyle=&no1=0&linkId=26605812 | 🔍 검색 (A.7 항목과 동일 문서) |
|
||
| 식약처 전자민원창구 (주간 원료의약품 등록현황 게시) | http://ezdrug.mfds.go.kr | (문서 기재, 미검증 ⚠️) |
|
||
| 국가법령정보센터 (기관 홈) | https://www.law.go.kr | 🔍 검색 |
|
||
| 식품의약품안전처 (기관 홈) | https://www.mfds.go.kr | 🔍 검색 |
|
||
| python.org (XlsxWriter 하이퍼링크 문서 예제에 등장) | http://www.python.org/ | (문서 예제) |
|
||
|
||
---
|
||
|
||
## 부록 B. 미해결 질문 / 실측 필요 항목
|
||
|
||
### B.1 소스 데이터
|
||
|
||
- [ ] `nedrug.mfds.go.kr/pbp/CCBAC03` 의 **페이지네이션·정렬 파라미터**를 실제 HTTP 요청으로 확정한다(WebFetch 로는 첫 페이지 렌더 결과만 확인함).
|
||
- [ ] 같은 페이지에 **엑셀/CSV 다운로드 버튼**이 있는지, 있다면 요청 형식(GET/POST, 파라미터)을 확인한다. 있으면 HTML 파싱보다 우선.
|
||
- [ ] `취소/취하구분` 컬럼의 **전체 값 도메인**을 실측한다(관측된 값은 `정상` 뿐). `취소`와 `취하`가 별개 값인지 확인.
|
||
- [ ] `대상의약품` 컬럼의 값 도메인 전체(`별표1`, `신물질` 외에 `별표1의2` 등이 있는지).
|
||
- [ ] 주간 공고 게시글(`nedrug.mfds.go.kr/bbs/117`)의 **첨부파일 형식(hwp/hwpx/xlsx/pdf)** 과 표 컬럼을 실측한다. 목록 뷰에서는 확인 불가였다.
|
||
- [ ] `ezdrug.mfds.go.kr` 의 주간 등록현황 게시 위치가 현재도 유효한지 확인(문서 기재는 오래된 경로일 수 있음).
|
||
- [ ] OpenAPI `getMdcDmfList01` 의 **갱신주기**를 실측한다(문서에 명시 없음). nedrug 공고와의 반영 시차도 측정.
|
||
- [ ] OpenAPI 응답에 `totalCount` 가 있는지, 있다면 `메타` 시트 교차검증에 쓸 수 있는지 확인.
|
||
- [x] OpenAPI 가 `최종변경일자`/`취소여부`를 제공하지 않는 것이 확실한지 재확인(응답 샘플 1건 확보). → **해소**: `design/00b-baseline-data-analysis.md` 가 API 7필드만으로 만들어진 실제 운영 산출물을 전수 분석해 확인. 필드가 없는 것은 맞지만, `발급일자` 이동과 레코드 소멸로 같은 정보를 간접적으로 얻을 수 있음이 실측으로 확인됐다(§8.0).
|
||
- [ ] `data.go.kr` **serviceKey 발급** 및 일일 호출 한도 확인.
|
||
|
||
### B.2 도메인 규칙
|
||
|
||
- [ ] **재공고(re-announcement)** 의 데이터상 표현을 확인한다. 해설서 통계표 주석 `( )의 숫자는 재공고된 숫자를 의미함` 의 실제 판정 기준은? 등록번호가 같은가 다른가?
|
||
- [ ] `연계 등록 원료의약품(사전등록)` 이 nedrug 공고 표의 어느 컬럼에 어떻게 표기되는지 실측(`대상의약품`? 별도 `등록구분`?).
|
||
- [ ] 총 등록 공고의 **`등록구분` 항**이 CCBAC03 페이지에도 있는지, 별도 페이지인지 확인.
|
||
- [ ] 등록번호 시행군 알파벳 중 **`J` 의 성분내일련번호 제외 규칙**이 파싱에 미치는 영향 검증(정규식이 `J` 를 안전히 처리하는지).
|
||
- [ ] 등록번호 괄호 표기 `(1~9)` 의 실제 출현 형태(범위 표기인지 단일 숫자인지) 실측.
|
||
- [ ] 「원료의약품 등록에 관한 규정」 **최신판(식약처 고시 제2024-89호, 2024.12.30)** 의 `[별표1]`·`[별표1의2]` 성분 목록을 확보해 `성분일련번호` → 성분명 역매핑 테이블을 만든다.
|
||
- [ ] 제조국가 표기 정규화 매핑표를 실데이터로 만든다(고유값 전수 조사 후 코드북 작성).
|
||
|
||
### B.3 시각화 · 구현
|
||
|
||
- [ ] 대시보드 열 폭 합계(168 문자폭)가 **실제 1366×768 / 1920×1080 에서 스크롤 없이 들어가는지** 스크린샷으로 검증.
|
||
- [ ] 한글 폰트 `맑은 고딕`에서 **28pt 숫자가 26 문자폭 타일에 넘치지 않는지** 실측(5자리 + 콤마).
|
||
- [ ] `add_sparkline` 의 `series_color` 가 `win_loss` 타입에서도 적용되는지 확인(문서에는 타입별 제약 언급 없음).
|
||
- [ ] 조건부서식 규칙 5개 + 데이터바 + 아이콘셋을 한 시트에 걸었을 때 **Excel 파일 열기 성능**(수만 행) 측정. 느리면 규칙을 `stop_if_true` 로 최적화하거나 정적 서식으로 전환.
|
||
- [ ] 상태색 배경을 **행 전체(20+열)** 에 적용하는 것이 인쇄 시 잉크를 과소비하지 않는지 확인. 필요하면 인쇄용 흑백 시트를 별도 제공(Paul Tol high-contrast 팔레트 사용).
|
||
- [ ] **CVD 시뮬레이터**로 실제 산출 xlsx 스크린샷을 protanopia/deuteranopia/tritanopia 3종 변환해 검증(Wilke 권고).
|
||
- [ ] `방식 B`(템플릿 피벗)를 실제로 만들어 `refreshOnLoad = True` 가 Excel 2016/365 양쪽에서 동작하는지 확인.
|
||
- [ ] `openpyxl` 의 `ws._pivots` 는 **비공개 API**다. openpyxl 4.x 에서 깨질 가능성 → 버전 핀 고정(`openpyxl==3.1.5`) 필요 여부 판단.
|
||
- [ ] `워치리스트` 시트 보호(`protect()`)를 걸었을 때 사용자가 A:G 입력 영역만 편집 가능한지(`unprotect_range` 조합) 검증.
|
||
- [ ] 파일 크기: 수만 행 × 26컬럼 + 조건부서식에서 xlsx 가 몇 MB 가 되는지 측정. 10MB 넘으면 `전체 누적` 을 별도 파일로 분리 검토.
|
||
|
||
### B.4 접근 차단 · 재조사 필요
|
||
|
||
- [ ] FDA 의 실제 **DMF 목록 xlsx 다운로드 URL** 을 찾는다(현재 안내 페이지 2개 모두 404).
|
||
- [ ] Orange Book / Drugs@FDA 데이터 파일 페이지의 **현재 유효 URL** 을 찾는다(둘 다 404).
|
||
- [ ] EDQM CEP 전체 다운로드 컬럼 목록을 **원문에서** 확인한다(현재 403, 검색 스니펫 기반).
|
||
- [ ] Leucine FDA Tracker 의 실제 UI/필드를 확인한다(403). 워치리스트 UX 벤치마크에 유용.
|
||
- [ ] PharmaCompass US DMF DB 의 컬럼·필터 구성을 확인한다(403).
|
||
- [ ] Chandoo.org 및 Big Excel Energy 본문(403 ×3)을 다른 경로로 확인해 KPI 카드 폰트 크기 권고(28–44pt)를 1차 출처로 검증한다.
|
||
- [ ] PDA Letter 의 regulatory intelligence 성숙도 모델(403)을 확인해 `스냅샷·로그` 필드 설계를 보강한다.
|
||
|
||
### B.5 운영
|
||
|
||
- [ ] 06:00 실행 시 nedrug 서버가 응답하지 않을 때의 **재시도·백오프 정책**과 `메타` 시트 기록 형식 확정.
|
||
- [ ] 전일 스냅샷이 없는 최초 실행(cold start)에서 `오늘 변경분` 시트를 어떻게 표시할지 결정(전량을 `신규`로 표시할지, `초기적재`라는 8번째 상태를 둘지).
|
||
- [ ] xlsx 생성 실패 시 Windows 알림 문구와 복구 안내 텍스트 확정.
|
||
- [ ] 리포트 보관 정책(며칠치 xlsx 를 남길지)과 `logs/archive/*.parquet` 롤오프 임계값 확정.
|
||
|
||
---
|
||
|
||
## 이 문서와 확정 설계의 관계
|
||
|
||
이 문서는 근거 자료(research)이며, 최종 xlsx 스펙의 정본(SSOT)은 `design/03-xlsx-report-spec.md` 다. 절별 상태를 정리한다.
|
||
|
||
| 절 | 상태 | 비고 |
|
||
|---|---|---|
|
||
| §0 한눈에 보기 | 부분 정정 | 시트 수·색·KPI 개수 서술을 "제안"으로 완화. 수치 자체는 §8.0 반영 후에도 유효 |
|
||
| §2 설계 원칙(Stephen Few, 5초 규칙, KPI 배치, 색 최소화, 데이터잉크비율, 대시보드≠스토리, 차트 선택) | **유효** | 출처 기반 일반 원칙. `00b` 실측과 무관 |
|
||
| §3 KPI 타일 해부 | **유효** | 예시 수치 일부를 `00b` 실측으로 갱신(§9.2 참조) |
|
||
| §4 규제 인텔리전스 실무 관점 | **유효** | |
|
||
| §5.1–5.6 (소스 3종, nedrug 15컬럼, 등록번호 체계, 법적 정의, 등록구분 값, 처리기간·수수료) | **유효**(역할 각주만 정정) | nedrug 도메인 지식 자체는 정확하다. 다만 "diff 판정의 주 소스" 라는 역할 규정은 §5.1 각주로 정정 |
|
||
| §5.7 OpenAPI 필드 — "API 는 diff 판정에 부족하다" | **반증 → 정정** | `00b` 실측으로 반증. 정정 내용은 §8.0 |
|
||
| §5.8 시장 통계(데일리팜) | **유효** | 외부 통계 인용. `00b` 와 시점·모수가 달라 병기 |
|
||
| §6 색 접근성 전체 | **유효**, 단 §6.6 값은 **충돌 표시** | research/06 §7.1 과 상태색 배경 HEX 가 다르다. 정본은 `design/03` 이 확정 |
|
||
| §7 해외 공개 데이터 벤치마크 | **유효** | |
|
||
| §8.0 API 필드 충분성 — 정정 | **신설** | 이번 정정의 핵심 근거 |
|
||
| §8.1 공통 규칙 | **유효** | |
|
||
| §8.2 `대시보드` | **유효**(컬럼 스펙 없음, 영향 없음) | |
|
||
| §8.3 `오늘 변경분` 컬럼표 | **정정(재작성)** | API 7필드 + 파생 필드 기준. 구안은 `<details>` 로 보존 |
|
||
| §8.4 `전체 누적` 컬럼표 | **정정(재작성)** | 동일 |
|
||
| §8.5 `성분별` / §8.6 `업체별` | **부분 유효** | 구조는 유효하나 `신청인_표시`·`신청인_전체` 등 컬럼명이 §8.3/8.4 의 `업체명` 표기와 다르다. `design/03` 확정 시 명칭 통일 필요 |
|
||
| §8.7 `워치리스트` | **유효** | |
|
||
| §8.8 `스냅샷·로그` | **유효** | |
|
||
| §8.9 `메타` | **유효** | 건수 검증 블록이 오히려 §8.0 의 756건 차이 실측과 정확히 맞아떨어진다 |
|
||
| §8.10 탭색·링크 요약 | **유효** | |
|
||
| §9 와이어프레임 | **유효** | 예시 수치를 `00b` 실측으로 갱신 |
|
||
| §10 피벗테이블 문제와 대안 | **유효** | |
|
||
| §11 구현 레시피 | **부분 정정** | §11.4 diff 코드(`DIFF_FIELDS`, 취하 판정)를 §8.0 기준으로 정정. 나머지 코드의 컬럼명(`업체키` 등)은 `design/03` 확정 시 §8.3/8.4 명칭과 재정합 필요 |
|
||
| §12 한국 제약 실무자 관점 | **유효** | |
|
||
| 부록 A 출처 목록 | **유효** | |
|
||
| 부록 B 미해결 질문 | **부분 해소** | B.1 의 OpenAPI 최종변경일자/취소여부 항목 1건이 `00b` 로 해소됨(체크 표시). 나머지는 유지 |
|
||
|
||
**남은 충돌**(이 문서 밖에서 해결 필요):
|
||
- 색 팔레트: research/06 §7.1 vs 본 문서 §6.6 — HEX 값 상이(§6.6 각주 참조).
|
||
- 시트 구조: research/06 이 암시하는 시트 수(7개 내외)와 본 문서의 8시트 제안이 다르다. 최종 확정은 `design/03-xlsx-report-spec.md`.
|