# 사이트맵 수집·정리 매뉴얼

공공저작물 실태조사 대상 기관의 홈페이지를 사이트맵 단위로 점검해 엑셀에 정리하는 작업의 **재현 가능한 절차서**.
한 기관이 끝나면 다음 기관도 이 문서만 보고 동일하게 만들 수 있어야 한다.

기준 양식: `자료_취합_예시.xlsx` (시트명 예: `04_구소영`).
완료 예시: `충청남도\1.계룡시\계룡시.xlsx`.

> # ⛔ 절대규칙 — `☑️ 검수완료` 기관은 손대지 마라
> 작업 전 반드시 `작업파일\_작업현황.md`를 먼저 읽고, **`☑️ 검수완료`로 표시된 기관은 재수집·수정·재계산·일괄작업·병합 무엇도 하지 않는다(읽기만).** 일괄 스크립트는 검수완료 대상을 반드시 제외한다. (현재: 계룡·공주·금산·논산)

---

## 0. 입력·산출물

### 입력
- **기관명** (예: "계룡시", "공주시")
- **베이스 URL** (예: `https://gyeryong.go.kr/kr/`)
- **저장 경로** (예: `충청남도\2.공주시\`)
- (선택) **담당자명** — 시트명 접두에 사용

### 산출물
- `{저장경로}\{광역}_{기관명}.xlsx` (예: `충청남도_계룡시.xlsx`. ★2026-06-01 파일명에 광역(도) 접두 부여 — 폴더명 `{번호}.{기관}`은 유지)
  - 시트명: `{번호}_{기관명}` (예: `01_계룡시`)
  - 행 1~2: `자료_취합_예시.xlsx`의 헤더 그대로 유지
  - 행 3~끝: 사이트맵에서 추출한 데이터

---

## 1. 단계 개요

| 단계 | 결과 | 진행 여부 |
|---|---|---|
| Phase 1 | 사이트맵 → D~K열 채우기 + 병합·정렬·스타일 | ✅ 정립 완료 |
| Phase 2 | L(게시판형태) · M(수량) | ✅ 정립 완료 |
| Phase 3 | N(저작물 유형) | ✅ 정립 완료 |
| Phase 4 | O(공공누리 부착) · P(마크 부착위치) · Q(마크 하이퍼링크) | ✅ 정립 완료 |
| Phase 5 | R(공공누리 연계) · S(비고) · T~W(검토) · Y~AA(완료체크) | 📝 사용자 수기 영역 |

---

## Phase 1 — 사이트맵 수집·정리 (현재 정립된 절차)

> ### ⛔ 1-0. 권위 트리 = 사이트맵이 아니라 "본 페이지 메뉴(GNB/LNB)" (프리랜서2 #3·#16 — 최우선)
> **사이트맵에는 중간 메뉴가 누락된 경우가 매우 많다. 무조건 실제 홈페이지 메뉴(GNB 메가메뉴 / 좌측 LNB)를 권위 트리로 삼아 전수 작성한다.** 사이트맵은 보조 참고일 뿐. 사이트맵만 믿으면 페이지·게시판 누락이 대량 발생(프리랜서2 지적 #16).
> - 절차: GNB/LNB를 라이브로 펼쳐(필요 시 Playwright JS렌더) 대→중→소→세부 트리를 추출 → 사이트맵과 **합집합**(사이트맵에만 있는 것도 보존). 누락 0이 목표.
> - 본문 탭(1-5b/c)·카테고리탭까지 합쳐야 진짜 전수. "사이트맵에 없으니 없다"는 금지.
> - 실제 적용: 장수(41→356)·교육청 3곳·전주 GNB 4차 등 — 전부 사이트맵 누락을 GNB/LNB로 복구.

### 1-1. 사이트맵 페이지 찾기 (보조 입력)

베이스 URL의 메인 페이지 푸터/헤더에서 **"사이트맵"** 링크를 찾는다. (단 1-0대로 **본 페이지 메뉴가 권위**, 사이트맵은 보조)
- 일반 패턴: `/kr/html/guide/0701.html` (계룡시 등 다수 충남 시·군)
- 다른 패턴 예: `/sitemap.do`, `/html/sub06/sitemap.html`, `/sitemap.html`
- 못 찾으면 메인 페이지의 모든 `a` 태그에서 텍스트에 "사이트맵" / href에 `sitemap` 이 들어가는 것을 검색

### 1-2. 사이트맵 HTML 구조 파악

대부분의 시·군 홈페이지는 다음 두 패턴 중 하나를 따른다.

**패턴 A — `dl > dt > dd > ul > li` (계룡시 형)**
```
.sitemap
└─ dl (대분류 1개)
   ├─ dt → 대분류명          → D 메뉴명
   └─ dd (중분류 1개)
      ├─ b > a → 중분류명      → E 카테고리(대)
      └─ ul
         └─ li > a → 소분류    → F 카테고리(중)
            └─ ul (중첩!) → li > a → G 카테고리(소)
                          └─ ul → H, I, J ...
```

**패턴 B — `ul.depth1 > li.depth2 > ul.depth3` (단순 중첩 ul)**
- depth 1 → D, depth 2 → E, depth 3 → F, depth 4 → G ...

> **핵심**: `<li>` 안에 `<ul>` 이 또 있을 수 있다(예: 계룡시의 `<ul class="s_4th_ul">`). 평면적으로 파싱하면 자식들이 F=빈칸으로 떨어지니, **반드시 재귀로 깊이를 추적**한다.

### 1-3. 컬럼 매핑

| 엑셀 열 | 의미 | 채워지는 값 |
|---|---|---|
| B | 순번 | 1부터 자동 부여, 행 삭제·삽입 후 재정렬 |
| C | 사이트명 | 기관명 고정 (예: "계룡시") |
| D | 메뉴명 | 사이트맵 최상위 (dt) |
| E | 카테고리(대) | 그 다음 깊이 |
| F | 카테고리(중) | 그 다음 깊이 |
| G | 카테고리(소) | 그 다음 깊이 |
| H | 카테고리(소·세부사항) | 그 다음 깊이 |
| I | 카테고리(소·세부사항2) | 그 다음 깊이 |
| J | 카테고리(소·세부사항3) | 가장 깊은 깊이 (그 이상은 J에 합침) |
| K | 주소(url) | 잎 노드의 href, 상대경로는 베이스로 절대화 |
| S | 비고 | 외부 도메인이면 "외부링크" |

### 1-4. URL 처리 규칙

- `javascript:...` , `#` → **빈 값**
- `http://`, `https://` 시작 → 그대로
- `/` 시작 → `https://{도메인}` + path 로 절대화
- 그 외 상대경로 → `urljoin(베이스, href)`
- **외부 판정**: `http(s)://` 이면서 도메인이 기관 도메인이 아니면 외부 → S열에 "외부링크"

### 1-5. 부모-자식 URL 중복 제거 (G 활성화 룰)

`<li>` 안에 중첩 `<ul>` 이 있는 경우 부모 li의 a와 첫 자식 li의 a가 **같은 URL**을 가리키는 경우가 많다(예: 계룡시 "자동차 민원안내" = 01050601, 첫 자식 "신규등록" = 01050601).

규칙(★2026-05-30 전 깊이로 일반화):
1. 사이트맵 파싱 결과를 단순 행 리스트로 만든다 (부모 행 1개 + 자식 행 N개).
2. **임의 깊이**에서, 부모 행의 가장 깊은 카테고리 컬럼을 lc라 할 때(lc+1은 빈칸), 직후 자식 행이 ① D~lc 값이 모두 같고 ② lc+1 컬럼이 채워져 있고 ③ **K열 URL이 정확히 같으면** → **부모 행 삭제**. (D→E·E→F·F→G 모두 동일 적용. D=E=F가 같은 URL인 다중 중첩은 가장 깊은 행만 남김)
3. 카테고리 텍스트(D/E/F)는 자식 그룹 병합으로 살아남는다.
4. URL이 다르면 부모 행 그대로 둠.

> 기존 구현(phase1)은 **F→G 한 깊이만** 처리했다. 중분류(E)·대분류(D)가 클릭 랜딩이고 그 URL이 첫 하위행과 같은 사이트(서산시 등 다수 eGov)는 D→E·E→F 중복이 남아 있었음 → `_스크립트/_dedup_all.py`(dry/run)로 전 기관 일괄 정리(2026-05-30, 26개 기관 −791행). 신규 수집 시 phase1 단계에서 이 일반화 규칙을 적용할 것.

### 1-5b. 본문 탭(서브내비) → 하위 카테고리 확장 ★

사이트맵에는 한 줄로만 나오지만, **그 페이지 본문에 탭(서브내비)이 있어 실제로는 여러 하위 페이지를 거느리는 경우**가 많다(예: 공주시 "납세자보호관" 페이지에 `납세자보호관 / 납세자권리헌장 / 납세자보호관 업무추진실적` 3개 탭). 이 탭들도 **한 단계 아래 카테고리로 수집**한다. (leaf가 F면 탭은 G, E면 F …)

**사이트마다 탭 마크업이 다르므로 매 기관 본문을 직접 열어 패턴을 확인**한 뒤 셀렉터를 등록한다. 1-10에 기관별 탭 셀렉터를 기록.

> **확인 절차:** `_tab_expand.py`는 흔한 탭 클래스만 자동 탐지하므로, dry-run에서 **"확장 0건"이 나와도 탭 없음으로 단정하지 말 것.** 대표 페이지 2~3개의 본문을 직접 열어 탭 UL의 class를 확인 → `TAB_CLASS_PATS`에 없는 이름이면 추가 후 재실행. 본문 탭이 실제로 없을 때만 "탭 없음"으로 확정한다.

**확장 대상 판별 규칙(4조건 모두 충족할 때만 확장):**
1. **탭 컨테이너 클래스**가 본문에 있다. (공주시: `ul.tab-ul`. 흔한 후보: `tab-ul`·`tabmenu`·`tab-menu`·`tablist`·`subtab` 등. UL 자신의 class로 판별 — div 래퍼만 보고 오인하지 말 것)
2. 탭이 **2개 이상**이고, **모든 탭 href가 실제 페이지 링크**다. → `#anchor`(인페이지 스크롤 탭, 예 `#nav1`)·`javascript:`·빈 href·**쿼리스트링만 다른 필터 탭**(예 `?catecd=01`)은 **제외**.
3. **현재 페이지 URL이 탭 URL 집합에 포함**된다(= 자기 자신의 탭 그룹). 자기와 무관한 상위 nav 오인 방지.
4. 탭 중 **엑셀에 아직 없는 URL이 1개 이상**. → 탭들이 이미 전부 사이트맵 행으로 존재하면(상위 공통 nav) **제외**(중복 방지). 예: 공주시 정보공개 6탭 nav는 이미 F행으로 다 있으므로 재확장 안 함.

**확장 방식:**
- leaf 컬럼(E~J 중 가장 깊은 값)을 부모로 두고, 탭들을 **leaf+1 컬럼**에 **탭 순서대로** 채운다. 부모 F는 자식들에 걸쳐 병합된다.
- 현재 페이지와 **같은 URL인 탭("on" 탭) = 원래 행 재사용** → 이미 채운 L~T(Phase 2~4) 데이터 보존, leaf+1 컬럼에 그 탭 라벨만 기입.
- 나머지 새 탭 = **신규 행**(L~T 비움 → Phase 2~4 별도 수집 대상).
- "on" 탭 라벨이 부모(F)와 같아 G에 동일 텍스트가 한 번 더 들어가도 그대로 둔다(사용자 확정 방식).

> **일괄 적용(2026-05-29):** 수집완료 39개 기관 전체에 `_스크립트/_tab_batch.py`(probe·dry·run 모드)로 일괄 수행. base/domain은 엑셀 K열 자동도출, 영동군만 `--weak-ssl`. 확장 11곳(+1,240행). **전북 14곳·제주시·충남/충북 나머지는 본문 서브탭 실제 없음**(클래스 무관 탐지 `_tab_classfind.py`로 검증 — 반복 UL은 전역 패밀리/유틸 nav). `--weak-ssl` 옵션을 `_tab_expand.py`·`_tab_phase234.py`에 추가, dry먼저→신규행>0일 때만 write로 0건 불필요 재기록 방지.

**스크립트:** `_tab_expand.py <엑셀> <BASE_URL> <도메인키워드> [--write] [--weak-ssl]` — 인자 없이 실행하면 확장 계획만 출력(dry-run), `--write`로 백업(`*_backup_tab전.xlsx`) 후 기입. 신규 행 L~P 수집은 `_tab_phase234.py <엑셀> <Phase234 모듈> <도메인> [body_sel콤마]`:
> - **개별형**(공주시 `_phase234.py`, `get_body(soup)`): body_sel 생략 — 예 `_tab_phase234.py 공주시.xlsx 충청남도/2.공주시/_phase234.py gongju.go.kr`
> - **일괄형**(`_chungnam_phase234_all.py` 등, `get_body(soup, sel)`): body_sel 지정 — 예 `_tab_phase234.py 논산시.xlsx _chungnam_phase234_all.py nonsan.go.kr "#txt,#contents,main"`
> - get_body 인자 개수를 자동 감지해 두 형태 모두 대응. body_sel은 해당 모듈 `SITES[기관]['body_sel']` 값을 그대로 쓰면 됨.

> ⚠ fetch는 **바이트(`resp.content`)로 받아 BeautifulSoup가 meta charset으로 인코딩 자동판별**할 것. `apparent_encoding`은 일부 페이지(공주시 읍면동)에서 오판해 한글이 깨진다.

#### 1-5c. 탭 정밀화 — CMS 위젯 클래스·인페이지탭·카테고리탭 ★ (누적 재작업 교훈)

> ⚠ **"탭없음 0건"은 신뢰 금지.** 서산(0→577)·부여(222→351)·서귀포(367→502)·제주시(271→363) 모두 "탭없음" 오판이었다. 반드시 대표 페이지 본문 UL을 직접 확인.

- **위젯 클래스 변형 — `[class*=tab]`는 `taps`/`tap`(b 없는) 위젯을 통째로 놓친다.** 제주교육청 `ul.taps-dt4/taps-dt5(tapScroll)`가 broad `[class*='tab']`에서 누락됨(2026-06-12 발견). broad 스캔은 **`[class*='tab'], [class*='tap']`** 둘 다 매칭. 벤더별 실제 클래스: `tab-ul`(공주)·`tab_menu`(서산)·`basic_tab`(부여)·`ul.taps`(장수)·`div.tabmenu`(제주시·제주교육청)·`menu_tabmenu`(단양)·`ul.item3/item4`(진안)·`tabTy2/tab_st`(충남교육청)·`taps-dt4/dt5`(제주교육청).
- **인페이지(#앵커) 탭 2종 구분**:
  - 단순 스크롤 내비(섹션 점프, 본문이 한 페이지) → **그 자체는 행 추가 안 함**(어문 섹션).
  - **콘텐츠 섹션탭**(tapScroll/taps-dt5 등 실제 하위 설명섹션을 탭으로 나눔) → **페이지 행이면 M=탭수** 기입(인페이지탭=설명섹션). **게시판이면 M=총게시물 유지**(탭은 부가).
- **카테고리 탭(`categoryCode`)**: 한 게시판(boardId 동일)을 카테고리로 나눈 탭(`categoryCode1=A/B/C/D`, 예 학교자료 운영계획서/운영현황/자체평가보고서/학교헌장) → **각 카테고리를 leaf+1 별행**(게시판, M=카테고리별 총건수, boardId는 같아도 categoryCode로 구분). **"전체"(categoryCode 없는 board)·주석처리(`<!-- -->`)된 li = 제외.**
- ⚠ **탭 ul 안의 li를 전부 분류하라.** 카테고리 탭 그룹에 **선행 일반메뉴(`index.jje?menuCd=` 소개/랜딩 페이지)** 가 섞여 있으면 그것도 별행(페이지)으로 수집 — 카테고리 4개만 보고 랜딩을 빠뜨리기 쉽다(학교자료 글로벌역량학교 등 15개 누락 사례, 2026-06-12). ⚠ 사이트 자체 오링크 주의(미래기술인재학교 첫 li가 타학교 menuCd를 가리킴 → 권위값=메뉴트리/소개페이지 menuCd 사용).
- ⚠ **탭 앵커는 행번호가 아닌 URL로 배치.** 랜딩셸 제거 등으로 행이 밀리면 옛 행번호(stale anchor) 기반 삽입이 탭을 엉뚱한 부모 밑에 흩뿌린다. 메뉴페이지를 라이브 broad스캔→self-탭 URL이 그 행이면 거기 앵커, 나머지는 자식. 기존 L/M/N/O는 **URL 매핑으로 보존**.
- **중첩 탭(탭의 탭)·한 페이지 다중 탭그룹**: self-그룹 전부 union 후 **fixpoint 반복 전개**(새 탭 0까지). 충남교육청 관·과장(바깥 부서대분류 + 안쪽 부서) 사례.

### 1-5d. 리다이렉트 중복 — URL(menuCd) 다른데 같은 페이지 ★

일부 사이트(예: 고창군 `index.gochang?menuCd=…`)는 **메뉴 카테고리(랜딩)를 클릭하면 실제 콘텐츠가 있는 자식 페이지로 자동 리다이렉트**한다. 그래서 "랜딩 행"과 "자식 행"이 **URL(menuCd)만 다르고 화면은 똑같다** — 같은 페이지인데 행이 둘로 부풀려진다.

> ⚠ **1-5(URL이 정확히 동일)로는 안 잡힌다.** URL 문자열이 다르기 때문. 리다이렉트로만 같은 페이지가 된다.

**탐지:** 각 행의 K URL을 **실제로 열어**(`requests` `allow_redirects=True`) **최종 `resp.url`을 비교**한다. 최종 URL(또는 최종 `menuCd`+`contentsSid`)이 같은데 행이 둘 이상이면 같은 페이지.
- 보조 신호: 두 행의 **게시물수 M 동일** + **F(또는 E)라벨 동일** + 한쪽이 카테고리 랜딩(`menuCd` 끝 `…000000` 또는 첫 자식).

**처리:** 리다이렉트해 **떠나가는 셸**(자기 `menuCd` ≠ 최종 `menuCd`, 그리고 최종 `menuCd`가 **다른 행으로 이미 존재**) 행을 **삭제**, 최종(타겟) 행은 유지. 셸의 카테고리 텍스트(D/E/F)는 자식행에 병합으로 살아남으므로 손실 없음. flatten→타겟 제외→D/E/F 재병합(1-6).

> 예(고창군 2026-05-31): 군정알림→공지사항·고창소식→보도자료·문화관광→관광공사·참여마당→고창군에바란다 등 **13건 셸 삭제(342→329)**. 전 행 fetch로 최종URL 충돌 탐지. 도구 `1.고창군/_del_dup.py`.

### 1-5e. 랜딩셸 삭제 — 병합 그룹 첫 행이 빈 다음깊이인 메뉴 껍데기 ★

카테고리 메뉴(대/중/소…)가 **자기 자식 목록만 보여주는 섹션 랜딩 페이지**일 때, 사이트맵에 그 랜딩이 한 행 + 자식들이 뒤따라 들어와서 **병합 그룹의 첫 행이 "껍데기"**가 된다. 이 첫 행을 삭제하고 카테고리 라벨은 첫 자식이 승계한다.

> ⚠ **1-5(URL 정확히 동일)·1-5d(리다이렉트로 같은 화면)로는 안 잡힌다.** 랜딩셸은 URL이 자식과 **다르고**(섹션 랜딩 `/field/welfare.do`·`/info/news.htm`式) 리다이렉트도 안 되는, **독립된 섹션 인덱스 페이지**다.

**삭제 조건 (4개 모두 충족):** 깊이 컬럼은 **D(대)→E(중)→F(소)→G→H→I→J(세부)**. 임의의 깊이 컬럼 **Cₙ**(n = D~I)에 대해
1. **Cₙ이 여러 행에 걸쳐 병합**돼 있고
2. 그 병합 그룹의 **첫 행에서 바로 다음 깊이 Cₙ₊₁(오른쪽 한 칸, 최대 J)이 비어있음**
3. **K가 섹션 랜딩 URL**(자식 목록만 거느린 카테고리 인덱스)
4. → 그 **첫 행(셸)만 삭제**, Cₙ 라벨은 첫 자식이 승계(병합 재계산).

즉 D병합+첫행 E빈칸 / E병합+첫행 F빈칸 / F병합+첫행 G빈칸 / G병합+첫행 H빈칸 … 전 깊이 동일하게 적용.

**⚠ 보존 예외 (삭제 금지):**
- **게시판·공공누리 부착 랜딩**(M 또는 O 값 있음) — 실제 콘텐츠.
- **단일행 리프**(병합 아닌데 다음깊이 빈칸) — 예산정보·민원발급정부24式 실제 페이지/사이트. **반드시 "병합된 그룹"일 때만** 셸로 본다.
- 1-5(URL 동일)·1-5d(리다이렉트)에 해당하면 그 룰로 처리.

**처리:** unmerge 전체 → 셸 앵커의 D~J 라벨을 첫 자식이 승계 → bottom-up 삭제 → 신규행 remap → K 하이퍼링크 value 재생성 → B 재부번 → 1-6 재병합. 검증: 잔여 셸 0·K링크 불일치/누락 0·병합겹침 0·B순번 연속.

**도구:** `_del_shell.py`(제주식 견고삭제, 전 깊이) · `_스크립트/_collapse_landing.py --safe`(전북 일괄, 순수 메뉴셸·페이지·M≤1·미부착만).

> 적용 이력: 전북 일괄 2026-05-31(`_collapse_landing.py` 98행) · 김제 −2 · 남원 −1 · 전주 −27 · 제주시 34행(E병합/F빈·`/field/welfare.do`) · 서귀포 36행(E병합/F빈·`/info/news.htm`) · 완주(검수時) · 정읍 23행(G병합/H빈, javascript 가짜탭 제거 병행). ⚠ 한 깊이 더 깊은 변형(G/H 등)도 같은 룰. 자식 전부 페이지면 부모로 **접는** 재귀합치기(`_collapse_recursive.py`, 고창 F그룹)는 **삭제가 아니라 병합**이라 별건.

### 1-6. 병합·정렬 규칙

#### 병합
- **D 열**: 같은 값이 연속되는 구간 전체를 병합 (대분류 단위)
- **E 열**: 같은 D 그룹 안에서 같은 E 값 연속 구간만 병합
- **F 열**: 같은 D+E 그룹 안에서 같은 F 값 연속 구간만 병합
- **G 이상**: 병합하지 않음
- **헤더 병합 보존**: `B1:R1`, `S1:W1`, `Y1:AA1` 세 개는 절대 풀지 말 것
- **★ 병합 순서: 반드시 F → E → D (D를 마지막에).** E·F 병합은 상위 컬럼(D, D+E) 값으로 그룹을 묶는데, D를 먼저 병합하면 openpyxl이 블록 2행부터 D를 None으로 비워 E 그룹키가 깨진다 → 각 대메뉴(D) 블록의 **첫 행 E가 단독으로 남는 누락** 발생. D를 맨 마지막에 병합하면 해결. (2026-05-29 발견·수정. 기존 산출물은 `_fix_emerge_all.py`로 재크롤링 없이 보정 완료)

#### 정렬
- 병합/단독 무관 — D · E · F · G 셀: **가로 가운데, 세로 가운데, 줄바꿈 ON**
- **K 열: 가로 왼쪽, 세로 가운데** (링크 본문 가독성)
- B(순번), Y/Z/AA(검토/완료) 등 헤더 기본 정렬 유지

### 1-7. 스타일 규칙

- **모든 행 높이 15** (헤더 1~2행 포함)
- **K열 하이퍼링크 활성화**:
  - 셀의 `hyperlink` 속성에 URL 지정
  - 폰트: 색 `0000FF`, 밑줄 `single`, 폰트명/크기는 기존 값 유지 (없으면 맑은 고딕 11)
- 외부링크인 행: S열에 "외부링크" 텍스트
- 셀 테두리·배경 등은 양식 파일의 기본 서식을 그대로 따른다

### 1-7b. 사용자 부분검수 보존 · 수정/신규셀 표기 · 국소삽입 ★ (누적 교훈)

재수집·정정 작업이 거의 항상 **이미 사용자가 일부 검수한 파일** 위에서 일어난다. 사용자 작업을 훼손하지 않는 것이 최우선.

- **부분검수 보존**: 사용자가 "N행까지(또는 1~N 순번) 검수했다/정답이다"라고 하면 그 범위는 **0셀 변경 보존**. 변경은 그 아래(또는 사용자가 지목한 범위)만. 작업 후 **diff로 보존 범위 0변경 검증**.
- **수정/신규셀 하늘색**: 내가 변경하거나 새로 추가한 셀은 `FFBDD7EE`(하늘색, `fg+bg 둘 다 지정` — fg만 주면 안 보임)로 표기해 사용자가 확인. **"적용은 내가 시킬 때"** 류 지시면 색칠만(=확인용)·실반영은 사용자 지시 시.
- **국소삽입(행 추가)**: 전체 재병합은 사용자 수동병합/B순번 스킴을 훼손하므로 회피. 절차 = **기존 병합 기록 → unmerge 전체 → `insert_rows` → generic remap(merge 시작:`+삽입수<r0`, 끝:`+삽입수≤r1` → 부모/H 병합 자동확장) → 재병합 → K 하이퍼링크 전행 재적용**(insert_rows 버그). 전역 B재부번은 **사용자 수기스킴 있으면 회피**(신규행 B=None, 검수 시 재부번). 워크북 간 스타일은 `cell._style` 직접복사 금지 → 스타일 객체 `copy`.
- **라이브 사본 확인**: 작업 전 `프리랜서1` vs `제출_*` 등 2~3벌 중 라이브를 mtime/md5로 확인([[submit_package_two_copies_gotcha]]).

### 1-8. 검증 체크리스트

작업 완료 후 다음을 확인한다:

- [ ] 행 1~2 헤더가 원본과 동일하고 병합 3개(`B1:R1` `S1:W1` `Y1:AA1`) 유지
- [ ] 시트명이 `{번호}_{기관명}` 형식
- [ ] D 열 병합 개수 = 대분류 수 (계룡시: 7개)
- [ ] E 열 병합이 D 그룹을 넘지 않음
- [ ] F 열 병합이 D+E 그룹을 넘지 않음
- [ ] G 활성화된 부모 그룹은 첫 자식이 부모와 같은 URL이 아님 (중복 제거됨)
- [ ] K 열에 빈 값 / `javascript:` / `#` 가 없음
- [ ] K 열 셀 클릭 시 브라우저로 이동되는지 (하이퍼링크 활성)
- [ ] 외부 도메인 행만 S='외부링크'
- [ ] 행 높이가 전부 15
- [ ] 순번(B)이 1부터 끝까지 연속

### 1-9. 작업 메모용 표 (다음 기관에서 채우기)

| 항목 | 값 |
|---|---|
| 기관명 | (예: 공주시) |
| 베이스 URL | (예: https://www.gongju.go.kr/) |
| 사이트맵 URL | |
| 대분류(dt) 개수 | |
| 총 잎 노드 수 | |
| 부모-자식 URL 중복으로 삭제된 행 수 | |
| 최종 행 수 | |
| 외부링크 행 수 | |
| F 병합 그룹(자식 있는 부모) 수 | |
| 특이 사항 | (예: 사이트맵 위치가 다른 패턴, 중첩 ul 클래스명 등) |

### 1-10. 기록된 사이트별 셀렉터·통계

#### 계룡시 (gyeryong.go.kr) — 2026-05-28 완료
- 사이트맵: `/kr/html/guide/0701.html`, 셀렉터 `.sitemap > dl > dt|dd > ul > li`, 중첩 `<ul class="s_4th_ul">`
- 본문(페이지/게시판 공통): `#txt`
- 페이징: `.pagination`
- 검색 input: `<input type="text" name="sval" placeholder="검색어를 입력해주세요">`
- 전체 건수 텍스트: `총 게시물 N 건` 또는 `총 N 건` (정규식 `총\s*(?:게시물\s*)?([\d,]+)\s*건`)
- 게시판 상세글 URL 패턴: `?mode=V&mng_no=...` 또는 `?mode=V&no=...&GotoPage=...`
- 통계 — 대분류 7, 총 행 442, F 병합 그룹 71, 외부링크 31, L 분포(페이지 288 · 게시판 122 · 사이트 31 · 접근 실패 1)
- 공공누리 부착 14건 (모두 Q=Y, 대부분 P=게시물). 미부착 396건
- O 이미지 기준 재판정(2026-05-29): 1유형 11·2유형 1(감사결과)·4유형 2(개인정보·공지사항). 링크 하드코딩(`licenseType4`) 불일치 12건 → S='링크주소 오기'

#### 공주시 (www.gongju.go.kr) — 2026-05-28 완료
- 사이트맵: `/kr/sitemap.do`, 셀렉터 `.sitemap dl > dt|dd > ul > li`
- **특이점**: a 태그 href가 더미(`#enURI1` 등), **실제 URL은 onclick의 `encodeURI('...')` 인자**. 정규식 `location\.href\s*=\s*encodeURI\(\s*['\"]([^'\"]+)['\"]\s*\)`
- 사이트맵 중첩 ul 없음 (F 병합 그룹 0개)
- 본문(페이지/게시판 공통): `#txt`
- 페이징: `.pagination`
- 검색 input: `<input type="text">` — 단, 페이지 헤더 검색이 본문(#txt) 안에는 없으므로 #txt 내 검사로 한정 시 정상
- 전체 건수 텍스트: `총 게시물 N , 페이지 1 / N` 형태. **"건" 미포함** → 정규식에 `총\s*게시물\s*([\d,]+)` 추가 필요
- 게시판 상세글 URL: `<a onclick="fn_search_detail('nttId'); return false;">` + `list.do` → `view.do?nttId={id}` 로 path 변환. 또는 첨부파일 td의 직접 `view.do?nttId=` 링크
- 통계 — 대분류 7, 총 행 306, F 병합 그룹 0, 외부링크 7, L 분포(페이지 218 · 게시판 81 · 사이트 7 · 접근 실패 1)
- 공공누리 부착 6건 (Q=Y 5건, Q=N 1건 — sub02_26_01.do 공공저작물 정책소개 1·2·3·4유형 마크 이미지가 a 태그 밖)
- **본문 탭(서브내비): `ul.tab-ul`** (실제 하위페이지). 2026-05-29 91개 그룹 확장 → **306행 → 592행(신규 286)**. `_tab_expand.py`(--write) + `_tab_phase234.py`(개별형, body_sel 생략)로 처리. 제외 케이스: `tab-ul type3 col*`(=`#nav` 인페이지 스크롤 탭)·정보공개 상위 6탭 nav(이미 사이트맵에 있어 중복)·쿼리필터 탭. 읍면동 페이지는 인코딩 자동판별(바이트) 필요. 신규 286행 L 분포(페이지 260 · 게시판 27), O 전부 미부착

#### 금산군 (www.geumsan.go.kr) — 2026-05-28 완료
- 사이트맵: `/kr/html/sub07/0701.html`, 셀렉터 `.sitemap > dl > dt|dd > ul > li`, 중첩 `<ul class="s_4th_ul">` (계룡시와 동일 구조 — sub 번호만 차이)
- 본문(페이지/게시판 공통): `#txt`
- 페이징: `.pagination`
- 전체 건수 텍스트: `총 N 건` (계룡시 정규식 그대로 적용)
- 게시판 상세글 URL: `?mode=V&site_dvs_cd=kr&menu_dvs_cd=...&code=...&no=...&GotoPage=...&group_at=...` — `mode=V` 조건만 봐도 충분히 추출됨
- Phase 1 통계 — 대분류 8(민원안내·정보공개·소통참여·금산소개·분야별정보·읍면·누리집가이드·부서안내), 원본 617행 → 부모-자식 URL 중복 93행 제거 → 최종 524행, F 병합 그룹 92, 외부링크 26
- Phase 2~4 통계 — L 분포(페이지 369 · 게시판 125 · 사이트 26 · 접근 실패 4), 공공누리 부착 65건(Q=Y 64·N 1), 미부착 429건
- 특이사항: DOM·게시판·셀렉터가 계룡시와 사실상 동일하여 계룡시 `_phase234.py`를 XLSX 경로만 바꿔 재사용

#### 무주군 (www.muju.go.kr) — 2026-06-02 F전개+탭+게시물KOGL 재수집
- 사이트맵: `/index.9is?contentUid=ff8080816db80238016dc8e98fa10ef6`, `div#sitemap > div > (h4=D) + ul>li(E) > ul.sitemap_blist>li(F)` **3단계**. (구 `parse_muju`는 2단계만 파싱해 F누락 → `5.무주군/_muju_rebuild.py`로 재구성)
- **정보공개 섹션 보강**: 사이트맵 h4가 `농특산물(s07)=실제 정보공개`로 오라벨 + 해당 8개 E의 F가 접힘 → **GNB 메가메뉴**(`#gnb li.m0X>div.gnbW>ul.depth2>li(E)>ul.depth3>li(F)`)로 F 보강. D 라벨은 사이트맵 원본 유지.
- **리다이렉트 셸(1-5d)**: 중분류 랜딩 fetch 최종 contentUid가 첫 자식과 같으면 셸 삭제. 외부는 netloc+path+query 비교.
- ★ **본문 하위탭 = `ul.deviceN`**(device2/3/4/5/6…). `li>a` 중 `class="on"`=현재페이지(=자기그룹). 조건: 탭≥2·실제 index.9is 링크·`on`존재·신규 contentUid≥1 → **leaf+1 컬럼으로 전개**(F leaf면 G, E leaf면 F). 단일 sibling 그룹(중첩 없음 — 탭페이지가 보이는 device는 같은 그룹의 on이라 contentUid 가드로 재전개 안 됨). 47개 그룹 전개.
- 게시판 상세글 URL: `view.9is?dataUid=...`(목록 `list.9is` → `./view.9is`). **상세 10개**(없으면 건수대로).
- ★ **공공누리(O/P/Q) 무주 검출**:
  - **게시판/페이지 자체 마크**: 본문의 `img_open(type|code){1~4}.(png|jpg|gif)` + `kogl.or.kr/info/licenseType{n}` 링크. (※ `01~04` 전 유형 = 공공저작물이용안내 **범례 → 미부착**, 4-1 규칙2)
  - **게시물 마크**: 상세페이지(`div.bbsView`) 하단 `div.kogl-wrap` 의 koglType1~5가 전부 `display:none`이고 **스크립트 `$("#koglType{N}").css("display","block")`** 로 활성유형 결정. **N=1~4=실제유형, N=5(opencode0)=자유이용불가=미부착, 숫자없음=미부착.**
  - **O** = 게시판·게시물에서 발견된 유형 **전부 합집합**, 숫자 오름차순 콤마(공백없음).
  - **P** = 게시판마크 있으면 `게시판`(둘 다 있어도 게시판 우선) / 게시물에만 있으면 `게시물`.
  - **Q** = 게시판·게시물 중 **하나라도 `licenseType` 링크가 있으면 `Y`, 전부 이미지/텍스트뿐이면 `N`**. (무주 게시물 koglType은 링크 없음 → 게시물만 부착이면 보통 N)
  - 도구: `5.무주군/_muju_phase234.py`(L/M/N/O/P/Q, device-aware, 상세10·bbsView·koglType스크립트).
- 처리 순서: `_muju_rebuild.py`(P1+탭) → `_muju_phase234.py`(P2~4) → `_스크립트/_redo_N_all.py run 무주군`(N 정교화).

---

## Phase 2 — 게시판형태(L) · 수량(M)

### 2-1. L 판별 규칙

K열 URL에 접속하여 본문에서 **다음 3요소 중 하나라도 있으면 "게시판"**, 아니면 "페이지"로 판별.

| 요소 | 의미 | 흔한 DOM 신호 |
|---|---|---|
| 전체 건수 | "전체 N건" / "Total N" 같은 카운터 | `<div class="total">`, `#totalCount`, `span.total b`, `<p class="page"><span class="total"><b>` |
| 페이징 | 1, 2, 3, ... 다음 페이지 버튼 | `.paging`, `.pagination`, `nav.paging`, `?GotoPage=` / `?pageIndex=` 형태 query |
| 검색 기능 | 키워드 검색창 + 검색 버튼 | `<input type="search">`, `<input name="searchKeyword">`, `.search_box`, `<form class="search">` |

판정 결과:
- 3요소 중 **하나라도 발견** → `L = "게시판"`
- 하나도 없음 → `L = "페이지"`
- S='외부링크'인 행 → `L = "사이트"` (URL 미접속, 위 검사 스킵)

> **★ 2-1a. 사이트(L) 판별 정밀화 (누적 교훈 + 프리랜서2 #7)**
> - **현재 창이 아닌 다른 창/사이트로 넘어가면 무조건 `L="사이트"`** (새창 `target="_blank"` 포함, M/N/O/P/Q 전부 빈칸). [[feedback_blank_newwindow_site]]
> - **예외 — 분야별 정보**: 분야별 정보 "페이지"가 그 사이트 안에 따로 구성돼 있으면 **그 페이지는 정상 작성**. 다만 분야별 정보 항목 안에서 **또 다른 사이트로 넘어가는 링크는 `사이트`**로 표기.
> - **외부·서브사이트 판별 = 최종 등록도메인이 기관 도메인과 다르거나, 같은 도메인이라도 비-www 서브사이트**(news.·school.·jinhak. 등). fetch(allow_redirects) 후 최종 도메인 기준이 안전 — 로고 alt 휴리스틱은 부서명 이미지를 로고로 오인(오탐). 교육청 룰: **좌상단 로고가 그 기관(○○교육청)이 아니면 사이트.**
> - epeople·국민신문고·정부24·위택스·부동산공시가격 등 외부 포털 임베드/이동 = 사이트.

> **★ 2-1b. 게시판 형태 판별 보강 (프리랜서2 #5·#10·#13)**
> - **게시글 목록이 비어 보여도(글이 안 들어가도) 게시판 형태면 `L="게시판"`** (예: 설문조사 페이지). 게시판 형태 정의는 2-1 3요소.
> - **게시판은 행 1개만.** 게시판 안의 **게시물(상세글)을 별도 행으로 추가하지 말 것**(예 진안 BBS_0000057 문화/전시시설 보드의 용담호사진문학관 등은 행 추가 X → 게시판 1행, M=글수, P=게시물).
> - 한 페이지에 **게시판이 여러 개**면 전부 작성(예 서귀포 `/info/news/law/law.htm` 게시판 3개). 1-5c 탭/섹션 스캔으로 누락 방지.

### 2-2. M(수량) 채움 규칙

| L 값 | M 값 |
|---|---|
| 게시판 | 페이지에 표시된 **총 게시글 수 (숫자만)** — 콤마·"건" 같은 단위 제거하고 정수로 |
| 페이지 | **1** |
| 사이트 | **빈칸** |

총 건수 추출 (우선순위):
1. **"전체/총 N건" 카운터** 셀렉터에서 텍스트 → 정규식 `(?:전체|총)\s*:?\s*([\d,]+)` 숫자만 정수 변환
2. 카운터가 없으면 **첫 글행의 번호(최신글번호 = 총건수)**. ⚠ 삭제분이 있으면 실게시물수보다 클 수 있음(사용자 컨벤션 = 번호 기준).
3. 페이징만 있으면 마지막 실페이지 번호 × 페이지당. 불가능하면 **0** + 비고(S)에 "수량 미확인".

> **★ 2-2a. M 카운트 함정 (누적 재작업 교훈)**
> - ⚠ **원본이 글수 대신 "페이지수"(≈글수/10)를 저장**한 경우(아산시) → list를 in-page fetch해 첫 글행 td.no 최댓값으로 정정. 게시판 M이 비현실적으로 작으면 의심.
> - ⚠ **번호 기반 카운트**: 통계/소통 게시판은 첫 데이터행의 번호(`td.hidden-md-down` 등)가 최신글번호=총건수. `act=view`·사이드바 "N건" 노이즈로 오집계 주의(서귀포시).
> - ⚠ **대형 edufine/외부연계 리스트 = 가짜 페이저**: `linkPage(N)`이 고정 상한이고 pageIndex가 범위를 넘어도 **서버가 마지막 페이지를 반복 반환**한다. 마지막 페이지 데이터가 **바뀌는 지점**(다음 페이지가 같은 데이터면 거기가 끝)으로 실제 끝을 확인. 여러 게시판이 **동일한 거대값**(예 6개 전부 16100)이면 = menuCd 필터가 안 걸린 통합뷰(같은 데이터)이므로 그대로 기록하되 의심·확인(서귀포교육청 계약현황).
> - **빈 게시판**: M=0, 본문/상세 글 0이면 N=`없음`.

### 2-3. 예외 처리
- **★ 로그인 리다이렉트 링크 → 행 전체(B~AA)를 연한 빨강 `FFC7CE`로 칠함**(삭제하지 말 것·사용자 검토용). (★사용자 2026-06-15 지시: 기존 "행 제거"에서 "색표시"로 변경 — 본인이 직접 판단해 정리). 판별: `requests.get(url, allow_redirects=True)` 후 `resp.url`에 `personalLoginPage`/`login` 포함 = 로그인 리다이렉트(본인인증·조회/신청·마이페이지·청구 등). ⚠ 탭전개 등으로 행이 밀릴 수 있으니 **K URL(norm)로 현재 위치를 재탐색**한 뒤 `PatternFill(FFC7CE)`로 B~AA(2~27열) 칠함. 단순 게시판인데 일부 글만 로그인이면 게시판 유지(색 안 칠함).
- 응답 실패 / 타임아웃 → `L=""`, `M=""`, `S`에 "접근 실패" 기록
- 자물쇠 페이지(게시물 0건 포함) → `L`은 정상 판별, `M=0`, N=`없음`, Phase 4의 O는 자동으로 "미부착"

---

## Phase 3 — 저작물 유형(N)

### 3-1. 기본 룰
- 본문에 **글자가 조금이라도 있으면 → "어문" 기본 포함**
- 추가 미디어 감지 시 콤마(공백 없음)로 이어 붙임. 순서: **어문 → 이미지 → 영상 → 오디오**
- **★ N 유형은 8종에서만 고른다(2026-06-12 프리랜서2 확정): `어문 · 이미지 · 영상 · 오디오 · 글꼴 · 3D · 기타 · 없음`.** 카테고리 밖 표기 금지(예 "동영상"X → `영상`).
  - **글꼴** = 폰트 파일/서체 배포, **3D** = 3D모델·VR, **기타** = 어느 유형도 아닌 것(아래 3-1b 링크전용·그래프단독 등).
- 결과 형식 예: `어문` / `어문,이미지` / `어문,영상` / `어문,이미지,영상` / `어문,이미지,오디오`
- 글자도 미디어도 전혀 없음 → `없음`. **게시판인데 게시물 0건 → `없음`**(프리랜서2 #17).

> ### ★ 3-1a. "이미지" 판정 핵심 원칙 (2026-06-01 당진 / 2026-06-12 프리랜서2 정정)
> **이미지는 "별도 정보를 주는 그림"만. 글을 그대로 옮긴 것·그래프는 이미지가 아니다.**
> - **이미지로 인정**: 사진(전경·모습·행사), **평면도/배치도**, **지도/약도**, **QR코드**, **악보**, **소식지·연보 표지**, **상징물(심벌마크·시그너처·캘리그라피)**, 게시판 업로드 사진, **포스터·카드뉴스**(그림+글자 함께). → **그림+글자가 같이 있으면 이미지**.
> - **이미지 아님 = 어문**: ①본문 텍스트를 그대로 이미지화한 **"○○ 한눈에"式 인포그래픽·흐름도·절차도·조직도**, ②**글자만 있는 이미지**(보고서 캡쳐·hwp 캡쳐본 등 = 글뿐이라 어문).
> - **★ 그래프(막대·선·파이)는 이미지가 아니다(프리랜서2 #4 — 기존 "그래프=이미지/어문" 모두 정정)**: **그래프 + 글자 = `어문`**, **그래프만(글자 거의 없음) = `기타`**.
> - 판단 기준: 그림이 텍스트에 없는 정보(공간배치·위치·실사진·악보 등)를 담으면 **이미지**, 텍스트를 그림으로 옮겼을 뿐이면 **어문**, 데이터 차트면 **어문/기타**.

> ### ★ 3-1b. 링크전용 페이지 = 기타 (프리랜서2 #2)
> **페이지(게시판 아님)에 링크/링크모음만 있으면 N = `기타`.** 단 **링크 + 어문(설명글)이 같이 있으면 `어문`**. (링크 목록만 나열한 바로가기 페이지 등)

### 3-2. 검사 대상

| L 값 | 검사 위치 |
|---|---|
| 게시판 | (1) 목록의 썸네일 `<img>` 존재 여부 → 이미지 (2) 상세글 **상위 6~10개** 본문에서 이미지·유튜브 (3) 상세글 본문에 유튜브 iframe/링크(`youtube.com`, `youtu.be`) → 영상 |
| 페이지 | 본문 영역 안의 `<img>`, `<iframe src=youtube...>`, 유튜브 링크 |
| 사이트 | 검사 안 함 (N 빈칸) |

> **★ 3-2a. N 이미지 정밀 검출 (누적 재작업 교훈)**
> - **게시판 N 이미지 = 상세글 표본의 "본문 컨테이너 div" 내부 콘텐츠 img만.** 본문영역 셀렉터로 **스코프 한정**(제주교육청 `div.bdvCntWrap`·서귀포 `/files|/storage|/board/`). ⚠ 본문 밖 **webCheck/추천위젯·QR·고정ID 위젯·썸네일·첨부 PDF뷰어·아이콘**은 제외(스코프 안 잡으면 위젯 오탐 127→실제 24, 제주교육청). 첨부가 아닌 **본문에 보이는 진짜 사진/갤러리썸네일**만.
> - **페이지 N = HTML `<img>` 휴리스틱만으로 부족** — 지도타일·통계차트·인포그래픽 등 **JS그래픽은 `<img>`가 아니라 누락**된다. → **Playwright 렌더**로 콘텐츠이미지(≥170×110, 노이즈 제외)+지도컨테이너(`.root_daum_roughmap`/iframe map)+PDF임베드 검출 후 **몽타주 시각판정**(과표시 방지).
> - **시각판정 기준 재확인**(3-1a 보강): 지도·평면도·악보·상징물(시기/시화/캐릭터)·소식지표지·캐릭터 일러스트·실사진=**이미지** / 흐름도·조직도·절차도·"한눈에"式 인포그래픽·**대진표 등 표-이미지**=**어문**.
> - **과표시 방지**: 역방향(이미지 과표시 → 어문) 정정은 사용자 검수영역. 게시판은 상위 표본이라 깊은 글 사진은 누락 가능(표본 한계 명시).

### 3-3. 노이즈 제외 (이미지 판정에서 빼는 것)
- 공공누리 마크 이미지 자체(`new_img_opentype`, `img_opentype`)
- 사이트 공통 헤더·푸터·로고 이미지 (본문 셀렉터로 한정하면 자동 제외)
- **장식/공통UI**: `move.png`(스크롤표시)·`no-img`/`noimg` 플레이스홀더·아이콘·불릿·버튼·배경(`ico_`·`bul_`·`btn_`·`bg_`·`see_btn` 바로보기 버튼)
- **로고류**: 사이트 로고·**파트너/연계기관 로고**(국민신문고 `epeople`·`open.go.kr`·노동권익센터 등)·`relation_item`·카피라이트(`copy_logo`)
- **헤드라인 텍스트이미지**(`headline` — 글자를 그림으로)·**팝업배너**(`popup`/popupZone)·**웹접근성 마크**(`wa_mk`)·트래킹픽셀(`tracer`)
- **"한눈에"式 설명 인포그래픽**(3-1a) — `한눈에`·`흐름도`·`절차도`
- ⚠ **"로고송"은 노래(오디오)** 이므로 로고 키워드로 제외하지 말 것 (`로고(?!송)`)

### 3-4. 영상 판별 기준
- `<iframe>` src에 `youtube.com/embed`, `youtu.be`, 또는 일반 비디오 호스팅(예: `vimeo.com`)
- `<a href>` 가 `youtube.com/watch?v=`, `youtu.be/...`
- `<video>` 태그 또는 `.mp4`, `.webm`, `.mov` 파일 링크

### 3-4b. 오디오 판별 기준 (2026-06-01 신설)
- `<audio>` 태그
- `.mp3` / `.wav` / `.m4a` / `.ogg` / `.flac` 파일 링크 — 정규식 `\.(?:mp3|wav|m4a|ogg|flac)\b`
  - ⚠ 다운로드 핸들러 쿼리형도 포함: `download.do?fileName=…in04_02_04_A.wav&…` (확장자 뒤 `&`가 와도 잡히게 `\b` 사용)
- 예: 당진 "플러그인당진 브랜드송"(로고송 wav 5개) → `어문,이미지,오디오`

---

## Phase 4 — 공공누리(O · P · Q)

### 4-1. 유형 추출 패턴

**패턴 A — 링크 (교차검증 전용, 유형 판정에는 쓰지 않음)**
- `<a href="...kogl.or.kr/info/licenseType{n}">` 에서 끝 숫자 `n`(1~4) 추출 → `{n}유형`

**패턴 B — 이미지 파일명 (★ 1순위 유형 판정 기준)**
- 정규식: `(?:new_)?img_open(?:type|code)(\d{1,2})\.(?:png|jpe?g|gif)` — `<img src>` · `style` 배경이미지 · raw HTML 전체에서 검사. 추출 숫자가 1~4일 때만 유효.
- **`opentype`·`opencode` 두 변형 모두**, 확장자 png/jpg/gif, **1~2자리 숫자** 모두 허용.
- 예: `img_opentype01.png`→`1유형`, `new_img_opentype04.png`→`4유형`, `new_img_opencode4.jpg`→`4유형`(서천군)

> **★ 유형 판정 우선순위 (2026-05-29 확정):**
> 1. **이미지명(패턴 B) 있으면 이미지 우선**(권위값). 링크가 존재하면서 이미지≠링크면 4-3b에 따라 비고 `링크주소 오기`. 일부 사이트는 게시물 템플릿에 링크를 하드코딩해 어긋난다(계룡시: 모든 링크 `licenseType4` 고정).
> 2. **이미지명으로 판정 불가 + 링크/텍스트 부착만 있으면 → 링크(패턴 A) 유형으로 인정.** 배지가 비표준 파일명(`sub040501_img11.png`)이거나 CSS 마크(`<div class="mark--area mark2">공공누리 2유형…`)인 사이트(천안시). 단 **링크 유형이 1·2·3·4 전부면 "공공저작물 개방 설명(범례) 페이지"이므로 미부착** 처리(증평군 `sub01_07_05.do`).
> 3. 이미지·링크 둘 다 없으면 `미부착`.

> **★ 4-1a. O 판정 함정 (누적 재작업 교훈 — 반드시 준수)**
> - ⛔ **본문 "공공누리 제N유형" 텍스트 정규식으로 판정 금지.** 본문 안내문·설명문("…제4유형으로 개방합니다")을 마크로 오탐해 **대량 false 부착**이 난다(진안·제주시 반복). **이미지마크(`img_opentype`/`new_img_opentype`)** 또는 **cclBox `span.ccl_img` 클래스(`open0N`)** 만 권위 신호. cclBox `<p>`문구를 굳이 쓰면 **"조건에 따라 이용할 수 있습니다"만 진짜**, **`open05`=자유이용불가=미부착**(진안 [[jinan_kogl_cclbox_open05]]).
> - **게시판 상세 추적 시 boardId/list URL = 상세 view링크의 최빈값**으로 정확히 잡는다. ⚠ 크롤러가 홈아이콘/공통 boardId를 dominant 계산에서 잘못 제외하거나(자료실 BBS_0000157 미검출), 페이지 menuCd와 안 맞는 boardId를 보면 **진짜 보드를 놓쳐 미부착 오판**(진안 r190·r205). boardId는 그 페이지 menuCd와 일치하는 view링크 최빈으로.
> - **기관군 예외 — 공공누리 전혀 안 쓰는 곳**: **시·도 교육청**은 홈·푸터·목록·상세 표본(≤10)을 다 봐도 마크 0 → **전부 미부착**(O=미부착, P/Q 빈칸). (시·군은 푸터 선언 흔함과 대조.) 이런 기관군은 마크 0 확인 후 전 행 `미부착` 일괄.
> - **anti-scraping 서버**: 대량 동시요청 시 '마크 든 가짜 페이지'를 섞어 반환하는 사이트(진안) → **rate-limit(sleep)+canary(마크없는 글 주기확인)+calm 재검증**으로 false 4유형 제거. throttle 한계로 대형보드는 calm 표본만 확정.

### 4-2. 탐색 전략

1. K URL에 접속 → **본문 영역**에서 패턴 A·B 검사
2. **L="게시판"인데 본문에서 미발견** → 게시판 목록에서 상세글 URL **상위 10개** 추출 → 각 상세글 본문에서 재검사 (건수가 많은 게시판일수록 10개 권장. 2026-06-02 5→10 상향)
3. 두 단계 합산해서 발견된 모든 유형 수집

본문 영역 셀렉터(사이트별 다름, 부록 참조 + 작업 시 확인):
- 계룡시 페이지/본문 — `#txt`, `.program--contents`
- 사이트별로 셀렉터가 다르면 그 사이트 작업 시 "1-9. 작업 메모용 표"에 기록

### 4-3. O열 (공공누리 부착) 결과 포맷

| 상황 | O 값 |
|---|---|
| 미발견 (자물쇠·게시물 0건 포함) | `미부착` |
| 단일 유형 | `1유형` (등) |
| 다중 유형 | **이미지(패턴 B) 기준** 발견된 모든 유형 **숫자 오름차순**, **콤마만 (공백 없음)** 으로 결합 → `1유형,4유형` |

> O열은 **이미지명(패턴 B) 우선**으로 집계한다. 이미지명으로 판정 불가하고 링크/텍스트 부착만 있으면 **링크 유형**으로 인정(4-1 우선순위 2). 다중 유형은 실제로 서로 다른 유형이 쓰인 경우에만 나온다.

### 4-3b. 링크-이미지 불일치 검증 → 비고(S)

이미지(패턴 B) 유형 집합과 링크(패턴 A) 유형 집합을 비교한다.

| 상황 | 처리 |
|---|---|
| 링크가 **존재**하면서 이미지 집합 ≠ 링크 집합 | S열 비고에 **`링크주소 오기`** 기재 (O열은 이미지 기준 그대로 유지) |
| 이미지 있음 + **링크 없음**(`licenseType` 링크 자체가 페이지에 없음) | **비고 비워둠.** 이미지가 권위값이므로 O열만 확정, 정상 처리 |
| 이미지 집합 = 링크 집합 | 패스 (비고 건드리지 않음) |
| 미부착(이미지·링크 모두 없음) | 검증 대상 아님 |

> **★ "링크주소 오기"는 링크가 존재하면서 이미지와 다를 때만 기재한다.** KOGL 배지 이미지만 있고 `kogl.or.kr/info/licenseType` 링크가 아예 없는 경우(충북 음성군·청주시 등 다수)는 "오기"가 아니므로 비고를 비워둔다. (2026-05-29 사용자 결정)

예: 이미지=`1유형`인데 링크가 `licenseType4` → 불일치 → `S = "링크주소 오기"`.
계룡시 부착 14건 중 12건이 링크 하드코딩 오류로 불일치 → 비고 기재.

### 4-4. P열 (마크 부착위치)

| 발견 위치 | P 값 |
|---|---|
| **L=페이지**의 본문에 마크 | **`게시물`** (프리랜서2 #1 — 페이지는 게시판이 아니므로 `게시판`이 아니라 `게시물`. 진안때 "페이지마킹→P=페이지" 정정됨) |
| L=게시판의 목록 본문에 마크 | `게시판` |
| L=게시판의 상세글에서만 마크 | `게시물` |
| 게시판에서 양쪽 모두 발견 | `게시판` (본문 우선) |
| 미부착 | 빈칸 |

### 4-5. Q열 (마크 하이퍼링크 활성 여부)

마크가 클릭 가능한 a 태그 안에 들어 있는지 확인:

| 케이스 | Q 값 |
|---|---|
| 패턴 A(`<a href="...kogl.or.kr...">`)로 발견 | `Y` |
| 패턴 B(이미지)로 발견했고, 그 `<img>` 의 부모 체인에 `<a href="...">` (의미 있는 링크)가 있음 | `Y` |
| 패턴 B로 발견했지만 `<img>` 가 a 태그 밖에 있거나, 부모 a의 href가 `#`/`javascript:` | `N` |
| 미부착 (O=미부착) | 빈칸 |

다중 유형이 발견되어 일부는 Y, 일부는 N인 경우 → **하나라도 Y면 Q=Y**.

### 4-6. 게시판 상세글 URL 추출

게시판 목록에서 상세글 링크 10개를 모아오는 패턴(사이트별 상이):
- 일반: `table.bbs_table tbody td.title a`, `table.tstyle_list td.txt_left a`
- href에 `View.do`, `view.do`, `seqRepeat=`, `mode=V`, `?bbtSn=` 같은 query
- 상대경로 → 현재 페이지를 base로 `urljoin`

추출 못 하면 → 상세글 검사 스킵, 본문 결과로만 판정.

---

## Phase 5 — 수기 입력 컬럼  📝

자동화 대상 아님. 검토 단계에서 채운다.

| 열 | 의미 |
|---|---|
| R | 공공누리 연계 |
| S | 비고(특이사항) — 자동: Phase 1 "외부링크", Phase 4 "링크주소 오기"(링크 존재 & 이미지≠링크일 때만; 링크 없음은 비움). 나머지는 수기 |
| T | 신유형 적용 가부 |
| U | 적용 사유 |
| V | 검토 대상 |
| W | 검토 확정(기관협의결과) |
| Y | 완료여부(Y, N) |
| Z | 확인 날짜 |
| AA | 수집 담당자 |

### 5-1. 결과영역 Y·Z·AA — 검수완료 시 일괄 작성 ★

`Y1:AA1`은 헤더 "결과", 2행 라벨은 `완료여부(Y,N) / 확인 날짜 / 수집 담당자`. **기관 검수가 끝나(`☑️ 검수완료`) 데이터가 확정되면** 이 세 열을 **모든 데이터 행에 동일하게** 채운다. (검수완료 5곳 계룡·공주·금산·논산·보령에서 확정된 패턴)

| 열 | 값 | 규칙 |
|---|---|---|
| **Y** 완료여부 | `Y` | 모든 데이터 행에 `Y`(빠짐없이 전부). 미완료 행을 따로 `N`으로 두지 않음 |
| **Z** 확인 날짜 | 검수일 | **검수완료 처리한 날짜**(datetime, 표시형식 `yyyy-mm-dd`). 기관 전체를 한 날짜로 통일(예 보령 `2026-05-31`) |
| **AA** 수집 담당자 | 담당자 | **광역별 담당자**: 충남·충북=`프리랜서1`, 전북·제주=`프리랜서2` (대시보드 담당자와 동일). ※ 기존 충남 4곳은 plain `프리랜서`로 돼 있으나 신규는 1/2 구분 표기 |

- **대상 행 판별**: `B`(순번)이 정수인 행만(3행~). 트레일링 빈행·잡행은 제외(공주 사례: 실데이터 400행만 채움, 이하 빈행 미기입).
- **도구**: `작업파일/_fill_YZAA.py <엑셀> <YYYY-MM-DD> <담당자> [--write]` — dry-run으로 행수 확인 후 `--write`(백업 `*_backup_YZAA전.xlsx` 생성, 기존 셀 스타일/테두리 보존, 값·표시형식만 기입).
- ⛔ `☑️ 검수완료`로 이미 잠긴 기관은 재기입 금지(읽기만). 이 작업은 **그 기관을 검수완료로 막 전환하는 시점에 1회** 수행.

---

## 부록 — 자주 마주치는 변수

### 사이트맵 셀렉터 후보
1. `.sitemap` (계룡시)
2. `#sitemap`, `div.sitemap_wrap`, `.site_map`
3. `main .content ul` (드물게 직접 ul만 있는 경우)
4. 사이트맵 페이지가 없으면 → 메인 GNB(글로벌 내비) 메뉴 사용

### 본문(상세 페이지) 셀렉터 후보
- 계룡시: `#txt`, `.program--contents`
- 보건복지부: `#contents_body`, `article.board_view`
- 외교부: 본문 영역 클래스 다양 — 사이트별 확인 필요

### URL 절대화 시 주의
- `urljoin` 사용 시 베이스에 슬래시(`/`)를 정확히 붙여야 한다 (`BASE + '/'`)
- `?bbtSn=` 같은 query만 다른 상대 URL — 현재 페이지를 베이스로 결합

### 외부 도메인 화이트리스트(시·군 공통)
정부 공통 서비스. 외부링크이지만 자주 등장하므로 인지만:
- `gov.kr` (정부24)
- `cheongwon.go.kr` (청원24)
- `wetax.go.kr` (위택스)
- `epeople.go.kr` (국민신문고)
- `safetyreport.go.kr` (안전신문고)
- `car365.go.kr` (자동차민원대국민포털)
- `kogl.or.kr` (공공누리)

---

## 변경 이력

- 2026-05-28 — Phase 1 정립 (계룡시 1차 작업 기준). Phase 2~4 자리만 잡음.
- 2026-05-28 — 공주시 Phase 1~4 완료 (306행, 부착 6건). 사이트맵 URL이 onclick encodeURI에 들어가는 패턴 발견, 추출 로직에 반영. 매뉴얼 1-10에 기록.
- 2026-05-28 — 계룡시 Phase 2~4 자동 채우기 완료 (442행, 부착 14건). 셀렉터 1-10에 기록.
- 2026-05-28 — Phase 2~4 정립:
  - L: 전체 건수 / 페이징 / 검색 **3요소 중 하나라도 있으면 게시판**
  - M: 게시판은 총 게시글 수(숫자만), 페이지는 1, 사이트는 빈칸
  - N: 어문 기본 + 이미지/영상 콤마(공백 없음) 결합. 게시판은 상세글 상위 10개까지 검사
  - O: `licenseType{n}` 링크 + `new_img_opentype{NN}.png` 이미지 양쪽 검사, 다중 유형은 오름차순 콤마(공백 없음) — 예: `1유형,4유형`
  - P: 본문 발견 → `게시판`, 상세글 발견 → `게시물` (양쪽이면 본문 우선)
  - Q: 마크가 a 태그(유효 href) 안에 있으면 `Y`, 아니면 `N`
- 2026-05-29 — **Phase 4 O 판정 기준 변경**: O열 유형은 **이미지(`img_opentype{NN}`)만으로 판정**(링크 숫자 합산 폐지). 링크(`licenseType{n}`)는 교차검증용으로만 사용하고, **이미지 숫자 ≠ 링크 숫자면 S열 비고에 `링크주소 오기` 기재**(같으면 패스). 계룡시 게시물 템플릿이 KOGL 링크를 `licenseType4`로 하드코딩해 실제 배지(이미지)와 어긋나는 오탐을 막기 위함. 4-1·4-3·4-3b 반영.
- 2026-05-29 — **"링크 없음" 처리 규칙 추가**: `licenseType` 링크가 페이지에 아예 없는 경우(이미지만 존재)는 `링크주소 오기` 비고를 **달지 않는다**(비움). "링크주소 오기"는 링크가 **존재하면서** 이미지와 다를 때만 기재. 충청남도 2~15 + 충청북도 1~11 일괄 재검토(`_recheck_kogl_all.py`)에 적용. 4-3b 반영.
- 2026-05-29 — **본문 탭(서브내비) 하위 카테고리 수집 규칙 정립(1-5b 신설)**. 사이트맵에 한 줄로만 나오는 페이지가 본문 탭으로 하위 페이지를 거느리는 경우, 4조건(탭 클래스·실제링크·자기그룹·신규URL) 충족 시 leaf+1 컬럼으로 확장. 인페이지 `#anchor`·쿼리필터·이미 사이트맵에 있는 상위 nav는 제외. `_tab_expand.py`(확장) + `_tab_phase234.py`(신규행 L~P 수집). 공주시 적용: 91개 그룹 확장, 306행 → 592행(신규 286). 다른 시·군은 보류 후 동일 로직 적용 예정.
- 2026-05-31 — **1-5d 신설(리다이렉트 중복)**: menuCd 등 URL은 다른데 자식으로 자동 리다이렉트되어 화면이 같은 페이지인 셸 행 삭제. 전 행 fetch로 최종 `resp.url` 비교. 고창군 13건(342→329).
- 2026-06-01 — **Phase 3 N 판정 정교화(당진시 사용자 확정)**: ①**3-1a 이미지 판정 핵심 원칙** — 별도 정보를 주는 이미지(사진·평면도·지도·QR·악보·소식지표지·상징물)만 "이미지", **글을 그대로 옮긴 "한눈에"式 설명 인포그래픽은 어문**. ②**3-3 노이즈 제외 확장** — 로고·파트너/연계기관 로고·아이콘·헤드라인 텍스트이미지·바로보기 버튼·팝업배너·웹접근성마크·트래커 추가('로고송'은 노래라 예외). ③**3-4b 오디오 유형 신설** — `<audio>`·`.mp3/.wav/.m4a` 링크(다운로드 쿼리형 포함) → `오디오`. 도구 `5.당진시/_redo_N.py`(r22~끝 19행 변경). [[feedback-n-image-rule]]
- 2026-06-01 — **작업파일명 광역 접두**: `{기관}.xlsx` → `{광역}_{기관}.xlsx`(예 `충청남도_계룡시.xlsx`) 42개 일괄 rename(`_스크립트/_rename_prefix.py`). 폴더명은 유지. 참조 스크립트 전부 갱신: 리터럴 경로 39파일·64건 치환 + 동적 빌더 5곳(`_dedup_all.xpath`·`_tab_batch.xlsx_path`·`_jeonbuk_phase234.site`·phase1 3개·`_fix_emerge`)에 `{prov}_` 접두 삽입. dd.xpath·3개 phase234 SITES 경로 누락 0 검증. (백업 `*_backup_*.xlsx`는 미개명)
- 2026-06-01 — **수집완료 전 기관 N 일괄 재검토**(검수완료 5곳+당진 제외 **36곳**, `_스크립트/_redo_N_all.py`). 3-1a/3-3/3-4b 규칙으로 약 **1,211행 N 변경**(장식·로고·파트너로고·아이콘·SNS공유버튼·KOGL마크·"한눈에/흐름도/절차도" 인포그래픽·파일아이콘 → 어문 강등 / 오디오·영상 추가 / **거짓 M=0 게시판은 내용기반 판정**=글 있으면 어문, 진짜 글0개만 없음). 벤더별 fetch 시그니처(충남 `fetch(url)` vs 충북·전북 `fetch(session,url)`+make_session) 자동 흡수. 동시성4+재시도3으로 throttle 거짓'없음' 차단. 기관별 백업 `*_backup_N재검토전.xlsx`.
- 2026-06-11 — **1-5e 신설(랜딩셸 삭제)**: 카테고리 컬럼 Cₙ(D~I)이 병합돼 있고 그 첫 행의 다음 깊이 Cₙ₊₁(최대 J)이 빈 "섹션 랜딩 껍데기"면 첫 행 삭제(라벨은 첫 자식 승계). 1-5(URL 동일)·1-5d(리다이렉트)와 달리 URL이 자식과 다르고 리다이렉트도 안 되는 독립 섹션 인덱스. 보존=게시판/공공누리 부착·단일행 리프(병합 아님). 도구 `_del_shell.py`(전 깊이)·`_collapse_landing.py --safe`. 기적용: 전북 일괄(98)·김제−2·남원−1·전주−27·제주시34·서귀포36·완주·정읍23(G/H). 이전부터 사이트별로 반복 요구돼온 규칙을 정식 등재.
- 2026-06-12 — **★ 프리랜서2(발주처) 공식 검수 지침 17항 반영([[feedback_pro2_review_guidelines]], 기존 관행 덮어쓰는 정정)**: ⛔**1-0 권위트리=사이트맵 아니라 본 페이지 메뉴(GNB/LNB)**(누락 多, #3·#16) · **3-1 N유형 8종**(어문/이미지/영상/오디오/**글꼴/3D/기타**/없음, #6) · **3-1a 이미지 세분**(포스터·카드뉴스=이미지·**글자만 이미지=어문**·**그래프만=기타·그래프+글자=어문**, #4 — 기존 "그래프=이미지" 정정) · **3-1b 링크전용 페이지=기타**(링크+어문=어문, #2) · **2-3 로그인/본인인증 페이지=행 제거**(#15, 기존 "L=페이지 유지" 정정) · **2-1b 게시글 안 보여도 게시판형태=게시판**(#5)·**게시판은 1행, 게시물 행 추가 금지**(#10)·**한 페이지 다중게시판 전부**(#13) · **2-1a 다른창=사이트+분야별정보 예외**(#7) · **4-4 P열 페이지마크=게시물**(#1, 진안 "P=페이지" 정정). #8 아이콘제외·#11 사이트뒤공백·#14 게시물수·#17 게시물0=없음은 기존 반영 재확인.
- 2026-06-12 — **누적 재작업 기반 정밀화 5종 등재**(시·군 30+곳·교육청 3곳 경험): ①**1-5c 탭 정밀화** — `[class*=tab]`가 `taps`/`tap`(b없는) 위젯 누락(`tab`+`tap` 둘 다)·인페이지#앵커 콘텐츠탭(페이지 M=탭수)·카테고리탭(`categoryCode` 각 별행 게시판)·탭 ul의 선행 일반메뉴(랜딩) 누락 주의·앵커는 URL로 배치·중첩탭 fixpoint. ②**2-1a 사이트 판별** — `_blank`=무조건 사이트·최종도메인/서브사이트 기준·교육청 로고룰. ③**2-2a M 함정** — 번호=최신글번호·페이지수 저장버그(아산)·edufine 가짜페이저(동일 거대값=통합뷰)·빈게시판 M0. ④**3-2a N 검출** — 게시판=본문 div 스코프 img만(위젯·첨부 제외)·페이지=렌더+몽타주 시각판정(JS그래픽 누락 방지)·표이미지/조직도=어문. ⑤**4-1a O 함정** — 본문 "제N유형" 텍스트정규식 금지(이미지마크/ccl_img만)·boardId 상세 view최빈·교육청 등 기관군 전부 미부착·anti-scraping rate-limit+canary. + **1-7b** 사용자 부분검수 보존·수정/신규셀 하늘색(fg+bg)·국소삽입(generic remap). SKILL.md 핵심규칙·절대규칙·함정에도 요약 반영.
