# 국가조달 데이터베이스 분석 가이드 (LLM·에이전트용)

이 문서는 PostgreSQL에 축적된 조달 데이터를 **처음 접하는 LLM/에이전트가 올바르게 이해하고 안전하게 분석**할 수 있도록 작성된 안내서입니다. 사람용 화면이 아니라 데이터 해석 규칙이 목적입니다.

- 작성일: 2026-10-04
- 대상 서버: 운영 PostgreSQL (`audit-pg` 컨테이너), 웹 뷰어 (`audit-web`, 포트 3100)
- 관련 DB: `national_audit` (수집·분석 데이터), `audit_review` (검토 결정)
- 읽기 전용 계정: `explorer_ro` (SELECT 전용), 검토 쓰기는 `review_rw` (audit_review 전용)
- 웹 기준 문서 발생지: 이 저장소 `db/migrations/001~015`, `shopg2b/`, `analysis/`, `web/`

---

## 0-1. 접속 방법 (어디에, 어떻게 붙는가)

- **공식 데이터 원본은 서버의 `audit-pg` 컨테이너뿐**이다. 외부로 개방된 PostgreSQL 포트는 없다.
- 접속은 두 가지 중 하나다:
  1. 서버(175.207.12.35) 안에서 실행: `docker exec -it audit-pg psql -U explorer_ro -d national_audit`
  2. 외부에서는 SSH 터널: `ssh -L 5432:localhost:5432 ubuntu@175.207.12.35` 후 `postgresql://explorer_ro:<비밀번호>@localhost:5432/national_audit`. 비밀번호는 이 문서 같은 공개 문서에 적지 않는다 — 팀 비밀 채널 참조.
- **쓰면 안 되는 것**: 수집기가 도는 개발자 로컬 머신의 `national_audit_pg`(localhost:5433, `audit`). 옛 문서들의 `audit:audit@localhost:5433` 연결 문자열은 **2026-10-05에 폐기**됐다(외부 바인딩 제거·비밀번호 교체). 그 주소로는 어떤 경로로도 외부 접속이 안 되며, 그 DB에 붙는 팀 작업을 새로 만들지 않는다.
- **서버 복제본 신선도 확인**: `SELECT max(created_at) FROM listing_version;`의 시각이 1~2일 이상 오래됐으면 분석을 멈추고 복제 갱신을 요청한다(담당: 태일). 낡은 복제본 대신 개발자 로컬 원본에 직접 붙는 우회는 금지.

---

## 0. 30초 요약

1. **두 개의 공식 몰을 수집했다**: 조달청 종합쇼핑몰(shop.g2b.go.kr, ~104만 등재)과 디지털서비스몰(digitalmall.g2b.go.kr, 10,180 등재). 둘 다 같은 정규화 모델에 들어 있고 `listing_source`로 구분한다.
2. **가격에는 종류가 있다**: `price_kind='registered_unit'`(등재 단가)과 `'catalog_reference'`(카탈로그 계약 참고가, 협상 전 참고값)를 절대 합쳐서 집계하지 않는다.
3. **판매량 지표 `ntsl_qnty`는 실제 수량이 아닐 수 있다**: 집계기간·포함 기준 미검증. 지출액 계산에 그대로 곱하지 않는다.
4. **실제 납품(지출) 데이터는 별도 스키마** `procurement_delivery.delivery_order_line`(약 285만 행, 기관·계약·실제 단가·수량·금액)에 있다. 공식 자료요청 CSV 적재분이다.
5. **분석 산출물(`anomaly_*`)은 후보 생성기 결과이지 판정 결과가 아니다.** 사람 검토 결정은 `audit_review` DB에 있다.
6. **조인은 논리 컬럼으로 한다.** 물리 FK는 매우 적게(3개)만 만들어져 있다. 아래 조인 레시피를 따른다.

---

## 1. 데이터 지도 — 층위 구조

```
[L1 수집 원천]   종합쇼핑몰 내부 API / 디지털서비스몰 내부 API
        │            (shopg2b 수집기가 스냅샷 단위로 수집, 초당 속도 제한 하 운영)
        ▼
[L2 정규화]      item(상품) · listing(등재) · listing_version(가격/조건 버전)
                 vendor(업체) · shop_category / source_category(분류)
                 · 첨부: attachment → attachment_section(텍스트) / attachment_version(내용 버전)
                 · 관측: collection_run/snapshot · listing_observation(스냅샷별 관측)
                 · 출처: listing_source(shop|digital, 권위)
        ▼
[L3 연결]        item_xref(상품 교차 후보, 2단계 정밀도)
                 contract_xref(계약번호 공유)
                 supplier 겹침(287사 — 조인으로 계산)
        ▼
[L4 분석]        anomaly_candidate / market_judge / hedonic / score / label (public)
                 외부 리테일 비교·정책 검토는 웹 JSON 페이로드로도 존재
        ▼
[L5 실적/판정]   procurement_delivery.delivery_order_line (실제 납품요구·실제 금액)
                 audit_review.review_decisions + review_decision_history (사람의 판단)
```

**오해 금지**: L4의 결과는 L5가 아닙니다. L4는 "이상해 보이는 후보"이고, L5만이 실제 거래/판단의 근거입니다.

---

## 2. 코어 엔티티와 "한 행의 단위"

| 테이블 | 한 행의 단위 | 기본 키 | 핵심 주의 |
|---|---|---|---|
| `item` | 상품(물품식별번호 8자리). 두 몰 공용 | `item_idnf_no` | 상품명 파싱 결과(`mnftr_etps_nm`, `model_raw`, `spec_raw`)는 추정. `parse_status`가 ok가 아니면 신뢰도 낮음 |
| `listing` | 등재 = 어떤 업체가 어떤 계약으로 어떤 상품을 올렸는가 | `ctrt_item_mng_no` | `is_active=true`는 "최신 목록 스냅샷에서 보임"이지 판매 중 보증이 아님 |
| `listing_version` | 등재의 가격·조건 버전(SCD-2) | `version_id` | `current_version_id` 조인이 최신 상태. 이력 분석은 `valid_from/to_snapshot` 사용. **`ctrt_uprc`는 카탈로그 참고가일 수 있다(→ `price_kind` 필수 확인)** |
| `vendor` | 업체(통합그룹번호) | `ctent_unty_grp_no` | 사업자번호 `bzmn_reg_no`로 외부 데이터와 조인 가능 |
| `source_category` | 출처별 분류 트리 | `(source_system, category_code)` | **카테고리 코드(예: 00302)는 shop과 digital에서 다르게 의미된다. 출처 없이 코드 비교 금지** |
| `attachment` | 첨부 파일 메타+추출 텍스트 | `(unty_atch_file_no, atch_file_sqno)` | 원본 파일은 추출 후 삭제 정책. `text_extracted`(텍스트)만 남음. `extract_status: ok/empty/failed:*` — empty는 스캔 PDF/XLSX 등 텍스트 없는 문서일 가능성 |
| `attachment_section` | 첨부 텍스트의 절(section) 분할 | `(unty_atch_file_no, atch_file_sqno, seq)` | 약 3,400만 행 — 가장 큰 테이블. 상세 읽기는 `attachment_section_head`(앞 2,000자 캐시) 사용이 빠름. **분석·전문검색은 subject 아님** |

### 가격 관련 컬럼 (listing_version)

- `ctrt_uprc`: 등재 단가(원). 카탈로그면 참고값.
- `srch_uprc`, `dscnt_aplcn_uprc`: 검색/할인가 표시용.
- `price_kind`: `registered_unit` | `catalog_reference` | NULL(구버전 백필). 분석 시 **NULL도 registered로 취급해도 되는지 목적에 따라 결정**하고 명시한다.
- `ctrt_unt_val`, `devy_cndt_nm`, `sppl_rgn_nm`: 단위·인도조건·공급지역 — 가격 비교의 최소 고정 조건.
- `ntsl_qnty`(+1~4), `sppl_revw_cnt`: 사이트 표시 지표. **실제 수량 확정 아님.**

### 관측 설명 컬럼

- `content_hash`: 추적 필드들의 해시 — 버전 생성 기준.
- `observed_at`, `valid_from_snapshot`, `valid_to_snapshot`, `last_seen_snapshot`: 시점의 의미를 문서 후반 "시간 모델" 참고.
- `data_source`, `endpoint_version`: 어느 내부 API 스펙으로 수집했는지 출처.
- `raw` (jsonb), `raw_ref`: API 원문 전체와 원시 파일 위치. 파싱 신뢰가 필요하면 raw를 본다.

---

## 3. 출처 구분(shop vs digital) — 가장 중요한 필터

**권위는 `listing_source` 테이블이며, 다른 휴리스틱(ID 형식 등)을 쓰지 않는다.**

```sql
SELECT s.source_system, count(*)
FROM listing_source s
GROUP BY 1;
-- shop | 1,043,081
-- digital | 10,180
```

- 디지털 필터: `EXISTS (SELECT 1 FROM listing_source s WHERE s.ctrt_item_mng_no=l.ctrt_item_mng_no AND s.source_system='digital')`
- 카탈로그 계약은 `listing_version.ctlg_ctrt_yn='Y'` + `price_kind='catalog_reference'`.
- 디지털몰에는 5개 영역(상용S/W, 디지털서비스, 공개S/W, 데이터거래, 생성형AI업무지원서비스)이 있고, `source_category.mall_code`로 구분.
- **카테고리 렌더링 주의**: search 카드 카테고리 표기 등은 기존 `shop_category` 조인을 쓰는데 디지털 코드는 shop 코드 체계와 별개다. 교차 표시가 부정확할 수 있으니 분석에는 `source_category`를 출처와 함께 쓴다.

### 디지털몰 수집 범위와 미수집 채널

- 수집됨: 분류(34개 말단 20개 → 10,180건), 목록, 상세 5종(기본정보/속성/설명/계약조건/첨부목록), 첨부 다운로드+텍스트 추출(22,641 ok / 922 empty / 23 failed).
- **미수집(사이트 미확인 경로)**: 판매자 상세, 납품사례, 등재의 웹 딥링크. 추측으로 만들지 않았으므로 해당 테이블들은 디지털에 대해 비어 있을 수 있다.
- 디지털 수집 스냅샷: 346(카테고리), 347(trial), 348(전체 목록), 349(상세 수집 실행). 분석 기준일 2026-10-03.

---

## 4. 시간·스냅샷 모델

- `collection_run` = 수집 실행. `snapshot_id`가 전체를 식별. `source_system`으로 출처 구분, `status`(running/done/aborted), `totals`(결과 집계 jsonb).
- `listing_observation`(snapshot_id, ctrt_item_mng_no, listing_version_id) = "그 실행에서 이 등재가 이 버전으로 보였다". **수집 건수·커버리지 계산의 정답 테이블.**
- `listing_source.presence_status`: active / missing_candidate. missing_candidate는 "가장 최근 전체 수집에서 안 보였다"는 후보지 자동 삭제가 아니다.
- `listing_version`의 SCD-2 이력: `valid_from_snapshot`(그 버전이 처음 관측된 실행), `valid_to_snapshot`(다음 버전으로 대체된 실행). 최신은 `listing.current_version_id`.
- 과거 버전에서 현재 카탈로그 값을 읽는 함정: 예전 스냅샷의 `ctrt_uprc`가 카탈로그 참고가인지는 그 버전의 `price_kind`로 판단한다.

---

## 5. 상품 간 연결(몰 간 매핑)

### 계약 단위 — `contract_xref` (50건)

- 같은 계약번호(`ctrt_no`)가 양쪽 몰에 존재하는 경우의 양측 등재 수.
- **"같은 계약 체계"이지 "같은 상품"이 아니다.** 실측: 공유 계약 50건 내부의 상품명 일치는 0건. 상품 매핑으로 파생해 쓰지 않는다.

### 상품 단위 — `item_xref` (544쌍)

| match_type | 규칙 | 쌍 수 | 커버 상품 | 정밀도 성격 |
|---|---|---:|---|---|
| `maker_model` | 제조사+모델 소문자 정규화 완전 일치 | 162 | 디지털 34 ↔ shop 6 | 고정밀이나 규격 버전 차이는 남음 |
| `maker_model_family` | 위에서 버전 단말(v8.1 등) 제거 후 일치 | 382 | 디지털 20 ↔ shop 22 | 중정밀 — 다른 세대/Eco variant가 섞일 수 있음 |

- **544쌍 ≠ 544개 상품.** `item_xref`에서 distinct 상품은 디지털 54개, shop 28개뿐이다.
- 디지털 전체 10,041 상품 중 연결된 것은 54개(0.54%) — 커버율이 매우 좁다. "매핑 안 되면 무관하다"는 결론 금지.
- 규격 원문 완전 일치 쌍은 0건. **가격 비교는 반드시 spec/옵션/단위 정규화 후에.**
- 재규칙 추가는 가능(다른 match_type으로 함께 저장 가능). 테이블 재생성은 쌍 단위 DELETE+INSERT 결정적.

### 공급업체 겹침 (조인으로 계산)

- 두 몰 모두에 등재가 있는 업체 287사. `listing.ctent_unty_grp_no` 기준 INTERSECT.
- 유명 사례: 이노뎁(주) — shop 1,626건(영상감시·주차관제 등 하드웨어 중심) + digital 113건(SW 개발·유지관리 등 서비스 중심). **하드웨어 몰과 서비스 몰을 가로지르는 기업 포트폴리오가 보이는 게 이 매핑의 실질 가치.**

---

## 6. 분석 산출물 (public.anomaly_*) — 읽는 법

> 이 5개 테이블은 **후보 생성 파이프라인의 산출물**입니다. "이상" 라벨이 곧 위법/낭비 판정이 아닙니다.

| 테이블 | 내용 | 핵심 컬럼 |
|---|---|---|
| `anomaly_candidate`(999) | 초기 규칙(MAD/모델 스프레드) 후보 | `kind`, `ratio`, `ref_price`, `excess_est` — excess_est는 ntsl 곱이라 미검증 |
| `anomaly_market_judge`(193) | LLM+외부시세 조사 결과 | `same`(동일상품 판정), `fair_price` |
| `anomaly_hedonic`(1,000) | 헤도닉 가격 모형 상위 1,000 | `fitted`(기대가격), `z`(표준화 잔차), `q`(BH FDR), `excess` |
| `anomaly_score`(1,037,690) | 전수 스코어(차트/드릴다운) | `z`, `q`, `excess` — shop만 포함, digital 제외 |
| `anomaly_label`(1,000) | 종합 라벨 L1~L5 + 점수 분해 | `parts`(jsonb: stat/scale/llm/struct), `reasons` |

**중요 이력(정정됨)**: 초기 "2,581억원 낭비" 같은 집계는 ntsl_qnty의 실제 수량성 미검증으로 폐기되었고, q값 기반 오탐률 보장과 LLM 라벨 기반 "확정급" 표기도 중단되었다. 상세는 /anomaly/review 페이지의 정정 안내와 `analysis/review_publish.py`의 manifest(`actualWaste: null`, `costPolicy`)를 따른다.

웹 측면에는 이 결과의 **게시본(JSON 페이로드)**도 있다:

- `web/src/lib/anomaly-review-data.json` — 재검증 증거 포함 게시본(v4). `/anomaly/review`와 CSV export의 원본.
- `web/src/lib/policy-100-data.json`, `web/src/lib/external-benchmark-data.json` — 정책 검토 484개, 외부 리테일 기준(extbench) 게시본.

---

## 7. 실제 납품(지출) 데이터 — `procurement_delivery`

**이 가이드에서 가장 가치 있는 스키마입니다.** 다른 팀원이 공식 자료요청으로 받아 적재했습니다.

### `procurement_delivery.delivery_order_line` (약 285만 행)

"나라장터쇼핑몰 납품요구 물품 내역" CSV 적재분. **한 행 = 납품요구 1건의 품목 1라인.**

주요 컬럼:

- 식별: `delivery_request_no`+`delivery_request_revision`+`line_no` (PK 추정 인덱스 존재)
- 기관: `agency_code`, `agency_name`, `agency_type`, `agency_region`
- 공급: `supplier_business_no`(→ `vendor.bzmn_reg_no`), `supplier_name`, `supplier_size`, `supplier_region`
- 상품: `product_id`(→ `item.item_idnf_no`), `product_name`, `item_class_code/name`, `detail_class_code/name`
- 계약: `contract_type`, `contract_no`(→ `listing.ctrt_no`), `contract_revision`, `mas_flag`, `direct_purchase_flag`
- 금액: **`contract_unit_price`(계약 단가), `delivery_unit_price`(납품 단가), `delivery_quantity`, `delivery_amount`(실제 금액)**
- 조정: `quantity_adjustment`, `amount_adjustment`, 누적 조정 2종 — **음수 라인(감액)이 존재하므로 합계 시 포함 여부를 명시적으로 처리**
- 시점: `request_date`(date), `delivery_deadline`
- 추적: `source_file`, `source_row_no`, `imported_at`

인덱스: `ix_delivery_line_agency_date(agency_code, request_date)`, `ix_delivery_line_contract(contract_no, contract_revision, product_id)`, `ix_delivery_line_product(product_id)`, `ix_delivery_line_supplier`, `ix_delivery_line_source_file`, PK(status) `delivery_order_line_pkey(delivery_request_no, revision, line_no)`.

### `procurement_delivery.import_source`

적재한 CSV 목록(row_count, header_sha256, imported_at). 재적재·증분 판단용.

### 이 데이터와 등재 데이터를 잇는 조인

```sql
-- 등재 단가 vs 실제 납품 단가 (디지털 해당 상품 예시 패턴)
SELECT d.product_id, i.item_idnf_nm_raw,
       count(*) AS 납품행, sum(d.delivery_amount) AS 총납품액,
       min(d.delivery_unit_price) AS 최저납품단가, max(d.delivery_unit_price) AS 최고납품단가
FROM procurement_delivery.delivery_order_line d
LEFT JOIN item i ON i.item_idnf_no = d.product_id
WHERE d.product_id = '25023514'   -- 예: 매핑된 디지털 상품
GROUP BY 1, 2;
```

주의:

- `delivery_unit_price`는 납품 시점·버전별로 다를 수 있음(계약 단가와 괴리 분석의 재료인 동시에 함정).
- product_id가 item에 없는(매핑 실패) 행이 있을 수 있다 — LEFT JOIN으로 조인 성공률을 먼저 잰다.
- shop 목록 데이터와 납품 내역의 '상품' 정의가 다르면 그 차이를 가정하지 않고 실측한다.

---

## 8. 스테이징 — `digstg` (분석 대상 아님)

디지털 슬라이스를 로컬→운영으로 옮길 때 쓴 이관 복사본이 운영에 남아 있습니다(digstg.attachment, digstg.listing 등 24개, 각각 public의 디지털 부분과 동일 건수).

- **분석에는 사용하지 않는다**(public의 디지털 데이터와 중복).
- 정합 확인용: `SELECT count(*) FROM digstg.listing` = 10,180 등 대조.
- 향후 정리 후보이나, 다른 팀이 참조 중일 수 있어 임의 삭제 금지.

---

## 9. 사람의 검토 판정 — `audit_review` DB

별도 PostgreSQL DB(`audit_review`)에 저장되며 읽기·쓰기 계정이 분리되어 있습니다.

- `review_decisions(listing_id PK, decision, item_name, vendor, category, price, origin, note, decided_by, roster_version, created_at, updated_at)`
  - decision: `candidate`(후보, 상한 20개) / `excluded`(제외) / `cleared`(검토 완료·무혐의)
  - **후보 관리 페이지의 단일 진실 공급원.** 분석 재실행 시 이 결정을 존중해야 한다(`analysis/policy_shortlist.py`가 excluded/cleared를 재추출에서 제외하는 근거).
- `review_decision_history(history_id, listing_id, decision, item_name, note, decided_by, roster_version, created_at)` — 변경 이력.

웹 앱은 `REVIEW_DATABASE_URL`로 이 DB에 접속하고, 국가조달 데이터 DB(national_audit)와 물리적으로 분리되어 있다.

---

## 10. 조인 모델 — 물리 FK는 의도적으로 거의 없음

실제 FK는 3개뿐입니다: `collection_task.snapshot_id→collection_run`, `listing_observation.snapshot_id→collection_run`, `attachment.current_attachment_version_id→attachment_version`. 나머지 관계는 **모두 논리 컬럼 조인**입니다.

핵심 논리 조인:

```text
listing.item_idnf_no            → item.item_idnf_no
listing.ctent_unty_grp_no       → vendor.ctent_unty_grp_no
listing.current_version_id      → listing_version.version_id
listing((ctrt_no, ctrt_chg_ord))→ listing_contract_terms((ctrt_no, ctrt_chg_ord)) [+ 스냅샷]
listing.ctrt_item_mng_no        → listing_observation / listing_source / listing_attachment /
                                  listing_base_info / listing_detail_part / listing_delivery_case /
                                  listing_image / listing_contract_terms 아님(계약 단위)
item.item_clsf_no               → item_class.item_clsf_no  (마스터 코드 외부 원천, 현재 빈 테이블 가능)
listing.m_cate / l_cate          → shop_category.shop_cgry_no (shop만 유효) 또는 source_category(출처 조인 필요)
vendor.ctent_unty_grp_no        → vendor_contact (스냅샷 버전 테이블)
attachment → attachment_section: (unty_atch_file_no, atch_file_sqno)
attachment_version(unty_atch_file_no, atch_file_sqno, sha256)
```

### 함정

- `shop_category`는 shop 전용. 디지털 카테고리 코드 조인하면 잘못된 이름이 매칭된다.
- `vendor_contact`, `listing_base_info`, `listing_detail_part`, `item_attribute`, `item_description` 등은 **스냅샷을 키에 포함**한다. 최신만 보려면 상품별 max(snapshot_id) 서브쿼리가 필요.
- `listing_delivery_case`는 페이지 1(최대 18건)만 수집 — 전체 납품 이력이 아니다.

---

## 11. 검증된 SQL 스타터 (모두 읽기 전용, 실측 결과 포함)

아래는 실제 DB에서 실행·검증된 패턴입니다. 라지 DB에서는 `SET max_parallel_workers_per_gather=0; SET statement_timeout='90s';`를 선행하면 안전합니다.

```sql
-- (a) 디지털몰 가격 구조: 참고가 분리
SELECT l.m_cate, c.name, v.price_kind, count(*) AS n,
       percentile_cont(0.5) WITHIN GROUP (ORDER BY v.ctrt_uprc) AS median
FROM listing_observation o
JOIN listing l USING(ctrt_item_mng_no)
JOIN listing_version v ON v.version_id=o.listing_version_id
LEFT JOIN source_category c ON c.source_system='digital' AND c.category_code=l.m_cate
WHERE o.snapshot_id=348
GROUP BY 1,2,3 ORDER BY n DESC;

-- (b) 상품 교차 링크 상세
SELECT x.match_type, di.item_idnf_no, di.item_idnf_nm_raw, di.spec_raw,
       si.item_idnf_no, si.item_idnf_nm_raw, si.spec_raw
FROM item_xref x
JOIN item di ON di.item_idnf_no=x.digital_item_idnf_no
JOIN item si ON si.item_idnf_no=x.shop_item_idnf_no
WHERE x.digital_item_idnf_no='25023514';

-- (c) 두 몰 겹치는 공급업체
SELECT DISTINCT vd.name
FROM listing l JOIN listing_source s USING(ctrt_item_mng_no)
JOIN vendor vd ON vd.ctent_unty_grp_no=l.ctent_unty_grp_no
WHERE s.source_system='digital'
INTERSECT
SELECT DISTINCT vd.name
FROM listing l JOIN listing_source s USING(ctrt_item_mng_no)
JOIN vendor vd ON vd.ctent_unty_grp_no=l.ctent_unty_grp_no
WHERE s.source_system='shop';

-- (d) 특정 상품의 실제 납품 실적
SELECT agency_name, request_date, delivery_quantity, delivery_unit_price, delivery_amount
FROM procurement_delivery.delivery_order_line
WHERE product_id='20935295'
ORDER BY request_date DESC LIMIT 20;

-- (e) 업체의 몰별 구성
SELECT s.source_system, i.item_cfnm, count(*)
FROM listing l JOIN listing_source s USING(ctrt_item_mng_no) JOIN item i USING(item_idnf_no)
WHERE l.ctent_unty_grp_no='CN0100000350207'
GROUP BY 1,2 ORDER BY 1,3 DESC;
```

---

## 12. 알려진 한계·주의사항 총정리

1. 등재 단가 ≠ 실제 지출. 카탈로그 참고가 ≠ 협상가. ntsl_qnty ≠ 실제 수량.
2. 이상탐지 라벨·점수는 후보 순위 매기기 도구이며 판정이 아니다.
3. `item_xref`는 두 단계 정밀도의 "후보"이며 규격·버전 차이를 보장하지 않는다.
4. shop과 digital의 카테고리 코드 체계는 별개다. 코드 숫자 유사성으로 뜻을 유추하지 않는다.
5. 스냅샷 키 있는 테이블은 max 스냅샷 선택이 필수다.
6. delivery_order_line은 음수(감액) 라인이 있다. 합계에 포함 여부를 명시한다.
7. attachment 추출 텍스트는 empty 상태가 922건(스캔 PDF/XLSX) — OCR 미완료.
8. 디지털몰은 판매자 상세·납품사례·웹 딥링크 미수집.
9. digstg는 이관 스테이징 — 분석 대상 아님.
10. 웹 화면의 JSON 게시본(anomaly/policy/extbench)은 특정 시점의 선별 결과. 원천이 아니라 "이번에 공개한 것"이다.

---

## 13. 새 데이터/분석을 추가하는 팀들을 위한 규칙

- 기존 수집 스키마를 직접 UPDATE/REPLACE하지 않는다. 신규 결과는 **별도 스키마 또는 접두어 테이블**로 추가(예: `procurement_delivery`가 그 모범 사례).
- 원천 명시 컬럼을 둔다: source_file/row_no/imported_at 또는 collection snapshot 참조.
- LLM/통계 추정값은 반드시 "모델명·버전·실행일·입력"을 함께 저장하고, 실측값과 컬럼명으로 구분한다(예: `llm_fair` vs `delivery_unit_price`).
- 새 테이블은 `explorer_ro`에 GRANT SELECT까지 완료해야 웹 스키마 가이드에 표시된다.
- 컬럼/테이블 추가 후 `web/src/db/queries/schema.ts`의 설명 사전(`schema-desc.json`)에 한 줄 의미를 등록한다. LLM들이 이 가이드를 읽고 의미를 배운다.
