Contents
바이브코딩 101

바이브코딩으로 세법 판례 검색기 만들기

세법 판례 검색기는 공식 원문 수집부터 검색 화면 연결까지 7단계로 개발합니다. 각 단계에서 생성되는 데이터와 다음 단계로 이어지는 흐름을 확인하면 개발 원리와 구현 단계를 이해할 수 있어요.

본문 44분 · 기술 부록 70분

UQA세법 판례 검색개발 원리와 구현 단계바이브코딩

1. 세법 판례 검색기 개발 흐름

공식 원문 수집부터 검증까지 이어지는 UQA 세법 판례 검색기 구축 단계
  • 바이브코딩으로 UQA를 사용해 세법 판례 검색기를 개발하는 과정을 살펴봅니다.
    • UQA는 질문에 맞는 답을 찾아주는 질의응답 시스템입니다. 검색기의 데이터 저장과 검색을 맡는다고 이해하면 쉬워요.
  • 검색기 개발은 공식 법률 자료 수집부터 질문과 답을 보여 주는 화면 연결까지 7단계로 진행합니다.
    • 각 단계에서 어떤 데이터가 생성되고 다음 단계로 어떻게 이동하는지 이해하는 것이 중요해요.
  • 코드 전체를 알 필요는 없지만 원본 데이터, 정리된 데이터, UQA, 그래프의 역할은 구분해야 합니다.
    • 그래야 검색기가 예상대로 작동하는지 확인할 수 있어요.

2. 원본을 지키는 안전한 작업 원칙

원본 보존과 단계별 검증 및 UQA 적재 순서를 보여주는 공통 작업 원칙
  • 검색기를 안전하게 개발하려면 몇 가지 작업 원칙이 필요합니다.
    • 이 원칙은 원본을 지키고 오류 확산을 막는 안전장치예요.
  • 원본 불변 보존: 공식 법률 자료는 처음 수집한 상태로 보관합니다.
    • 정리 과정에서 실수해도 원본이 보존되어 있으면 재수집 없이 정리 작업만 다시 실행할 수 있어요.
  • 가공 데이터 재생성: 검색 데이터와 관계 정보는 원본으로 다시 생성할 수 있습니다.
    • 변환 규칙을 기록하면 같은 입력으로 같은 데이터를 재현할 수 있어요.
  • 검증 후 UQA 적재: 수집과 정리를 마치고 검증을 통과한 데이터만 UQA에 적재합니다.
    • 이 순서를 지키면 수집 오류가 검색 데이터로 확산되는 것을 막을 수 있어요.
  • 단계별 실행: 원문 수집부터 검색 화면 연결까지 한 번에 진행하지 않습니다.
    • 초기 자료의 오류가 수십만 건으로 확산될 수 있기 때문이에요.
    • 자료 수집, 정리, UQA 적재, 그래프 생성 순서로 각 단계를 확인합니다.
실행 프롬프트 · 공통 구현 원칙
[프로젝트 전제]
- 개발 방식: 빈 디렉터리에서 시작하는 신규 프로젝트
- 목표 제품: UQA 기반 세법령·판례·해석례 검색 웹서비스
- 진행 방식: 단계별 독립 산출물 생성 후 검증
- 단계 전환: 현재 단계 완료 조건 충족 후 다음 단계 진행
- 데이터베이스: UQA만 사용
- 원본 정책: 공식 원문·수집 이력 불변 보존, 정리 결과로 덮어쓰기 금지
- 실행 단위: 수집·정리·UQA 적재·그래프 생성의 독립 실행 및 재실행 보장
- 멱등성: 동일 입력 재실행 시 중복 행·중복 간선 0건
- 변경 범위: 현재 단계에 필요한 파일만 생성·수정

[사전 승인 대상]
- 파괴적 삭제
- 전체 재구축
- 운영 UQA 대량 쓰기
- 스키마의 비호환 변경

[검증 기준]
- 실제 명령 실행 결과로 완료 판정
- 추측에 의한 성공 보고 금지
- 입력·신규·기존·실패·건너뜀 건수 제시
- 생성·수정 파일의 실제 경로 제시

[완료 보고]
- 변경 파일과 변경 이유
- 실행 명령
- 입력·출력 건수
- 검증 결과
- 남은 위험
- 다음 단계 실행 여부: 미실행

3. 데이터 흐름이 보이는 프로젝트 구조

세법 판례 검색기 프로젝트 폴더와 데이터 이동 경로
  • 검색기 개발은 데이터 이동 경로를 정하는 일에서 시작합니다.
    • 건물을 짓기 전에 설계도를 그리는 과정과 비슷해요.
  • 데이터 파이프라인은 데이터가 흘러가는 경로를 뜻합니다.
    • 어떤 자료를 어디에 저장하고, 어느 단계에서 형태를 변환하며, 검증된 자료를 언제 UQA에 적재할지 정해요.
    • 이 구조가 있으면 수집 규칙이나 검색 방식이 바뀌어도 모든 단계를 재실행할 필요가 없습니다.
  • 빈 프로젝트에는 데이터 용도에 맞춘 폴더를 먼저 준비합니다.
    • data/raw 폴더에는 원본 자료를, data/normalized 폴더에는 정리된 자료를, data/manifests 폴더에는 작업 기록을 저장해요.
    • scripts 폴더에는 수집(collect), 정리(normalize), UQA 적재(load), 검증(verify) 코드를 구성하고 검색 화면은 처리 단계와 분리해 연결합니다.
  • 이 단계에서는 자료 수집과 UQA 적재를 실행하지 않아요.
실행 프롬프트 · 신규 프로젝트 구조 생성
[선행]
- 공통 작업 원칙 적용

[목표]
- UQA 기반 세법 판례 검색기의 신규 프로젝트 생성
- 수집·정리·적재·그래프·검색·검증의 실행 경계 확정

[기술 구성]
- 데이터 수집·정리·백엔드: Python
- 웹 프론트엔드: React·TypeScript·Vite
- 데이터베이스: UQA
- 원본 교환 형식: JSON·JSONL
- 설정 형식: JSON

[디렉터리]
- config: 수집 대상·실행 정책
- data/raw/laws: 법령 MCP 원본
- data/raw/documents: 국세법령정보시스템 원본
- data/normalized: 검색용 정리 JSONL
- data/manifests: 수집 이력·체크포인트·해시
- scripts/collect: 원문 수집기
- scripts/normalize: 정리·관계 추출기
- scripts/load: UQA 적재·그래프 생성기
- scripts/verify: 데이터·검색 검증기
- server: 검색 API
- web: 검색 화면
- tests: 단계별 자동 검증

[생성 파일]
- README.md: 설치·환경설정·단계별 실행 순서
- .env.example: 비밀값 없는 환경변수 이름
- config/tax-data-collections.json: 수집 카탈로그 구조
- scripts/run_tax_collection.py: status·dry-run 실행 골격
- server/app.py: health 엔드포인트
- web: 검색 화면 기본 레이아웃
- tests: health·설정 검증 테스트

[안전 조건]
- 네트워크 수집 미실행
- UQA 파일 생성·쓰기 미실행
- 실제 API 키 저장 금지
- 임시 모의 데이터 생성 금지

[완료 조건]
- 디렉터리·기본 파일 생성
- 백엔드 health 테스트 통과
- 프론트엔드 빌드 통과
- 설정 검증 테스트 통과
- run_tax_collection status·dry-run 실행 성공

[완료 보고]
- 생성 파일 구조의 시각화
- 설치·실행 명령
- 테스트 결과
- 다음 단계 수집 인터페이스

4. 공식 법률 원문 수집

법령 MCP와 국세법령정보시스템에서 공식 법률 자료를 수집하는 경로
  • 검색기에 필요한 공식 법률 자료를 두 출처에서 수집합니다.
    • 공식 법률 자료의 출처는 두 곳이에요.
    • 세법령 (세금 관련 법률)은 법령 MCP (법제처 국가법령정보센터)에서 가져옵니다.
    • 판례 (법원 판결), 심판결정례 (조세심판원 결정), 해석례 (세금 관련 질의회신)는 국세법령정보시스템에서 가져와요.
  • 수집 전에 대상 자료, 저장 형식, 완료 기준을 순서대로 정합니다.

4.1. 출처별 수집기와 공통 원본 보존

  • 출처마다 자료를 제공하는 방식이 다릅니다.
    • 법령 MCP는 법률 버전과 종류를 기준으로 자료를 제공해요.
    • 국세법령정보시스템은 목록에서 문서 번호를 찾은 뒤 그 번호로 상세 내용을 다시 요청하는 구조입니다.
  • 수집기는 출처별로 나누어 구현합니다.
    • 수집한 자료에는 출처와 수집 시각 같은 공통 정보를 기록해 서로 연결할 수 있어요.

4.2. 법령 MCP를 통한 과거·현행·시행 예정 세법령 수집

  • 법령 MCP에서는 법률, 시행령, 시행규칙의 공식 번호와 모든 버전을 수집합니다.
    • 각 법률의 시행일과 전체 조문도 함께 저장해요.
  • 특정 날짜 기준 검색을 지원하려면 과거, 현행, 시행 예정 버전을 모두 보관합니다.
    • 시행 예정 법률은 '예정'으로 표시하고 현행 검색 대상에서 제외해요.
  • 수집기는 공식 자료와 수집 이력을 기록하는 역할만 담당합니다.
    • 조문 분할과 판례의 인용 조문 분석은 다음 단계에서 진행해요.
실행 프롬프트 · 세법령 원본 수집
[선행]
- 공통 작업 원칙 적용

[목표]
- 법령 MCP 기반 세법령 원문 수집기 신규 구현
- 샘플 법률군 1개의 연혁 원문·매니페스트 생성

[원천]
- 제공자: 현재 개발환경에 연결된 법령 MCP
- 선행 확인: 사용 가능한 법령 MCP 도구·리소스·입력 스키마
- 대체 원천: 임의 사용 금지
- MCP 미연결·기능 부족: 구현 중단 및 필요한 연결 정보 보고

[대상]
- 카탈로그: config/tax-data-collections.json
- 범위: 카탈로그에 선언된 세법 법률군
- 구성: 법률·시행령·시행규칙
- 버전: 과거 연혁·현행·시행 예정

[수집 순서]
- 법령명으로 법률·시행령·시행규칙 식별
- 법령별 공식 ID와 전체 버전 목록 조회
- 버전별 공포일·시행일·개정 유형 조회
- 버전별 조문 전체 원문 조회
- 법령 MCP 원응답의 JSON 보존

[수집 기준]
- 출력 계약: 법령 종류와 버전에 관계없이 동일 구조
- 버전 판정: 시행일 기준 과거·현행·시행 예정
- 증분 판정: 신규 MST 또는 변경된 원문 SHA-256

[매니페스트 필드]
- 법령명
- 공식 법령 ID
- 버전 ID(MST)
- 공포일·시행일·개정 유형
- 원문 경로·SHA-256·수집시각

[제약]
- UQA·그래프 변경 0건
- 빈 파싱 결과의 실패 처리
- 비정상 조문 수 감소의 실패 처리
- 기존 정상 매니페스트 교체 금지
- --dry-run·--collect-only 지원
- 전체 수집 미실행

[완료 보고]
- 사용한 법령 MCP 도구·리소스와 입력값
- 법률·시행령·시행규칙별 버전 수
- 과거·현행·시행 예정 버전 수
- 버전별 MST·시행일·조문 수
- 원본·매니페스트 경로
- 신규·변경·동일·실패 건수
- UQA 변경 0건 확인

4.3. 국세법령정보시스템의 목록·상세 원문 수집

  • 국세법령정보시스템에서는 목록 페이지와 상세 페이지를 구분합니다.
    • 목록 페이지는 신규 자료를 확인하는 '색인' 역할을 해요.
    • 목록에서 문서 번호와 등록 시각을 수집하고 기존 자료와 겹치지 않는 신규 자료를 선별합니다.
    • 상세 페이지에는 실제 검색 근거가 되는 원문이 있어요.
    • 신규 자료마다 상세 내용을 요청해 제목, 요지, 본문을 모두 수집합니다.
    • 하나라도 누락되면 다음 단계로 넘어가지 않고 다시 시도해요.
  • 목록과 상세를 분리하면 작업 중단 후에도 쉽게 재개할 수 있습니다.
    • 저장된 자료는 요청하지 않고 실패한 자료만 다시 수집하므로 처음부터 진행할 필요가 없어요.
실행 프롬프트 · 판례·심판결정례·해석례 원본 수집
[선행]
- 공통 작업 원칙 적용

[목표]
- 국세법령정보시스템 원문 수집기 신규 구현
- 목록→상세 2단계 판례·결정례·해석례 원본 확보

[원천]
- 제공자: 국세법령정보시스템
- 접근 범위: 공개 목록·상세 원문
- 원천 식별자: 국세법령정보시스템 문서 ID
- 원천 응답: 가공 전 JSON·HTML 원문 보존
- 대체 원천: 임의 사용 금지

[대상 분류]
- 001_01 사전답변
- 001_02 서면해석
- 001_03 세법해석 사전답변
- 001_04 공개 서면질의
- 001_05 과세적부
- 001_06 이의신청
- 001_07 심사청구
- 001_08 심판청구
- 001_09 판례
- 001_10 헌법재판소

[샘플 범위]
- 자료 유형별 최신 목록 1페이지
- 자료 유형별 상세 3건

[수집 기준]
- 요청 구조 확인: 브라우저 개발자 도구 또는 공개 요청 규격 기준
- 목록→상세 요청 분리
- 목록 정렬: 최신 등록일 내림차순
- 목록 필드: 원천 문서 ID·등록시각
- 경계 처리: 이전 체크포인트와 겹치는 구간 포함
- 중복 방지: detail_raw*.jsonl의 완료 ID 재사용
- 상세 원문: 제목·문서번호·요지·본문·생산일·세목·관련 법령·쟁점
- JSONL 추적정보: 수집시각·목록 원문·상세 원문·원천 ID
- 요청 정책: 타임아웃 30초·최대 5회 재시도·점증 대기
- 동시 실행 방지: OS 파일 잠금

[실패 조건]
- 최대 페이지 도달 전 체크포인트 이전 구간 미확인
- 신규 후보의 상세 원문 누락 1건 이상
- 원천 응답과 다른 임의 필드명 사용
- 국세법령정보시스템 외 자료의 혼합

[제약]
- UQA·그래프 변경 0건
- 체크포인트 전진 금지

[완료 보고]
- 유형별 목록·신규 후보·상세 성공·실패 건수
- JSONL 경로·행 수
- 샘플 3건의 제목·문서번호·생산일·본문 길이
- 관련 법령·쟁점의 실제 원문 필드 매핑표
- 재실행 시 완료 ID 건너뜀 결과
- UQA 변경 0건 확인

4.4. 출처와 변경 이력을 남기는 공통 원본 정보

  • 모든 자료에는 '신분증' 역할을 하는 공통 정보를 함께 저장합니다.
    • 원천 ID, 요청 시각, 원문, SHA-256, 수집 실행 ID가 해당해요.
    • 공통 정보로 중복·변경·누락을 확인하고 동일 작업을 재현할 수 있습니다.

5. 검색용 데이터 정리

법령과 판례 원본을 검색용 데이터와 관계 데이터로 정리하는 과정
  • 수집한 원본 자료를 검색기가 사용할 수 있는 형태로 정리합니다.
    • 원본을 유지하면서 검색용 JSONL 파일을 새로 생성해요. JSONL은 데이터를 저장하는 형식 중 하나입니다.
  • 세법령과 판례는 구조가 다르므로 각각 정리하고 관련 조문과 쟁점을 연결해요.

5.1. 조·항·호·목 구조를 보존한 검색 데이터

  • 자료 정리는 글자만 다듬는 작업이 아닙니다.
    • 법률의 계층 구조를 보존해야 해요.
  • 검색기는 단어가 포함된 문서를 찾는 기능을 넘어 시점과 인용 관계까지 처리해야 합니다.
    • 예를 들어 같은 조문의 과거 버전과 현재 버전을 연결하고, 판례가 인용한 조문을 정확한 법률 단위에 연결해요.
    • 따라서 짧은 요약보다 식별자와 계층(조-항-호-목 같은 구조)을 안정적으로 구성하는 작업이 중요합니다.

5.2. 시점 비교를 위한 Canonical ID와 Version ID

  • Canonical ID (논리적 조문 식별자)는 '별명'과 비슷합니다.
    • "부가가치세법 제38조"처럼 개정 전후에도 같은 논리 조문을 가리키는 고유한 이름이에요.
    • 이 ID를 기준으로 과거와 현재 조문을 비교합니다.
  • Version ID (특정 시점 조문 식별자)는 '정식 이름'과 비슷해요.
    • 특정 시점에 실제로 존재한 조문 내용을 가리키는 번호입니다.
    • 질문 기준일에 유효한 조문을 보여 주거나 당시 법률 요건을 검토할 때 사용해요.

5.3. 연결 근거를 추적할 수 있는 관계 데이터

  • 문서와 조문, 문서와 쟁점을 연결할 때는 연결 근거도 함께 기록합니다.
    • 예를 들어 판례가 특정 조문을 인용했다면 판례의 어느 부분에서 조문을 언급했는지 남겨요.
    • 연결이 부정확하면 근거를 역추적해 원인을 찾을 수 있습니다.

5.4. 법령·문서·관계별 JSONL 파일

  • 정리된 파일마다 역할이 다릅니다.
    • law_units.jsonl에는 법률의 계층 구조와 버전을 저장하고,
    • legal_documents.jsonl에는 판례와 해석례의 공통 검색 필드를 저장하며,
    • graph_edges.jsonl에는 문서, 조문, 쟁점 사이의 연결 관계를 저장해요.
  • UQA 적재 프로그램은 이 파일을 다시 분석하지 않고 검증된 내용을 그대로 적재합니다.

5.5. 버전별 조·항·호·목 계층

  • 법률 조문의 조, 항, 호, 목은 각각 독립된 단위로 정리합니다.
    • 원문의 순서와 부모-자식 관계(예: 조 안에 항이 있고 항 안에 호가 있는 관계)는 보존해요.
    • 검색 결과가 "제38조 제1항 제2호"처럼 특정 부분만 나오더라도 화면에서는 같은 버전의 "제38조 전체"를 보여 줘야 하기 때문입니다.
  • "제52조의2"처럼 가지 번호가 있는 조문은 "제52조"와 별개의 조문 ID로 관리해요.
실행 프롬프트 · 법령 버전과 조·항·호·목 정리
[선행]
- 공통 작업 원칙 적용

[목표]
- 법령 MCP 원본을 검색·적재용 법령 JSONL로 정리
- 법령군·법령 버전·조·항·호·목 계층의 손실 없는 복원

[입력]
- data/raw/laws의 법령 MCP 원응답
- data/manifests의 공식 법령 ID·MST·공포일·시행일·해시

[출력]
- data/normalized/law_families.jsonl
- data/normalized/law_versions.jsonl
- data/normalized/law_units.jsonl
- data/manifests/normalize_laws_run.json

[계층]
- 법령군: 같은 법령의 전체 생애
- 법령 버전: 특정 MST와 시행일의 원문
- 법령 단위: 버전에 속하는 조·항·호·목

[식별자]
- 공식 법령 ID 기반 법령군 ID
- 같은 논리적 조문을 연결하는 canonical ID
- 특정 MST의 원문을 가리키는 version ID
- 가지번호를 포함한 조문 ID: 제52조와 제52조의2 구분
- 상위 조문 ID와 원문 순서를 포함한 항·호·목 ID

[내용 보존]
- 조문 제목과 본문 분리
- 같은 버전의 조 전체를 원문 순서로 복원 가능한 계층 정보
- 삭제 조문의 is_deleted 표시
- 공포일·시행일·개정 유형·과거·현행·시행 예정 상태
- 모든 행의 원본 파일·원본 레코드·원본 해시 추적정보

[샘플 범위]
- 법률군 1개
- 서로 다른 MST의 같은 조문 2개 이상
- 조문별 하위 항·호·목 전체

[검증]
- canonical ID와 version ID 비교표
- 조·항·호·목 원문 순서 대조
- 부모 없는 하위 단위 0건
- ID 중복 0건
- 원본 조문 수와 정리 조문 수의 차이 0건

[제약]
- 원본 변경 0건
- UQA·그래프 변경 0건
- 샘플 검수 전 전체 변환 미실행

5.6. 공통 문서 필드와 인용 관계

  • 판례와 해석례는 자료 유형마다 필드 이름이 다르지만 검색 화면에 필요한 정보는 같습니다.
    • 문서 번호, 제목, 요지, 본문, 생산일, 세목을 공통 형식으로 정리해요.
    • 원본의 관련 법령과 쟁점은 별도 관계 데이터로 분리합니다.
  • 인용 조문을 정확히 식별하지 못해도 관계를 삭제하지 않아요.
    • REFERENCES_LAW_UNRESOLVED (해결되지 않은 법률 인용) 상태로 저장하면 법률 별칭이나 분석기가 개선된 뒤 원본을 재수집하지 않고 관계만 다시 해결할 수 있습니다.
실행 프롬프트 · 공통 문서와 문서↔조문·쟁점 관계
[선행]
- 공통 작업 원칙 적용

[목표]
- 국세법령정보시스템 원본을 공통 법률문서 JSONL로 정리
- 문서↔조문·문서↔쟁점 관계 JSONL 생성

[입력]
- data/raw/documents의 목록·상세 원응답
- 프롬프트 2-A의 canonical 법령 단위 목록

[출력]
- data/normalized/legal_documents.jsonl
- data/normalized/authority_nodes.jsonl
- data/normalized/graph_edges.jsonl
- data/manifests/normalize_documents_run.json

[문서 필드]
- 고정 ID: nts:[category]:[ntstDcmId]
- 자료 유형·원천 분류 코드·원천 문서 ID
- 문서번호·제목·요지·전체 본문
- 결정일·생산일·귀속연도·세목 코드·원문 URL
- 원본 파일 경로·줄 번호·수집시각·변환 규칙 버전

[조문 인용]
- 최우선 원천 필드: dcmRltnStttList
- 법령 ID와 조·항·호·목의 구조화
- canonical 조문 확인 완료 관계: REFERENCES_LAW
- canonical 조문 확인 불가 관계: REFERENCES_LAW_UNRESOLVED
- 관계별 원문 인용 문자열·신뢰도·해결 사유

[쟁점]
- 최우선 원천 필드: dcmRltnStttMatrList
- 원천 쟁점 ID 우선 사용
- 원천 ID 부재 시 정리된 쟁점 문구 기반 안정 ID
- 문서→쟁점 관계: ABOUT_ISSUE

[샘플 범위]
- 자료 유형별 3건
- 원문 레코드와 정리 결과의 나란한 비교

[검증]
- 해결 인용 수·미해결 인용 수·인용 해결률
- 문서 ID 중복 0건
- 빈 제목·빈 본문 0건
- 관계 끝점 누락 0건
- 깨진 JSONL 0건

[실패 처리]
- 치명적 오류 1건 이상: 실패 종료코드
- 치명적 오류 발생 시 UQA 적재 금지 표시

[제약]
- 원본 변경 0건
- UQA·그래프 변경 0건

6. 검증된 검색 데이터 저장

검증된 정규화 데이터를 UQA 테이블과 검색 인덱스에 기록하는 과정
  • 자료 정리를 마친 뒤 검증 오류가 0건일 때 UQA 적재를 시작합니다.
    • UQA는 앞에서 설명한 질의응답 시스템이에요. UQA에 데이터를 적재하면서 검색에 사용할 정보와 조회 규칙을 확정합니다.
  • 먼저 소량의 데이터를 테스트용 UQA에 적재해 검색 결과와 재실행 동작을 확인해요.
    • 새 차를 운행하기 전에 시운전하는 과정과 비슷합니다.
  • 검증을 마치면 운영용 UQA에는 신규 데이터만 증분 적재해요.

6.1. 검색 필드와 필터를 정하는 적재 계약

  • UQA에 적재할 때는 검색 필드, 필터 필드, 중복 방지용 ID를 확정합니다.
    • 원문과 구조화 정보(문서 번호, 세목, 연도 등)를 함께 저장하면 문서 번호 정확 검색, 세목·연도 필터, 자연어 본문 검색을 한 흐름에서 처리할 수 있어요.

6.2. 소량 데이터로 검증하는 샘플 적재

  • 법률군 1개와 문서 약 100건을 테스트용 UQA에 적재해 구조(스키마), 색인(인덱스), 검색 결과를 먼저 확인합니다.
    • 소량으로 검증하면 잘못된 구조의 데이터가 운영 시스템에 대량으로 들어가는 것을 막을 수 있어요.

6.3. 재실행해도 중복되지 않는 멱등 적재

  • 고유한 번호(ID)를 기준으로 이미 저장된 데이터는 건너뜁니다.
    • 같은 데이터를 다시 적재하면 두 번째 실행의 신규 데이터는 0건이어야 해요.
    • 똑같은 주문을 두 번 보내도 한 건만 처리되는 것과 비슷합니다.

6.4. 쓰기 충돌을 막는 단일 Writer

  • 여러 프로그램이 동시에 UQA에 데이터를 저장하지 못하도록 한 번에 한 프로그램만 쓰기 작업을 수행합니다.
    • 도서관에서 한 번에 한 사람만 대출 절차를 처리하는 것과 비슷해요.
    • 여러 검색 요청은 동시에 읽을 수 있지만 쓰기 작업은 하나만 실행합니다.

6.5. 문서 번호·자연어·필터를 위한 검색 인덱스

  • 빠르고 정확한 검색을 위해 검색 인덱스를 구성합니다.
    • 문서 번호나 법령명+조문 번호는 정확 일치 검색을 우선하고,
    • 제목, 요지, 본문 같은 자연어 내용은 GIN 기반 Bayesian BM25 기술로 검색해요.
    • 자료 유형, 연도, 세목, 기준일은 검색어와 분리된 필터로 적용합니다.
실행 프롬프트 · UQA 스키마·적재·검색 인덱스
[선행]
- 공통 작업 원칙 적용
- 프롬프트 2-A·2-B 검증의 치명적 오류 0건

[목표]
- 검증된 정리 JSONL을 UQA에 적재하는 스키마·로더·검색 인덱스 신규 구현
- 샘플 적재와 운영 증분 적재의 분리

[데이터베이스]
- UQA 단일 사용
- 다른 데이터베이스·임시 데이터베이스 사용 금지

[생성 파일]
- sql/001_tax_search_schema.sql
- scripts/load/prepare_uqa_rows.py
- scripts/load/uqa_batch_load.py
- scripts/verify/verify_uqa_load.py

[스키마]
- 원본 추적·수집 실행 이력
- 법령군·법령 버전·조·항·호·목
- 법률문서·자료 유형·연도·세목
- 그래프 원본 정점·관계
- 재실행 가능한 버전형 마이그레이션

[샘플 적재]
- 별도 테스트 UQA 파일
- 법률군 1개
- 법률문서 100건
- 동일 샘플 2회 실행
- 두 번째 실행의 신규 행 0건

[운영 적재]
- 샘플 검증 결과 보고 후 승인 대기
- 승인 후 OS 파일 잠금 기반 단일 writer
- 고정 ID 기준 신규 행만 적재
- 배치 크기 500
- 기존 행 삭제·전체 UQA 재구축 금지

[검색 인덱스]
- 제목·요지·본문·조문 본문 GIN 인덱스
- 제목·요지·본문 Bayesian BM25 신호의 fuse_log_odds 결합
- 문서번호 완전 일치 우선
- 법령명+조문번호 완전 일치 우선

[검증]
- ID 중복·필수값 누락 0건
- 본문 검색
- 자료 유형·연도·세목 필터
- 기준일 유효 조문 선택
- 동일 입력 재실행의 멱등성

[완료 보고]
- 샘플·운영 실행 결과 분리
- 입력·신규·기존·실패 행 수
- 실행 전후 테이블 건수·UQA 파일 크기
- 실행 시간·대표 검색 결과
- 운영 UQA 변경 여부

7. 문서·조문·쟁점을 연결한 지식 그래프

판례와 해석례, 세법 조문, 공통 쟁점을 연결한 지식 그래프
  • 일반적인 검색은 비슷한 단어가 포함된 문서를 찾지만 지식 그래프는 문서 사이의 관계까지 탐색합니다.
    • 검색된 문서가 실제로 어떤 조문을 인용했는지, 같은 쟁점(주요 논점)을 다루는 다른 문서가 있는지 확인해요.
    • 그래프는 직접 답을 생성하지 않고 관련 가능성이 있는 후보를 넓히는 통로 역할을 합니다.

7.1. 그래프로 발견하고 원문으로 확인하는 관련성

  • 특정 조문을 인용한 판례가 있다면 같은 조문을 인용한 다른 문서도 관련될 수 있어요.
    • 그래프는 이런 관련성을 탐색합니다.
    • 같은 쟁점에 연결된 심판결정례나 해석례도 함께 찾아요.
  • 그래프 연결만으로 관련성을 확정하지는 않습니다.
    • 최종 결과는 문서와 조문 원문을 확인해 실제 관련성을 검증해야 해요.

7.2. 문서·조문·쟁점을 잇는 정점과 간선

  • 그래프는 정점(점)과 간선(선)으로 구성됩니다.
    • 정점: 법령군, 버전, 조문, 문서, 쟁점이 각각 하나의 점이 돼요.
    • 검색 결과에서 그래프 탐색을 시작할 수 있도록 UQA 데이터의 고유 ID와 연결합니다.
    • 간선: 'REFERENCES_LAW' (법률 인용)나 'ABOUT_ISSUE' (쟁점 관련)처럼 의미가 분명한 관계를 선으로 표현해요.
    • 문구가 비슷하다는 이유만으로 법률 관계를 생성하지 않습니다.

7.3. 상위 후보만 탐색하는 제한된 확장

  • 그래프 탐색은 검색 결과 상위 문서 몇 개에서만 이웃을 확장합니다.
    • 전체 그래프를 탐색하면 응답이 느려지고 관련 없는 후보가 과도하게 증가할 수 있어요.
    • 대화형 검색에서는 제한된 확장으로 응답 속도를 관리합니다.

7.4. 후보 탐색으로 제한한 그래프의 역할

  • 그래프는 법률 요건을 판단하거나 답변을 생성하지 않아요.
    • 검색 후보를 확장하고 문서가 연결된 조문과 쟁점을 설명하는 데 사용합니다.
실행 프롬프트 · tax_knowledge 그래프 생성
[선행]
- 공통 작업 원칙 적용
- UQA 샘플 적재 검증 완료

[목표]
- UQA의 tax_knowledge 그래프 생성기 신규 구현
- 검증된 authority_nodes·graph_edges의 증분 반영

[생성 파일]
- scripts/load/materialize_uqa_graph.py
- scripts/verify/verify_uqa_graph.py

[필수 정점]
- 법령군·법령 버전·canonical 조·항·호·목
- 판례·심판결정례·해석례 문서
- 쟁점·미해결 법령 인용

[필수 관계]
- 법령 버전 → HAS_ARTICLE → 조문
- 하위 조문 → PART_OF → 상위 조문
- 같은 단계 조문 → NEXT → 다음 조문
- 문서 → REFERENCES_LAW → canonical 조문
- 문서 → REFERENCES_LAW_UNRESOLVED → 미해결 인용
- 문서 → ABOUT_ISSUE → 쟁점

[ID 정책]
- 외부 문자열 ID의 안정적인 64비트 내부 ID 변환
- 내부 ID 충돌 사전 검사
- 기존 정점·간선 건너뜀

[적재 정책]
- 양 끝 정점이 존재하는 간선만 추가
- 전체 그래프 삭제·교체 금지
- 샘플 UQA에서 우선 생성
- 샘플 결과 보고 후 운영 반영 승인 대기

[샘플 경로 검증]
- 문서 → 인용 조문
- 조문 ← 같은 조문을 인용한 다른 문서
- 문서 → 쟁점 ← 같은 쟁점의 다른 문서
- 법령 버전 → 조 → 항 → 호
- 각 경로의 실제 ID·라벨 출력

[완료 보고]
- 원본 정점·간선 수
- 그래프 신규·기존·실패 수
- ID 충돌 수
- 끝점 누락 수
- 샘플 경로 검증 결과

9. 새 자료만 반영하는 증분 업데이트

새로 추가되거나 변경된 법률 자료만 검증해 UQA와 그래프에 반영하는 과정
  • 운영 중에는 전체 자료를 처음부터 재구축하지 않습니다.
    • 공식 출처에서 신규·변경 자료만 수집하고 검증한 뒤 UQA와 그래프에 변경분을 반영해요.
  • 체크포인트 (작업 완료 지점)는 모든 단계가 완료된 뒤에만 다음 위치로 이동합니다.

9.1. 전체 반영 뒤에만 이동하는 체크포인트

  • 목록에서 신규 자료를 발견한 시점에는 체크포인트를 이동하지 않아요.
    • 상세 수집이나 UQA 적재가 실패하면 해당 자료를 다시 찾지 못할 수 있기 때문입니다.
    • 상세 원문 수집, 자료 정리, UQA 적재, 그래프 생성이 모두 성공한 뒤에만 체크포인트를 이동해요.
    • 이 조건을 지키면 작업이 중단돼도 안전하게 재개할 수 있습니다.

9.2. 예정 작업을 확인하는 Dry-run

  • Dry-run (시험 실행)은 실제 네트워크 요청, 파일 생성, UQA 쓰기 전에 예정 작업을 보여 줍니다.
    • 예상하지 못한 대량 수집이나 데이터 쓰기를 사전에 막을 수 있어요.

9.3. 원본만 먼저 수집하는 Collect-only

  • Collect-only (수집만)는 신규·변경 원본 자료와 작업 기록까지만 생성하며 UQA는 변경하지 않습니다.
    • 사람이 샘플과 건수를 확인하고 적재를 승인하는 단계예요.

9.4. 새 법령 버전의 원자적 전환

  • 새 법령 버전 전체를 UQA에 적재하고 검증한 뒤 현행 버전을 한 번에 전환합니다.
    • 전환이 실패하면 이전 현행 버전을 유지해요.

9.5. 완료 데이터를 건너뛰는 실패 복구

  • 실행 저널에는 각 단계의 시작·종료 시간, 처리한 자료 수, 마지막 성공 단계, 실패한 자료 번호를 기록합니다.
    • 작업이 실패하면 완료한 자료를 건너뛰고 실패 지점부터 다시 시작할 수 있어요.
실행 프롬프트 · 수집 검수 후 안전한 증분 반영
[선행]
- 공통 작업 원칙 적용
- 전체 샘플 파이프라인 검증 완료

[목표]
- 법령 MCP와 국세법령정보시스템의 신규·변경 원문만 증분 수집
- 검수 완료 데이터만 UQA·그래프에 증분 반영

[실행 전 확인]
- scripts/run_tax_collection.py status
- scripts/run_tax_collection.py --dry-run
- 현재 writer 잠금
- 마지막 정상 run
- 원천별 체크포인트
- 예정 네트워크 요청·파일·UQA 쓰기 범위

[수집]
- 법령 MCP의 신규 MST·변경 원문
- 국세법령정보시스템의 신규 목록 ID·변경 상세 원문
- --collect-only 실행
- UQA·그래프 변경 0건

[적재 전 검증]
- 신규 후보 상세 누락 0건
- ID 중복 0건
- 법령 조문 0건 결과 없음
- 원문 SHA-256 불일치 0건
- 정리·관계 JSONL 치명적 오류 0건

[승인 경계]
- UQA 테이블별 신규 예정 행 수 보고
- 그래프 신규 예정 정점·간선 수 보고
- 운영 적재 승인 전 대기

[운영 반영]
- OS 파일 잠금 기반 단일 writer
- 신규 고정 ID만 배치 적재
- 새 MST 전체 조문 적재·검증 후 current 전환
- 법령별 current 버전 정확히 1개
- current 검증 실패 시 트랜잭션 롤백
- UQA 적재 성공 후 그래프 증분 갱신
- 시행 전 버전의 scheduled 유지와 현재 검색 제외

[체크포인트]
- 상세 수집·정리 검증·UQA 적재·그래프 갱신 전체 성공 후 전진
- 중간 실패 시 기존 체크포인트 유지

[완료 보고]
- 단계별 입력·신규·기존·실패 건수
- 단계별 실행 시간
- UQA·그래프 전후 건수
- 체크포인트 전진 여부
- 중단 지점 기준 재개 명령

10. 데이터·검색·응답 속도 검증

세법 판례 검색기의 데이터와 검색 품질 및 실패 안정성과 응답 시간을 검증하는 영역
  • 검색 결과 한 건이 적절하다고 해서 시스템 검증이 끝나는 것은 아닙니다.
    • 데이터 구조, 기준일 조문 선택, 관련 문서 순위, 작업 취소와 시간 초과의 전파를 각각 확인해야 해요.

10.1. 정확도와 안정성을 나눈 검증

  • 데이터 무결성 (데이터가 깨지지 않았는지): ID 중복, 부모 없는 조문, 연결이 끊긴 그래프, 한 법령의 현행 버전 중복을 자동으로 검사합니다.
  • 검색 품질 (검색이 얼마나 정확한지): 문서 번호, 조문 번호, 사실관계, 과거 기준일을 포함한 대표 질문으로 검색 결과를 반복 측정해요.
  • 실패 안정성 (문제가 생겨도 잘 버티는지): 인공지능(LLM) 실패, UQA 오류, 사용자 취소, 네트워크 끊김을 재현해 진행 상태와 백엔드 작업이 함께 멈추는지 확인합니다.

10.2. 병목을 찾는 구간별 성능 측정

  • 전체 검색 응답 시간만 기록하면 질문 분석, UQA 검색, 그래프 확장 중 어느 구간이 느린지 구분할 수 없어요.
    • 검색 계획, 검색, 그래프, 원문 복원, 전체 응답 시간을 각각 기록해 병목을 정확히 찾아야 합니다.
실행 프롬프트 · 통합 검증
[선행]
- 공통 작업 원칙 적용

[목표]
- 세법 판례 검색기의 재현 가능한 통합 검증
- 데이터·검색·실패 처리·성능의 독립 측정

[금지]
- 특정 질문·문서 ID·정답의 코드 하드코딩
- 실패 숨김
- 통과 기준의 임의 완화

[데이터 무결성]
- 법령·문서 ID 중복 0건
- 부모 없는 항·호·목 0건
- 해결 간선의 끝점 누락 0건
- 법령별 기준일 현행 버전 정확히 1개
- 원본→정리→UQA의 단계별 수지표
- 인용 해결률과 미해결 인용 표본

[대표 검색]
- 문서번호 정확 검색
- 법령명+조문번호 검색
- 세목+연도+자료 유형 필터
- 사실관계 자연어 검색
- 같은 쟁점 유사 문서 검색
- 과거 기준일 조문 검색
- 현행/예정 신구대조
- 존재하지 않는 문서와 근거 부족 질문

[안정성]
- LLM 플래너 타임아웃 시 규칙 플래너 폴백
- UQA 검색 오류 시 요청 종료와 오류 응답
- 사용자 취소와 연결 종료가 백엔드 작업까지 취소
- 늦은 이전 응답이 현재 결과를 덮어쓰지 않음

[성능 측정]
- 질의 계획
- UQA 검색
- 그래프 확장
- 전체 원문 복원
- 전체 응답

[완료 보고]
- 항목별 통과·실패·경고
- 실패별 재현 명령·로그 경로
- 데이터 수지표
- 대표 검색 결과와 원문 근거
- 구간별 응답 시간

10.3. 요청 ID부터 추적하는 원인 진단

  • 검색 화면이 계속 로딩되거나 자료 수집 건수가 갑자기 줄었을 때 시간 초과 설정부터 늘리면 문제 발견만 늦어집니다.
    • 브라우저 요청 ID, 서버 로그, 실행 저널, UQA 검색, 그래프 확장, 원문 복원 순서로 같은 요청을 추적하면 중단 지점을 확인할 수 있어요.
실행 프롬프트 · 실패 원인 추적
[선행]
- 공통 작업 원칙 적용

[증상]
- [여기에 실제 증상·발생 시각·재현 조건 입력]

[목표]
- 수정 전 증거 수집
- 직접 원인과 상위 원인의 구분

[확인 순서]
- 브라우저 요청·request_id
- 서버 접근 로그·오류 로그
- 실행 중인 수집·적재·검색 프로세스·writer 잠금
- 최근 run.json의 마지막 성공 단계·실패 단계
- LLM 플래너 호출 시간·타임아웃·취소 여부
- UQA 검색 시간·반환 후보 수
- 그래프 시작점·깊이·이웃 수
- 전체 원문 복원 파일·SHA-256
- 프론트 진행 상태의 종료 조건

[진단 제약]
- 확인된 사실과 추정의 분리
- 직접 원인 확인 전 코드 수정 금지
- 직접 원인 확인 전 타임아웃 상향 금지
- 동일 기능의 중복 구현 금지

[보고 형식]
- 재현 절차
- 확인된 사실과 로그 위치
- 가장 가능성 높은 직접 원인
- 직접 원인을 만든 상위 원인
- 영향받는 범위
- 최소 수정안
- 회귀 테스트
- 수정 전·후 비교 지표
기술 부록 열기: 스키마·명령어·ID·검색 알고리즘 상세

세법 판례 검색기는 법령 원문, 판례, 심판결정례, 행정해석을 하나의 검색 흐름으로 연결해야 합니다. 이 튜토리얼은 공식 원문 수집부터 정규화, UQA 적재, Bayesian BM25 전문검색, 인용·쟁점 그래프 탐색까지 전체 과정을 단계별로 설명합니다.

최종 결과물은 검색어와 관련된 법령 조문과 판례·해석례를 함께 찾고, 인용 관계와 공통 쟁점으로 결과를 확장하며, 원문 근거까지 확인할 수 있는 검색기입니다.

완성할 검색기의 구조

1

공식 원문

세법령과 판례·해석례를 각각 수집합니다.

2

정규화

조문 계층과 법률문서 공통 구조를 만듭니다.

3

UQA·그래프

전문검색 인덱스와 관계를 구성합니다.

4

검색 API

근거 원문과 연결 경로를 반환합니다.

UQA는 SQL, 전문검색, 그래프 탐색을 하나의 데이터베이스에서 실행하는 엔진입니다. 구조화 필드와 원문을 SQL 테이블에 저장하고, GIN 인덱스로 검색하며, 문서와 조문 사이의 관계를 그래프로 연결합니다.

단계입력결과
수집법령 MCP·국세법령정보시스템 원문원본 JSON과 수집 이력
프로세싱원본 JSON정규화 JSONL과 연결 후보
UQA 적재정규화 JSONL검색 테이블과 GIN 인덱스
그래프 구성문서·조문 식별자인용·쟁점 관계
검색 검증사용자 질의검색 결과와 근거 원문

단계별 데이터 계약

파이프라인을 안정적으로 운영하려면 각 단계가 받는 값과 내보내는 값을 먼저 고정해야 합니다. 수집기는 검색 테이블을 직접 수정하지 않습니다. 수집기는 원문과 매니페스트를 만들고, 변환기는 원문을 정규화 행으로 바꾸며, 적재기는 그 행을 UQA에 기록합니다. 그래프 작업도 정규화된 정점·간선 행에서 시작합니다.

경계필수 입력필수 출력실패 시 보존할 것
원천 → 수집자료 유형, 기간, 페이지, 원천 식별자목록 JSON, 상세 JSONL, 수집 매니페스트마지막 성공 페이지, 실패 ID, 응답 상태
수집 → 정규화원문 경로, SHA-256, 수집 시각테이블명·열·값을 가진 JSONL 행파싱 오류 원문과 오류 위치
정규화 → UQA중복 제거된 행, 스키마 버전테이블 행, 문서 ID, 적재 건수미적재 행과 마지막 완료 배치
UQA → 그래프정점·간선 행, 외부 문자열 ID이름 있는 그래프와 정점·간선 수해시 충돌, 끝점 누락 간선
검색 → 답변질의 계획, 기준일, 검색 범위순위 결과, 근거 원문, 검색 추적실패 단계와 보강 검색어

재실행 기준도 계약에 포함합니다. 원문 해시가 같으면 수집 결과를 재사용하고, 정규화 스키마가 바뀌면 JSONL부터 다시 만들며, 인덱스 설정만 바뀌면 UQA 원문 테이블을 유지한 채 인덱스만 재생성합니다.

0. 프로젝트 준비

원본, 정규화 결과, 적재 프로그램을 분리해 두면 수집 규칙이나 스키마가 바뀌어도 필요한 단계만 다시 실행할 수 있습니다.

dataraw에는 원본을, normalized에는 검색용 JSONL을, manifests에는 수집 이력을 보관합니다.
scriptscollect, normalize, load, verify 작업을 독립 실행 가능한 프로그램으로 관리합니다.
UQA검증된 문서·조문·관계와 검색 인덱스를 한 곳에 저장합니다.

Python에서는 데이터베이스 파일을 열어 SQL과 그래프 작업을 시작합니다.

from pathlib import Path
import uqa

db_path = Path("uqa/tax-search.uqa.db")
engine = uqa.Engine.open(db_path)

실제 작업 디렉터리에서는 다음 파일들이 핵심 역할을 맡습니다.

경로역할
config/tax-data-collections.json수집할 법률군과 판례·해석례 분류를 선언합니다.
scripts/run_tax_collection.py상태 확인, Dry-run, 수집, 적재, 그래프 갱신을 순서대로 실행합니다.
scripts/collect_law_mcp.py법령 MCP에서 법률·시행령·시행규칙의 버전별 전체 원문을 수집합니다.
scripts/collect_nts_precedent.py국세법령정보시스템의 목록과 상세 원문을 JSONL로 수집합니다.
scripts/load_tax_laws_to_uqa.mjs법령 원문을 조·항·호·목 행과 그래프 정점·간선 행으로 변환합니다.
scripts/export_nts_legal_rows.mjs판례·해석례 원문을 공통 문서 스키마와 관계 행으로 변환합니다.
scripts/uqa_batch_load.py정규화 JSONL을 UQA에 배치 또는 문서 단위로 적재합니다.
scripts/materialize_uqa_graph.py관계 테이블을 tax_knowledge 그래프로 만듭니다.

수집 대상 카탈로그

수집 대상을 코드 곳곳에 흩어 두지 않고 하나의 카탈로그에서 관리합니다. 법령은 법률군과 세목 그룹으로, 국세법령정보시스템 자료는 분류 코드·슬러그·화면 표시명·자료 그룹으로 선언합니다.

{
  "law_collections": {
    "tax_laws": [
      {"group": "national_general", "name": "국세기본법"},
      {"group": "income", "name": "법인세법"},
      {"group": "consumption", "name": "부가가치세법"},
      {"group": "special", "name": "조세특례제한법"}
    ]
  },
  "nts_categories": [
    {"code": "001_01", "slug": "advance_ruling", "label": "사전답변", "group": "interpretations"},
    {"code": "001_08", "slug": "tribunal_request", "label": "심판청구", "group": "documents"},
    {"code": "001_09", "slug": "precedent", "label": "판례", "group": "documents"}
  ]
}

예시 카탈로그는 법령 MCP에서 받을 세법 법률군과 국세법령정보시스템의 자료 유형을 관리합니다. 법률군 하나에는 보통 법률·시행령·시행규칙이 들어갑니다. 새 세목을 추가할 때는 카탈로그를 수정한 뒤 Dry-run에서 생성되는 요청과 출력 경로를 먼저 확인합니다.

상태 확인과 Dry-run

운영 명령은 상태 확인, 실행 계획 확인, 실제 갱신의 세 단계로 나눕니다. Dry-run은 네트워크 호출과 데이터베이스 쓰기를 수행하지 않고 예정된 하위 명령과 작업 범위를 실행 저널에 기록합니다.

# 현재 실행 여부, 마지막 실행 결과, 데이터 최신성 확인
.venv-uqa/bin/python scripts/run_tax_collection.py status
.venv-uqa/bin/python scripts/run_tax_collection.py status --json

# 전체 범위 실행 계획 확인
.venv-uqa/bin/python scripts/run_tax_collection.py update \
  --scope all \
  --dry-run

# 특정 법률군의 계획만 확인
.venv-uqa/bin/python scripts/run_tax_collection.py update \
  --scope laws \
  --law-name 종합부동산세법 \
  --dry-run

상태 명령은 잠금 파일의 존재만 보지 않습니다. 실제 OS 파일 잠금 보유 여부를 확인해 실행 중인 프로세스를 판정합니다. 비정상 종료로 빈 잠금 파일이 남아도 운영체제가 잠금을 해제했으면 다음 실행을 시작할 수 있습니다.

1. 데이터 수집

수집 단계의 핵심은 원문 보존과 재현 가능한 이력입니다. 수집 시각, 출처 URL, 원문 식별자, 응답 해시, 성공 여부를 매 실행마다 남깁니다. 동일 문서를 다시 받아도 원문 해시가 같으면 중복 저장을 건너뛸 수 있습니다.

세법령 수집

세법령은 법령명만 저장하면 시점 검색이 불가능합니다. 법령 식별자와 함께 공포일, 시행일, 폐지일, 개정 구분을 수집합니다. 시행일을 기준으로 각 버전을 과거·현행·시행 예정 상태로 계산할 수 있습니다.

  • 법률, 시행령, 시행규칙을 각각 독립된 법령으로 수집합니다.
  • 법령별 개정 이력과 버전별 원문을 저장합니다.
  • 조·항·호·목의 계층 정보를 원문 순서와 함께 보존합니다.
  • 부칙과 별표도 독립 단위로 식별할 수 있게 원문 위치를 기록합니다.
{
  "source": "law_mcp",
  "law_id": "stable-law-id",
  "law_name": "예시세법",
  "promulgation_date": "2026-01-01",
  "effective_date": "2026-07-01",
  "version_id": "stable-law-id:2026-07-01",
  "status": "scheduled",
  "raw_path": "data/raw/laws/stable-law-id/2026-07-01.json"
}

수집 파일은 원문 응답을 그대로 보관합니다. 정제 규칙이 변경되면 공식 사이트를 다시 호출하지 않고 원본 파일에서 정규화 결과를 재생성할 수 있습니다.

법령 MCP 연결

세법령 수집기는 개발환경에 연결된 법령 MCP를 단일 원천으로 사용합니다. 먼저 MCP가 제공하는 도구·리소스와 입력 스키마를 확인하고, 법령 검색·버전 목록·버전별 전체 조문 조회에 대응하는 호출을 연결합니다. 특정 도구 이름을 미리 가정하지 않고 실제 연결에서 확인된 이름과 입력값을 수집 이력에 남깁니다.

# 법령 MCP 연결 상태와 수집 계획 확인
.venv-uqa/bin/python scripts/run_tax_collection.py status --json
.venv-uqa/bin/python scripts/run_tax_collection.py update \
  --scope laws \
  --law-source mcp \
  --dry-run

# 법률군 1개의 원문만 샘플 수집
.venv-uqa/bin/python scripts/run_tax_collection.py update \
  --scope laws \
  --law-source mcp \
  --law-name 부가가치세법 \
  --collect-only

법령 MCP가 연결되지 않았거나 필요한 연혁·조문 원문 기능을 제공하지 않으면 다른 사이트로 자동 전환하지 않습니다. 실행을 중단하고 부족한 연결 정보나 기능을 보고해야 원천이 섞이지 않습니다.

법령별 매니페스트에는 다음 값을 남깁니다.

필드의미사용 위치
law_id개정 전후에도 유지되는 법령 식별자법령 정점과 canonical 조문 ID
mst특정 법령 버전의 일련번호버전 ID와 증분 변경 판정
law_name공식 법령명검색 표시명과 별칭 생성
law_type법률·대통령령·부령법률·시행령·시행규칙 구분
source_file원문 JSON 경로재처리와 원문 감사
sha256원문 내용 해시변경·손상·중복 확인
promulgation_date공포일개정 이력 정렬
effective_date시행일기준일 조문 선택

과거·현행·시행 예정

법령 상태는 수집 시각과 시행일의 관계로 판정합니다. 이미 효력이 끝난 버전은 과거, 기준일에 효력이 있는 버전은 현행, 공포되었지만 시행일이 미래인 버전은 시행 예정으로 관리합니다. 시행 예정 개정은 현행 조문을 덮어쓰지 않습니다.

if effective_to and effective_to < as_of_date:
    version_status = "past"
elif effective_from <= as_of_date and not superseded:
    version_status = "current"
else:
    version_status = "scheduled"

연혁 수집은 MST별 원문을 보존합니다. 현행 수집기가 새 MST를 발견해도 과거 파일을 삭제하지 않습니다. UQA에서는 law_versions.is_current로 현재 검색 대상을 빠르게 필터링하고, 기준일 검색에서는 시행일과 종료일로 적절한 버전을 선택합니다.

판례·해석례 수집

판례·해석례 계열은 목록 응답과 상세 원문을 나누어 수집합니다. 목록에서 신규 문서 후보를 찾고, 상세 페이지에서 본문과 메타데이터를 확보합니다. 판례, 심판결정례, 사전답변, 질의회신 등 문서 유형을 원문 분류와 함께 저장합니다.

  • 원문 기관의 문서 식별자를 중복 판정 기준으로 사용합니다.
  • 문서번호, 생산일, 기관, 세목, 제목, 요약, 본문을 수집합니다.
  • 목록 페이지의 마지막 처리 위치를 체크포인트로 저장합니다.
  • 실패 문서는 별도 목록에 남겨 다음 실행에서 재수집합니다.
  • 첨부파일이 본문의 일부라면 파일 주소와 해시를 함께 기록합니다.
{
  "source_record_id": "source:document-number",
  "document_type": "tax_tribunal_decision",
  "document_number": "조심2026서0000",
  "decision_date": "2026-06-30",
  "organization": "조세심판원",
  "tax_type": "부가가치세",
  "title": "쟁점 거래의 과세 여부",
  "summary": "...",
  "body": "...",
  "source_url": "https://example.go.kr/document/0000"
}

목록·상세 2단계 수집

국세법령정보시스템 자료는 목록과 상세를 별도 요청으로 수집합니다. 목록 응답은 전체 범위, 정렬 순서, 신규 후보 ID를 제공합니다. 상세 응답은 제목·요지·본문·관련 법령·쟁점 배열을 제공합니다.

문서 분류 코드 선택
  → 최신 등록일 내림차순으로 목록 요청
  → ntstDcmId와 FRS_RGT_DTM 수집
  → 체크포인트보다 최신인 후보 선별
  → 후보별 상세 요청
  → detail_raw.incremental-<run-id>.jsonl 기록
  → 후보 전부의 상세 존재 여부 확인
  → list_items.jsonl과 체크포인트 갱신

목록 요청의 핵심 파라미터 예시는 다음과 같습니다. 해석례 분류와 판례·결정례 분류는 사용하는 컬렉션명이 다르므로 분류 카탈로그의 group 값으로 선택합니다.

{
  "dcmClCdCtl": ["001_08"],
  "collectionName": "precedent,precedent_gr",
  "sortField": "DCM_RGT_DTM/DESC",
  "startCount": 1,
  "viewCount": 50
}
분류 코드자료그룹
001_01사전답변해석례
001_02서면해석해석례
001_03세법해석 사전답변해석례
001_04공개 서면질의해석례
001_05과세적부불복·판례
001_06이의신청불복·판례
001_07심사청구불복·판례
001_08심판청구불복·판례
001_09판례불복·판례
001_10헌법재판소불복·판례

재시도·체크포인트·샤딩

공공 원천을 장시간 호출하면 타임아웃과 일시 오류가 발생합니다. 요청 함수는 기본 30초 타임아웃과 최대 5회 재시도를 두고, 실패할 때마다 대기 시간을 늘립니다.

for attempt in range(1, retries + 1):
    try:
        return request_json(timeout=30)
    except Exception:
        if attempt == retries:
            raise
        time.sleep(min(30.0, 2.0 * attempt))

증분 목록은 최신순으로 읽으며 기존 체크포인트를 포함해 한 번 더 겹쳐 확인합니다. 체크포인트와 같은 등록시각에 여러 문서가 있을 수 있기 때문입니다. 지정한 최대 페이지까지 읽었는데 체크포인트 이전으로 넘어갔다는 사실을 증명하지 못하면 실행을 실패 처리하고 체크포인트를 전진시키지 않습니다.

완료 가능한 종료 조건
1. 목록에서 체크포인트보다 오래된 레코드까지 확인
2. 원천 전체 범위를 끝까지 확인

실패 처리 조건
- max_pages에 도달했지만 cutoff를 통과하지 못함
- 신규 후보 중 상세 원문이 하나라도 없음
- 목록 응답 구조가 예상 계약과 다름

최초 전체 수집의 상세 요청은 샤딩할 수 있습니다. 문서 순번을 shard_count로 나눈 나머지가 shard_index인 문서만 처리하면 여러 프로세스가 서로 다른 ID를 담당합니다. 각 샤드는 별도 detail_raw.shardN.jsonl에 쓰고, 정규화 단계에서 모든 샤드를 읽습니다.

JSONL과 대용량 원문

목록과 상세 원문은 JSONL로 저장합니다. 한 문서가 한 줄이므로 마지막 줄까지 기록된 문서는 프로세스가 중단되어도 유지됩니다. 재시작할 때는 모든 detail_raw*.jsonl에서 완료 ID를 읽어 이미 받은 문서를 다시 요청하지 않습니다.

{
  "ntstDcmId": "원천문서ID",
  "fetchedAt": "2026-09-01T03:00:00Z",
  "listItem": {"목록에서 받은 필드": "..."},
  "raw": {"상세 API 전체 응답": "..."}
}

일정 크기를 넘는 원문 필드는 gzip 파일로 분리할 수 있습니다. JSONL에는 상대 경로, SHA-256, 인코딩, 원문 바이트 수를 남깁니다. 정규화기는 $raw_blob 표식을 발견하면 압축 원문을 읽고 해시를 확인한 뒤 파싱합니다.

{
  "$raw_blob": "raw_blobs/8f2b...a91.json.gz",
  "sha256": "8f2b...a91",
  "encoding": "json+gzip",
  "bytes": 123456
}

실행 저널과 쓰기 잠금

통합 실행기는 시작할 때 OS 파일 잠금을 독점합니다. 이미 다른 수집 또는 UQA 작성 작업이 잠금을 보유하면 즉시 종료합니다. 한 번에 하나의 writer만 허용해 데이터베이스와 체크포인트가 서로 다른 실행에 의해 교차 갱신되는 상황을 막습니다.

실행마다 .tmp/tax-data-pipeline/runs/<run-id>/run.json을 만들고 각 단계의 명령, 상태, 시작·종료 시각, 소요 시간, 종료 코드, 로그 경로를 기록합니다.

{
  "run_id": "20260901-120000",
  "status": "running",
  "scopes": ["laws", "nts"],
  "started_at": "2026-09-01T12:00:00+09:00",
  "steps": [
    {
      "name": "collect-laws-mcp",
      "status": "complete",
      "elapsed_seconds": 18.42,
      "exit_code": 0,
      "log": ".tmp/tax-data-pipeline/runs/20260901-120000/01-collect-laws-mcp.log"
    }
  ]
}

실행이 끝나면 최신 요약을 metadata/tax-collection-last-run.json에 원자적으로 기록합니다. 임시 파일을 완성한 뒤 이름을 교체하므로 기록 도중 프로세스가 중단되어도 이전 정상 파일이 유지됩니다.

수집 완료 조건

수집 건수만으로 완료 여부를 판단하지 않습니다. 아래 정보를 실행 이력에 함께 남기면 다음 증분 수집의 시작점을 정확히 정할 수 있습니다.

항목확인 내용
대상 범위시작일, 종료일, 문서 유형, 법령 목록
처리 결과발견 건수, 신규 건수, 변경 건수, 실패 건수
체크포인트마지막 목록 페이지와 마지막 문서 식별자
무결성응답 해시, 원본 파일 경로, 중복 식별자
최신성원문 기준 최신 생산일과 수집 시각

수집 완료 판정은 “요청이 끝났다”가 아니라 “다음 단계가 필요한 모든 원문을 재현할 수 있다”로 정의합니다. 신규 후보 100건을 찾았다면 상세 원문도 100건 있어야 합니다. 실패 ID가 남아 있으면 적재를 시작하지 않고 실행 저널에 실패 원인과 재개 위치를 기록합니다.

2. 검색용 데이터 프로세싱

프로세싱 단계에서는 출처마다 다른 구조를 검색에 적합한 공통 스키마로 변환합니다. 원문 값은 보존하고, 검색과 연결에 필요한 정규화 필드를 추가합니다.

원본·정규화·검색 구조

데이터는 세 단계로 관리합니다.

단계보관 내용수정 원칙
원본공식 응답, 출처, 수집 시각, SHA-256받은 값을 보존하고 파싱 결과를 덮어쓰지 않습니다.
정규화법령 버전·조문, 공통 문서 필드, 관계 후보규칙과 스키마 버전을 명시하고 재생성 가능하게 합니다.
검색·그래프GIN 인덱스, 필터용 파생 테이블, 이름 있는 그래프정규화 행에서 언제든 다시 만들 수 있게 합니다.

원본 한 건에서 여러 정규화 행이 생성됩니다. 법령 원문 하나는 raw_records 한 행, law_versions 한 행, 조·항·호·목 수만큼의 law_units, 권위 정점과 계층 간선으로 확장됩니다. 판례 상세 원문 하나도 문서 한 행, 문서 정점 한 개, 인용 조문 수만큼의 간선, 쟁점 수만큼의 정점·간선으로 확장됩니다.

세법령 프로세싱

법령 원문은 법령군, 법령 버전, 조문 단위의 세 단계로 정리합니다. 법령군은 동일 법령의 전체 생애를 나타내고, 법령 버전은 특정 시행일의 원문을 나타냅니다. 조문 단위는 조·항·호·목을 계층 구조로 저장합니다.

{
  "unit_id": "stable-law-id:2026-07-01:article-12:paragraph-2",
  "law_family_id": "stable-law-id",
  "law_version_id": "stable-law-id:2026-07-01",
  "unit_type": "paragraph",
  "article_no": "12",
  "paragraph_no": "2",
  "heading": "과세표준의 계산",
  "content": "② ...",
  "effective_from": "2026-07-01",
  "effective_to": null,
  "version_status": "scheduled",
  "parent_unit_id": "stable-law-id:2026-07-01:article-12",
  "sequence": 2
}

고정 식별자는 법령과 조문의 동일성을 나타내고, 버전 식별자는 시행 시점의 원문을 나타냅니다. 이 구분으로 특정 기준일의 조문 조회와 신구대조를 함께 처리할 수 있습니다.

법령·조문 식별자 설계

법령 ID는 검색 표시명과 분리합니다. 띄어쓰기나 명칭 표기가 달라져도 같은 법령을 가리킬 수 있어야 합니다. 특정 버전은 MST를 사용하고, 조문은 canonical ID와 version ID를 함께 만듭니다.

법령군       law-family:법인세법
법령         law:<law_id>
법령 버전    law-version:<mst>

조 canonical law-unit:<law_id>:article:52:0
조 version   law-unit-version:<mst>:article:52:0

항 canonical law-unit:<law_id>:article:52:0:paragraph:1
호 canonical law-unit:<law_id>:article:52:0:paragraph:1:item:3
목 canonical law-unit:<law_id>:article:52:0:paragraph:1:item:3:subitem:가

조문 가지번호가 없으면 0을 넣어 제52조와 제52조의2를 구분합니다. 번호에서는 점과 공백을 제거합니다. 원문에 항번호가 없지만 항 구조가 존재하면 원문 순서를 사용해 안정적인 위치 번호를 만듭니다.

그래프의 판례 인용은 canonical 조문 ID에 연결합니다. 답변을 만들 때 질의 기준일과 canonical ID를 사용해 해당 시점의 version 행을 선택합니다. 이 방식으로 판례가 인용한 “법인세법 제52조”라는 논리적 위치와 실제 적용되는 과거 원문을 함께 다룰 수 있습니다.

조·항·호·목 계층 복원

원문 조문단위 배열을 순회하며 조를 만든 뒤 항, 호, 목 순서로 내려갑니다. 각 단위는 상위 canonical ID를 parent_unit_id에 저장하고 원문 순서를 unit_order에 저장합니다.

for article in law["조문"]["조문단위"]:
    if article["조문여부"] != "조문":
        continue

    article_id = add_unit(type="article", parent=None)
    for paragraph in article.get("항", []):
        paragraph_id = add_unit(type="paragraph", parent=article_id)
        for item in paragraph.get("호", []):
            item_id = add_unit(type="item", parent=paragraph_id)
            for subitem in item.get("목", []):
                add_unit(type="subitem", parent=item_id)

계층 간선은 상위 단위에서 하위 단위로 PART_OF 또는 HAS_ARTICLE 관계를 만듭니다. 같은 단계의 다음 단위는 NEXT로 연결합니다. 검색 결과가 항 하나일 때 instrument_id, version_id, article_no, unit_order를 이용해 같은 조의 전체 내용을 순서대로 복원합니다.

삭제된 조문도 원문에서 제거하지 않습니다. 본문에 삭제 표시가 있으면 is_deleted=1로 저장합니다. 일반 검색에서는 제외하고, 연혁 조회와 신구대조에서는 포함할 수 있습니다.

판례·해석례 프로세싱

판례·해석례는 공통 문서 스키마로 변환합니다. 문서 유형과 기관명을 표준값으로 맞추고, 날짜와 문서번호 표기를 통일합니다. 본문은 주문, 이유, 사실관계, 판단 등 원문 구획을 보존한 채 검색용 전체 본문도 함께 만듭니다.

  • 법령 인용 표현에서 법령명과 조·항·호·목을 추출합니다.
  • 세목, 거래 유형, 절차 쟁점 등 검색용 개념 후보를 만듭니다.
  • 식별 가능한 인용은 조문 식별자와 연결합니다.
  • 시점을 확정하기 어려운 인용은 미해결 상태로 저장해 검토 대상으로 남깁니다.
{
  "document_id": "tax_tribunal_decision:조심2026서0000",
  "document_type": "tax_tribunal_decision",
  "title": "쟁점 거래의 과세 여부",
  "summary": "...",
  "body": "...",
  "decision_date": "2026-06-30",
  "tax_type": "vat",
  "citation_candidates": [
    {"law_name": "부가가치세법", "article_no": "38", "paragraph_no": "1"}
  ],
  "issue_candidates": ["매입세액공제", "사업관련성"]
}

원문 필드 매핑

국세법령정보시스템 상세 응답은 자료 유형마다 일부 필드가 다르지만 공통 검색 계약은 동일하게 만듭니다.

원문 필드정규화 필드설명
ntstDcmIdsource_record_id원천 문서의 안정적인 ID
ntstDcmDscmCntndocument_no사건번호·문서번호
ntstDcmTtltitle문서 제목
ntstDcmGistCntnsummary결정요지·해석요지
ntstDcmCntnbody전체 본문
ntstDcmRgtDtdecision_date결정일·생산일
attrYrattribute_year원천이 제공한 귀속연도
ntstTlawClCdtax_type_code세목 코드
ntstDcmClCddocument_type_code원천 문서 유형 코드

문서 ID는 nts:<category>:<ntstDcmId> 형식으로 만듭니다. 같은 원천 ID라도 분류가 이동할 가능성을 고려해 분류 슬러그를 포함합니다. is_full_text는 본문 길이와 존재 여부를 이용해 원문 확인 가능한 문서인지 표시합니다. 예시 구현에서는 본문이 100자를 넘으면 전체 본문 후보로 분류합니다.

metadata_json에는 원본 JSONL 파일, 줄 번호, 첨부파일 ID처럼 검색 결과에는 자주 쓰지 않지만 원문을 다시 찾을 때 필요한 정보를 넣습니다.

인용 조문과 쟁점 연결

상세 응답의 관련 법령 배열 dcmRltnStttList를 우선 사용합니다. 인용 텍스트에서 법령 ID와 조번호·가지번호를 읽고, 현재 수집된 법령 집합과 canonical 조문 집합에 모두 존재하면 문서에서 조문으로 REFERENCES_LAW 간선을 만듭니다.

label = citation["ntstTextNm"]
law_id = citation["bsafRfkNo1"][-6:]
article_no, branch_no = parse_article_number(label)
candidate = f"law-unit:{law_id}:article:{article_no}:{branch_no or '0'}"

if law_id in known_law_ids and candidate in known_unit_ids:
    edge_type = "REFERENCES_LAW"
    confidence = 1.0
else:
    edge_type = "REFERENCES_LAW_UNRESOLVED"
    confidence = 0.5

해결하지 못한 인용을 버리면 관계 재처리가 어렵다. 법령 ID가 수집 범위 밖이거나 조문 번호를 파싱할 수 없으면 unresolved_law_citation 정점을 만들고 원문 인용 문자열과 구조화 필드를 보존합니다. 이후 법령 범위를 넓히거나 파서를 개선했을 때 이 정점만 다시 해석할 수 있습니다.

쟁점 배열 dcmRltnStttMatrList는 쟁점 정점으로 바꿉니다. 원천 쟁점 ID가 있으면 issue:<id>, 없으면 정규화한 쟁점 문구의 해시로 ID를 만듭니다. 문서에서 쟁점으로 ABOUT_ISSUE 간선을 추가하고 원문 쟁점 문구를 근거로 남깁니다.

정제 결과 검증

정규화가 끝나면 UQA 적재 전에 기계 검증을 실행합니다. 식별자 중복, 필수 필드 누락, 조문 계층 단절, 비어 있는 본문, 날짜 역전, 존재하지 않는 인용 대상을 확인합니다. 검증 결과는 오류와 경고로 나누어 저장합니다.

검증 항목
✓ document_id와 unit_id가 전체 데이터에서 유일합니다.
✓ 모든 항·호·목은 유효한 상위 단위를 가집니다.
✓ 현행 버전은 기준일에 효력이 있습니다.
✓ 검색 대상 문서는 제목 또는 본문을 가집니다.
✓ 해결된 법령 인용은 실제 조문 식별자와 연결됩니다.

추가 검증에서는 다음 수치를 실행별로 비교합니다.

  • 원문 문서 수와 legal_documents 행 수의 차이
  • 전체 인용 수, 해결 인용 수, 미해결 인용 수와 해결률
  • 본문이 비어 있는 문서 수와 전체 본문 비율
  • 조·항·호·목별 행 수와 상위 단위가 없는 행 수
  • 동일 canonical 조문에서 겹치는 유효기간을 가진 버전 수
  • 동일 source record가 둘 이상의 문서 ID로 변환된 건수

오류는 적재를 중단해야 하는 문제이고, 경고는 적재 후 검토 가능한 문제입니다. ID 중복, 부모 단위 부재, 후보 상세 원문 누락은 오류로 처리합니다. 미해결 인용과 짧은 본문은 원천 데이터의 특성일 수 있어 경고와 통계로 관리합니다.

3. UQA 적재와 검색 인덱스

UQA 적재는 원본 보존 테이블, canonical 법령·문서 테이블, 관계 테이블, 검색 인덱스 순서로 진행합니다. 테이블을 먼저 만들고 대량 행을 넣은 뒤 GIN 인덱스를 생성합니다. 증분 적재에서는 들어오는 ID만 기존 테이블과 대조해 중복을 건너뜁니다.

UQA 테이블 스키마

법령 데이터의 핵심 테이블은 다음과 같습니다.

CREATE TABLE law_versions (
  id TEXT PRIMARY KEY,
  instrument_id TEXT NOT NULL,
  family_id TEXT NOT NULL,
  law_id TEXT NOT NULL,
  mst TEXT NOT NULL,
  law_name TEXT NOT NULL,
  law_type TEXT NOT NULL,
  promulgation_date TEXT,
  effective_date TEXT,
  revision_type TEXT,
  raw_record_id TEXT NOT NULL,
  is_current INTEGER NOT NULL
);

CREATE TABLE law_units (
  id TEXT PRIMARY KEY,
  canonical_unit_id TEXT NOT NULL,
  version_id TEXT NOT NULL,
  instrument_id TEXT NOT NULL,
  parent_unit_id TEXT,
  unit_type TEXT NOT NULL,
  unit_order INTEGER NOT NULL,
  article_no TEXT,
  article_branch_no TEXT,
  paragraph_no TEXT,
  item_no TEXT,
  subitem_no TEXT,
  heading TEXT,
  content TEXT NOT NULL,
  effective_date TEXT,
  source_key TEXT,
  is_deleted INTEGER NOT NULL DEFAULT 0,
  metadata_json TEXT
);

판례·해석례는 공통 테이블 한 개에 저장합니다.

CREATE TABLE legal_documents (
  id TEXT PRIMARY KEY,
  source_system TEXT NOT NULL,
  source_record_id TEXT NOT NULL,
  category_code TEXT NOT NULL,
  category_label TEXT NOT NULL,
  document_type_code TEXT,
  document_type_name TEXT,
  document_no TEXT,
  title TEXT,
  summary TEXT,
  body TEXT,
  decision_date TEXT,
  decision_year TEXT,
  attribute_year TEXT,
  tax_type_code TEXT,
  raw_record_id TEXT NOT NULL,
  is_full_text INTEGER NOT NULL,
  metadata_json TEXT
);

raw_sources는 파일 단위 출처를 기록하고, raw_records는 원문 한 건과 해시를 기록합니다. 정규화 행에는 반드시 raw_record_id를 남겨 검색 결과에서 원본으로 되돌아갈 수 있게 합니다.

CREATE TABLE raw_records (
  id TEXT PRIMARY KEY,
  source_id TEXT NOT NULL,
  source_system TEXT NOT NULL,
  record_type TEXT NOT NULL,
  source_record_id TEXT NOT NULL,
  raw_sha256 TEXT NOT NULL,
  raw_json TEXT NOT NULL,
  ingested_at TEXT NOT NULL
);

적재 행 JSONL 계약

변환기와 적재기 사이에는 SQL 문자열 대신 테이블명, 열 배열, 값 배열을 가진 JSONL 계약을 사용합니다. 각 줄은 UQA에 넣을 논리 행 하나입니다.

{
  "table": "legal_documents",
  "columns": [
    "id", "source_system", "source_record_id",
    "category_code", "category_label", "document_no",
    "title", "summary", "body", "decision_date",
    "raw_record_id", "is_full_text"
  ],
  "values": [
    "nts:tribunal_request:12345", "nts_taxlaw", "12345",
    "001_08", "심판청구", "조심2026서0000",
    "쟁점 거래의 과세 여부", "...", "...", "2026-06-30",
    "raw-record:nts:12345", 1
  ]
}

행 계약을 파일로 남기면 운영 UQA를 열지 않고도 생성 행 수, 테이블별 분포, ID 중복, 필수 필드 누락을 검사할 수 있습니다. 같은 원문에서 동일 테이블·동일 ID가 여러 번 생성되면 변환 단계에서 첫 행만 유지합니다.

# 국세법령정보시스템 상세 원문을 UQA 행으로 변환
node scripts/export_nts_legal_rows.mjs \
  --category tribunal_request \
  --input detail_raw.incremental-20260901.jsonl \
  --output .tmp/tribunal-rows.jsonl

# 선택한 현행 법령 MST를 행 JSONL로 내보내기
UQA_EXPORT_ROWS=.tmp/current-law-rows.jsonl \
node scripts/load_tax_laws_to_uqa.mjs \
  --collection tax_laws \
  --all \
  --msts 123456,123457 \
  --skip-indexes

배치 적재와 중복 방지

UQA Python API는 Engine.open, sql, sql_batch, add_document를 제공합니다. 정규화 행은 두 방식으로 적재할 수 있습니다.

  • sql-batch: INSERT 문과 파라미터를 모아 기본 500행 단위로 engine.sql_batch()를 호출합니다.
  • document: 테이블의 문서 수를 읽어 다음 내부 _doc_id를 정하고 engine.add_document()를 호출합니다.
.venv-uqa/bin/python scripts/uqa_batch_load.py \
  --db data/cpa_accounting_tax_knowledge.uqa.db \
  --rows .tmp/tribunal-rows.jsonl \
  --mode document \
  --incremental \
  --batch-size 500

--incremental은 먼저 들어오는 행을 테이블별 ID 집합으로 묶고, WHERE id IN (...)으로 이미 존재하는 ID만 조회합니다. 전체 테이블 ID를 매 행마다 다시 읽지 않으므로 데이터가 커져도 중복 검사 비용을 제한할 수 있습니다. 기존 ID는 skipped_existing에 집계합니다.

초기 대량 적재에서는 인덱스를 먼저 제거하고 데이터를 모두 넣은 뒤 다시 만듭니다. 행마다 큰 GIN 인덱스를 갱신하는 비용을 피할 수 있습니다. 운영 증분은 들어오는 행이 적으므로 기존 인덱스를 유지한 채 추가하고 마지막에 통계 정보를 갱신합니다.

GIN 인덱스

법령 조문은 제목과 본문, 판례·해석례는 제목·요지·본문을 전문검색 대상으로 삼습니다. 문서 분류와 연도는 별도의 필터용 인덱스를 만듭니다.

CREATE INDEX idx_law_units_fts
ON law_units USING gin (heading, content);

CREATE INDEX idx_legal_documents_fts
ON legal_documents USING gin (title, summary, body);

CREATE INDEX idx_legal_documents_filters_fts
ON legal_documents USING gin (category_label, decision_year);

ANALYZE law_units;
ANALYZE legal_documents;

일반 B-tree 인덱스도 함께 둡니다. source_record_id, category_code, document_no, decision_date, canonical_unit_id, version_id, parent_unit_id는 정확 일치와 범위 필터에 사용합니다.

Bayesian BM25는 검색어 희귀도, 문서 안의 출현 빈도, 문서 길이를 반영한 BM25 신호를 확률 형태로 제공합니다. 제목, 요지, 본문의 신호를 fuse_log_odds로 결합하면 한 필드의 과도한 반복이 전체 순위를 지배하는 현상을 줄이고 서로 다른 필드에서 반복 확인된 문서를 우선할 수 있습니다.

SELECT
  id,
  document_no,
  title,
  summary,
  category_label,
  decision_date,
  _score
FROM legal_documents
WHERE fuse_log_odds(
  bayesian_match(title, $1),
  bayesian_match(summary, $1),
  bayesian_match(body, $1)
)
ORDER BY _score DESC
LIMIT 20;

법령 검색은 canonical ID와 버전 ID를 함께 반환합니다.

SELECT
  id,
  canonical_unit_id,
  instrument_id,
  version_id,
  heading,
  content,
  effective_date,
  _score
FROM law_units
WHERE is_deleted = 0
  AND fuse_log_odds(
    bayesian_match(heading, $1),
    bayesian_match(content, $1)
  )
ORDER BY _score DESC
LIMIT 20;

문서번호, 사건번호, 법령명, 조문번호처럼 표기가 명확한 질의는 정확 일치 또는 text_match 결과를 먼저 확인합니다. 자연어 쟁점 질의는 Bayesian BM25 결과를 기본 후보로 사용합니다. 여러 검색어에서 같은 문서가 반복 발견되면 query_hits를 올려 추가 근거로 사용합니다.

분류·연도 필터

검색 화면에서 판례만 보거나 2024년 이후 심판청구만 보려면 대형 본문 테이블에서 매번 분류 문자열을 검색하지 않습니다. 문서의 내부 _doc_id, 분류명, 결정연도를 가진 작은 파생 테이블을 만듭니다.

CREATE TABLE legal_document_facets AS
SELECT
  _doc_id AS document_doc_id,
  category_label,
  SUBSTRING(decision_date, 1, 4) AS decision_year
FROM legal_documents;

CREATE INDEX idx_legal_document_facets_tags
ON legal_document_facets USING gin (category_label, decision_year);

CREATE INDEX idx_legal_document_facets_doc
ON legal_document_facets (document_doc_id);

증분 문서가 들어오면 해당 문서의 _doc_id가 facet 테이블에 있는지 확인하고 없는 행만 추가합니다. 해석례와 판례·결정례를 서로 다른 탐색 목록으로 제공하려면 분류명에 따라 별도 browse 테이블에도 같은 참조를 추가합니다.

4. 인용·쟁점 그래프 구성

전문검색은 문장에 포함된 단어를 잘 찾고, 그래프는 직접 같은 단어가 없어도 연결 관계가 있는 자료를 찾습니다. 그래프 이름은 tax_knowledge로 정하고, SQL 테이블의 고정 식별자를 각 정점 속성에 저장합니다.

정점과 간선 모델

구분종류역할
정점document판례·해석례 문서
정점law, enforcement_decree, enforcement_rule법률·시행령·시행규칙
정점article, paragraph, item, subitem세부 조문 단위
정점issue_concept, tax_type쟁점과 세목
간선REFERENCES_LAW문서가 조문을 인용
간선REFERENCES_LAW_UNRESOLVED시점 또는 세부 단위를 추가 확인할 인용
간선ABOUT_ISSUE문서와 쟁점 연결
간선PART_OF조·항·호·목 계층 연결
간선NEXT같은 계층의 원문 순서
검색어 ──전문검색──▶ 판례 A ──REFERENCES_LAW──▶ 조문 1
                         │                         │
                         └──ABOUT_ISSUE──▶ 쟁점 X ◀──ABOUT_ISSUE── 판례 B

정점과 간선의 생성 근거는 먼저 SQL 테이블 authority_nodesgraph_edges에 저장합니다. 그래프를 직접 유일한 원본으로 삼으면 연결 규칙을 바꿀 때 검증과 재생성이 어렵다. 관계 테이블을 canonical 결과로 두고 UQA 그래프는 빠른 탐색용 파생 구조로 만듭니다.

CREATE TABLE authority_nodes (
  id TEXT PRIMARY KEY,
  node_type TEXT NOT NULL,
  source_system TEXT NOT NULL,
  canonical_key TEXT NOT NULL,
  canonical_label TEXT NOT NULL,
  display_label TEXT NOT NULL,
  raw_text TEXT,
  metadata_json TEXT
);

CREATE TABLE graph_edges (
  id TEXT PRIMARY KEY,
  from_node_id TEXT NOT NULL,
  to_node_id TEXT NOT NULL,
  edge_type TEXT NOT NULL,
  source_authority_id TEXT,
  confidence REAL NOT NULL,
  evidence_text TEXT,
  evidence_json TEXT
);

source_authority_id는 관계를 만든 원문 문서를 가리킵니다. evidence_text에는 인용 조문이나 쟁점의 원문 표현을 넣고, evidence_json에는 원천 배열의 구조화 값을 보존합니다. 검색 결과에 관계 근거를 설명할 때 사용합니다.

안정적인 64비트 ID

SQL 테이블의 ID는 읽기 쉬운 문자열입니다. UQA 그래프 내부에는 64비트 정수를 사용하므로 문자열 ID의 BLAKE2b 8바이트 해시를 정수로 변환합니다.

import hashlib

def stable_u64(value: str) -> int:
    digest = hashlib.blake2b(
        value.encode("utf-8"),
        digest_size=8,
    ).digest()
    return int.from_bytes(digest, "big")

전체 그래프 재생성 시에는 vertex_id → external_id 역방향 맵을 만들어 서로 다른 문자열이 같은 정수로 변환되는 해시 충돌을 검사합니다. 충돌이 발견되면 그래프 생성을 중단합니다. 외부 문자열 ID는 정점 속성 external_id에 그대로 저장합니다.

그래프 물질화

정점 튜플은 (vertex_id, label, properties), 간선 튜플은 (edge_id, source_id, target_id, label, properties) 형식으로 만듭니다.

graph = engine.create_graph("tax_knowledge")

vertices = [
    (
        stable_u64("nts:tribunal_request:12345"),
        "legal_document",
        {
            "external_id": "nts:tribunal_request:12345",
            "display_label": "조심2026서0000",
        },
    ),
    (
        stable_u64("law-unit:123456:article:38:0"),
        "law_article",
        {
            "external_id": "law-unit:123456:article:38:0",
            "display_label": "부가가치세법 제38조",
        },
    ),
]

edges = [
    (
        stable_u64("edge:document-12345-references-article-38"),
        stable_u64("nts:tribunal_request:12345"),
        stable_u64("law-unit:123456:article:38:0"),
        "REFERENCES_LAW",
        {
            "external_id": "edge:document-12345-references-article-38",
            "confidence": 1.0,
            "evidence_text": "부가가치세법 제38조",
        },
    )
]

engine.add_graph_batch(graph, vertices, edges)

초기 전체 물질화는 기본 10,000개 단위로 정점과 간선을 추가합니다. 간선을 넣기 전에 양 끝 정점이 모두 존재하는지 확인하고, 끝점이 없는 간선 수를 skipped_edges로 기록합니다.

.venv-uqa/bin/python scripts/materialize_uqa_graph.py \
  --db data/cpa_accounting_tax_knowledge.uqa.db \
  --graph tax_knowledge \
  --replace \
  --confirm-full-rebuild

증분 물질화는 이번 실행의 행 JSONL만 읽습니다. 들어오는 정점 ID를 배치로 조회해 이미 존재하는 정점은 건너뛰고, 신규 정점과 간선만 추가합니다.

.venv-uqa/bin/python scripts/materialize_uqa_graph.py \
  --db data/cpa_accounting_tax_knowledge.uqa.db \
  --graph tax_knowledge \
  --rows .tmp/tribunal-rows.jsonl

인용·쟁점 탐색

인용 탐색에는 두 방향이 모두 필요합니다.

# 판례가 인용한 조문
document --REFERENCES_LAW(out)--> law_unit

# 특정 조문을 인용한 판례
law_unit <--REFERENCES_LAW(in)-- document

질의를 법령 Bayesian BM25로 먼저 검색한 뒤 상위 canonical 조문에서 REFERENCES_LAW를 역방향으로 탐색하면 해당 조문을 실제로 인용한 문서를 찾을 수 있습니다. 문서 검색 결과에도 존재하는 후보를 우선 유지해 그래프 확장만으로 관련성이 낮은 문서가 상위에 오는 현상을 억제합니다.

같은 쟁점의 문서는 2-hop으로 탐색합니다.

상위 문서
  --ABOUT_ISSUE(out)--> 쟁점
  --ABOUT_ISSUE(in)--- 동일 쟁점 문서

그래프 이웃은 답변이 아니라 검토 후보입니다. REFERENCES_LAW는 조문이 문서에 등장한다는 사실을 의미하고, 그 조문이 최종 결론의 핵심 근거라는 사실까지 보장하지 않습니다. ABOUT_ISSUE도 같은 쟁점 분류를 의미하며 사실관계와 결론의 동일성을 의미하지 않습니다. 최종 후보는 반드시 legal_documents.body와 기준일 조문 원문으로 다시 확인합니다.

그래프 탐색에는 시작 정점, 간선 유형, 방향, 발견 후보 수, 텍스트 후보와 교차한 결과 수, 소요 시간을 남깁니다. 이 기록으로 인용 연결 누락과 과도한 그래프 확장을 구분할 수 있습니다.

5. 검색 API 구성

검색 요청은 계획, 전문검색, 그래프 보강, 근거 검토, 원문 복원의 닫힌 흐름으로 처리합니다. 각 단계의 입력과 결과를 기록하면 오답이나 지연이 발생했을 때 원인을 검색어, 필터, 그래프, 원문 복원 중 하나로 좁힐 수 있습니다.

사용자 질문
  → 질의 구조화와 검색 계획
  → 법령·문서 Bayesian BM25 검색
  → 인용·쟁점 그래프 후보 보강
  → 근거 충분성 검토
  → 필요한 경우 보강 검색 1회
  → 상위 결과 원문 복원
  → 근거와 검색 추적 반환

질의 계획

사용자 문장을 그대로 하나의 검색어로 사용하면 사실관계, 법적 요건, 예외가 섞입니다. 플래너는 검색 의도, 하위 쟁점, 동의어, 법령 범위, 문서 검색어, 법령 검색어를 구조화합니다.

{
  "intent": "특수관계인 저가 양도의 부당행위계산부인 요건 확인",
  "issues": [
    "특수관계인 해당 여부",
    "저가 양도 기준",
    "시가 산정 순서",
    "적용 제외"
  ],
  "synonyms": {
    "시가": ["정상가액", "거래가액", "감정가액"]
  },
  "law_scopes": ["법인세법", "법인세법 시행령"],
  "document_queries": [
    "특수관계인 저가 양도",
    "부당행위계산 시가",
    "감정가액 정상가액"
  ],
  "law_queries": [
    "부당행위계산 부인",
    "시가 산정",
    "특수관계인"
  ],
  "expand_references": true
}

검색어 하나는 1~4개의 핵심 법률 용어로 제한합니다. 정의, 성립요건, 금액 기준, 절차, 예외를 서로 다른 검색어로 분리합니다. 법령명이나 조문번호가 명시되면 해당 값을 구조화 필터에 넣고 텍스트 검색어에서는 중복을 줄입니다.

언어모델 호출이 실패하거나 JSON 계약을 지키지 않으면 규칙 기반 파서가 기본 계획을 만듭니다. 규칙 기반 파서는 사건번호, 연도 범위, 판례·심판청구 등 문서 유형, 기관, 법령 약칭, 제N조·제N항 표현, 제외 조건을 인식합니다.

입력: 2019년 이후 조심 사건 중 부가세법 제10조 관련 자료

문서 유형 = 심판청구
기관 = 조세심판원
기간 시작 = 2019-01-01
법령 = 부가가치세법
조문 = 제10조
검색 전략 = filtered_fts + graph expansion

텍스트·그래프 결합

판례·해석례와 법령은 별도 쿼리로 검색합니다. 같은 검색어를 두 테이블에 그대로 복사하지 않고 문서용 표현과 법령상 표현을 각각 사용합니다.

# 문서 후보
SELECT id, document_no, title, summary,
       category_label, decision_date, _score
FROM legal_documents
WHERE fuse_log_odds(
  bayesian_match(title, $1),
  bayesian_match(summary, $1),
  bayesian_match(body, $1)
)
ORDER BY _score DESC
LIMIT 30;

# 법령 후보
SELECT id, canonical_unit_id, instrument_id,
       version_id, heading, content, effective_date, _score
FROM law_units
WHERE is_deleted = 0
  AND fuse_log_odds(
    bayesian_match(heading, $1),
    bayesian_match(content, $1)
  )
ORDER BY _score DESC
LIMIT 20;

각 쿼리 결과는 문서 ID 또는 canonical 조문 ID로 합칩니다. 반복 발견 횟수, 최고 Bayesian BM25 점수, 검색어 목록을 함께 저장합니다.

candidate = {
  "id": document_id,
  "text_score": max(scores),
  "query_hits": number_of_distinct_queries,
  "matched_queries": matched_queries,
  "graph_hits": 0,
  "graph_paths": []
}

그래프 보강은 다음 순서로 제한합니다.

  1. 법령 검색 상위 canonical 조문에서 REFERENCES_LAW 역방향 이웃을 가져옵니다.
  2. 그래프 문서 후보와 이미 랭킹된 문서 전문검색 풀을 교차합니다.
  3. 문서 상위 일부에서 ABOUT_ISSUE 2-hop 이웃을 가져옵니다.
  4. 역시 전문검색 풀과 겹치는 문서를 우선합니다.
  5. 유지된 후보의 graph_hits와 경로 설명을 추가합니다.

최종 점수는 특정 상수 하나보다 서로 다른 증거가 같은 결과를 지지하는지에 초점을 둡니다.

final_score =
    text_score
  + query_hit_bonus
  + direct_citation_bonus
  + shared_issue_bonus
  + exact_identifier_bonus

그래프에서만 발견된 문서를 무조건 제외할 필요는 없지만 별도 후보군으로 표시합니다. 텍스트와 그래프가 모두 지지한 문서를 먼저 보여 주고, 그래프 단독 후보는 원문 검토를 통과한 경우에만 결과에 포함합니다.

근거 충분성 검토

첫 검색이 끝나면 질문, 검색 계획, 법령 후보, 문서 후보를 비교합니다. 검토기는 다음 항목을 확인합니다.

  • 질문의 세목과 납세의무자 유형에 맞는 법률이 포함됐는가
  • 법률의 기본 요건과 시행령의 계산·판정 기준이 함께 있는가
  • 단서, 제외, 우선순위, 경과규정이 빠지지 않았는가
  • 질문의 사실관계와 직접 맞닿은 판례·결정례가 있는가
  • 판례의 결정일과 적용 법령 시점이 맞는가
{
  "sufficient": false,
  "reason": "시가 산정 순서와 보충적 방법을 정한 시행령 근거가 부족함",
  "gaps": ["감정가액 우선순위", "보충적 평가방법"],
  "additional_law_scopes": ["법인세법 시행령"],
  "additional_document_queries": ["감정가액 시가"],
  "additional_law_queries": ["시가 산정 감정가액"]
}

sufficient=false이면 추가 검색어를 기존 계획에 합쳐 보강 검색을 한 번 실행합니다. 최대 보강 횟수를 고정해 질의가 무한히 확장되지 않게 합니다. 빠른 검색 프로필은 검색어 상위 4개, 법령·문서 후보 수, 그래프 이웃 수, 원문 확인 수를 제한합니다.

원문 복원

summary는 검색 후보를 좁히는 필드입니다. 최종 판단 근거는 body 또는 원본 JSONL에서 복원한 전체 본문이어야 합니다. 상위 문서의 raw_record_idmetadata_json을 이용해 원문 파일과 줄 위치를 찾습니다.

검색 후보 ID
  → legal_documents 조회
  → body가 충분하면 바로 사용
  → body가 없거나 축약됐으면 raw_record_id 조회
  → raw_records.raw_json 또는 $raw_blob 복원
  → SHA-256 검증
  → 사실관계·쟁점·판단·결론 구획 추출

법령은 검색된 항 한 줄만 반환하지 않습니다. 같은 instrument_id, version_id, article_no, article_branch_no를 가진 조문 단위를 unit_order로 정렬해 조 제목, 각 항, 호, 목을 완전한 계층으로 복원합니다. 단서와 예외가 다음 항이나 호에 있을 수 있기 때문입니다.

검색 추적 정보

각 검색 단계는 실행 목적, 대상, 쿼리, 후보 수, 유지 결과 수, 소요 시간을 기록합니다.

{
  "engine": "UQA Graph",
  "target": "REFERENCES_LAW · 역방향",
  "purpose": "검색 조문을 직접 인용한 판례·결정례 확장",
  "query": "top_laws <- REFERENCES_LAW(in) <- legal_documents",
  "candidate_count": 137,
  "result_count": 6,
  "elapsed_ms": 18.4
}

최종 API 응답에는 질문과 결과만 넣지 않고 계획, 기준일, 적용 필터, 일치 조문, 일치 쟁점, 그래프 경로, 원문 출처, 검색 추적을 함께 제공합니다.

{
  "query": "매입세액공제 사업관련성",
  "as_of_date": "2026-06-30",
  "plan": {
    "law_scopes": ["부가가치세법", "부가가치세법 시행령"],
    "issues": ["매입세액공제", "사업관련성"]
  },
  "results": [
    {
      "document_id": "nts:tribunal_request:12345",
      "document_no": "조심2026서0000",
      "title": "쟁점 거래의 과세 여부",
      "text_score": 0.91,
      "query_hits": 2,
      "matched_law_units": ["law-unit:123456:article:38:0"],
      "matched_issues": ["매입세액공제", "사업관련성"],
      "graph_paths": ["document → REFERENCES_LAW → article"],
      "full_text_source": "raw_records",
      "source_record_id": "12345"
    }
  ],
  "trace": ["planner", "document_fts", "law_fts", "graph", "hydrate"]
}

6. 증분 업데이트와 검증

운영 중 업데이트는 새 원문을 별도 작업 공간에서 수집하고 검증한 뒤 UQA에 배치로 반영합니다. 단일 작성 작업만 데이터베이스를 갱신하게 해 쓰기 충돌을 막고, 검색 서비스는 완료된 스냅샷을 계속 읽습니다.

신규 수집
  → 원본 해시 비교
  → 신규·변경 문서 정규화
  → 식별자와 계층 검증
  → UQA 배치 적재
  → GIN 인덱스 갱신
  → 그래프 정점·간선 추가
  → 건수와 표본 질의 검증
  → 검색 스냅샷 전환

수집과 적재 분리

새 원문을 먼저 확인하고 UQA 반영은 나중에 진행하려면 --collect-only를 사용합니다. 수집 원문, 후보 ID, 실행 저널은 남지만 UQA 테이블과 그래프는 변경되지 않습니다.

# 전체 원천 수집, UQA 쓰기 없음
.venv-uqa/bin/python scripts/run_tax_collection.py update \
  --scope all \
  --collect-only

# 국세법령정보시스템 판례·심판결정례·해석례만 수집
.venv-uqa/bin/python scripts/run_tax_collection.py update \
  --scope nts \
  --collect-only

수집 후에는 실행별 상세 파일과 매니페스트를 검사합니다. 신규 ID 수와 상세 원문 수가 일치하고 정규화 미리보기에서 필수 필드 누락이 없을 때 적재 단계로 넘어갑니다.

현행 법령 안전 교체

현행 법령에서 새 MST가 발견되면 새 버전을 먼저 추가합니다. 그 다음 하나의 트랜잭션에서 현재 플래그와 현행 검색용 조문을 조정합니다.

BEGIN;

-- 1. 새 MST가 적재되었는지 확인
SELECT id
FROM law_versions
WHERE instrument_id = $instrument_id
  AND mst = $new_mst;

-- 2. 같은 법령의 이전 현행 조문을 현행 검색 집합에서 제거
DELETE FROM law_units
WHERE instrument_id = $instrument_id
  AND version_id <> $new_version_id;

-- 3. 현행 플래그를 새 MST 하나로 맞춤
UPDATE law_versions
SET is_current = 0
WHERE instrument_id = $instrument_id;

UPDATE law_versions
SET is_current = 1
WHERE instrument_id = $instrument_id
  AND mst = $new_mst;

COMMIT;

트랜잭션 안에서 새 MST가 정확히 한 건인지, 현행 플래그가 정확히 한 버전에만 있는지 확인합니다. 어느 조건이든 실패하면 ROLLBACK합니다. 원본과 연혁 데이터베이스에는 이전 MST가 그대로 남으므로 과거 조문 조회가 유지됩니다.

.venv-uqa/bin/python scripts/update_current_tax_laws.py \
  --msts 123456,123457 \
  --work-dir .tmp/tax-data-pipeline/runs/20260901-120000

체크포인트 전진 조건

판례·해석례의 체크포인트는 목록 수집 직후에 바꾸지 않습니다. 아래 단계가 모두 완료되어야 다음 등록일과 문서 ID로 전진합니다.

  1. 체크포인트를 포함한 겹침 목록 스캔이 cutoff 이전까지 완료됩니다.
  2. 모든 신규 후보의 상세 원문이 저장됩니다.
  3. 정규화 JSONL이 생성되고 필수 필드 검증을 통과합니다.
  4. UQA 문서 행과 필터 행 적재가 완료됩니다.
  5. 신규 정점과 간선의 그래프 물질화가 완료됩니다.
  6. 신규 문서 수와 UQA 증가 건수가 허용 범위 안에서 일치합니다.

적재 중 실패하면 체크포인트는 이전 값에 머뭅니다. 다음 실행은 같은 후보를 다시 발견하지만 UQA 증분 적재기가 기존 ID를 건너뛰므로 이미 성공한 행을 중복 생성하지 않습니다.

시행 예정 개정 관리

공포 후 시행 전 개정은 현행 조문과 분리해 수집합니다. 실행별 폴더에는 공식 목록, 상세 원문, 개정 대상 조문, 개정문이 인용한 조문, 매니페스트를 저장합니다.

.venv-uqa/bin/python scripts/collect_nts_latest_law_amendments.py \
  --promulgated-after 20260813 \
  --law-names '개별소비세법 시행령,지방세법 시행령'

시행 예정 데이터는 scheduled 상태로 검색할 수 있지만 현행 기본 결과에는 포함하지 않습니다. 사용자가 미래 기준일을 선택하거나 신구대조 화면을 열었을 때만 함께 보여 줍니다. 시행일이 도래하면 새 MST를 현행으로 승격하고 이전 현행 버전의 종료 범위를 확정합니다.

중단·실패 복구

복구 절차는 실패 단계에 따라 다릅니다.

실패 위치남아 있는 자료재개 방법
목록 수집완료된 페이지 JSON완료 페이지를 재사용하고 다음 페이지부터 계속합니다.
상세 수집detail_raw*.jsonl의 완료 ID완료 ID를 제외한 후보만 다시 요청합니다.
정규화원본 JSONL과 오류 줄 번호파서를 수정한 뒤 정규화 파일만 다시 만듭니다.
UQA 적재행 JSONL과 기존 적재 ID--incremental로 기존 ID를 건너뛰고 재실행합니다.
그래프 적재관계 테이블과 행 JSONL기존 정점을 조회한 뒤 신규 정점·간선만 추가합니다.
현행 법령 교체트랜잭션 전 상태와 새 MST 원문ROLLBACK 후 같은 작업 디렉터리에서 다시 검증합니다.

실행 저널의 마지막 완료 단계와 단계별 로그가 재개 기준입니다. 잠금 보유 프로세스가 없고 이전 실행이 실패 상태라면 새 run ID로 재개하되, 이전 실행의 원문과 행 JSONL을 입력으로 명시합니다.

7. 품질·성능 검증

데이터 파이프라인의 성공은 명령 종료 코드만으로 판단하지 않습니다. 구조 무결성, 데이터 최신성, 검색 품질, 응답 시간을 각각 확인합니다.

검증 SQL

적재 직후에는 다음 SQL을 자동 실행합니다.

-- 중복된 원천 문서 ID
SELECT source_record_id, COUNT(*) AS count
FROM legal_documents
GROUP BY source_record_id
HAVING COUNT(*) > 1;

-- 부모 조문이 없는 하위 단위
SELECT child.id, child.parent_unit_id
FROM law_units child
LEFT JOIN law_units parent
  ON child.parent_unit_id = parent.canonical_unit_id
WHERE child.parent_unit_id IS NOT NULL
  AND parent.canonical_unit_id IS NULL;

-- 법령별 현행 버전 수
SELECT instrument_id, COUNT(*) AS current_count
FROM law_versions
WHERE is_current = 1
GROUP BY instrument_id
HAVING COUNT(*) <> 1;

-- 존재하지 않는 정점을 가리키는 간선
SELECT edge.id
FROM graph_edges edge
LEFT JOIN authority_nodes source
  ON edge.from_node_id = source.id
LEFT JOIN authority_nodes target
  ON edge.to_node_id = target.id
WHERE source.id IS NULL OR target.id IS NULL;

UQA의 SQL 문법과 조인 지원 범위에 맞춰 검증 쿼리를 조정할 수 있습니다. 핵심은 각 위반 건수가 0인지 확인하고, 0이 될 수 없는 원천 특성은 기준값과 변화량을 기록하는 것입니다.

최신성 검사는 공식 원천의 최신 목록과 로컬 수집본을 비교합니다.

.venv-uqa/bin/python scripts/check_data_freshness.py

# 네트워크 없이 로컬 수집일과 UQA 최신 결정일 재계산
.venv-uqa/bin/python scripts/check_data_freshness.py --offline

결과는 metadata/data-freshness.json에 최신 스냅샷으로, metadata/data-update-history.json에 실행별 이력으로 저장합니다. 공식 목록 건수 차이는 신규 문서 수와 같다고 단정하지 않습니다. 분류 변경, 삭제, 과거 자료 추가가 있을 수 있어 문서 ID 전체 대조가 필요합니다.

검색 품질 평가

대표 질의 세트에는 정확 식별자 질의, 법령 용어 질의, 사실관계 질의, 시점 질의, 예외 질의를 고르게 넣습니다.

질의 유형예시주요 평가 항목
문서번호조심2026서0000정확 결과 1위, 오탐 없음
조문번호부가가치세법 제38조법령명·조문 일치와 전체 계층 복원
쟁점매입세액공제 사업관련성Recall@K, NDCG, 관련 세목 비율
사실관계특수관계인에게 자산을 저가 양도요건·예외·관련 판례 포함 여부
시점2020년 2월 기준 감사 임기기준일 당시 조문 선택 정확도

정답 세트에는 관련 문서 ID, 관련 조문 canonical ID, 기준일, 반드시 포함해야 할 요건을 기록합니다. 검색 품질 지표는 후보 검색과 최종 근거를 나누어 봅니다.

  • Recall@K: 관련 문서가 상위 K개 후보 안에 포함되는 비율
  • NDCG@K: 더 직접적인 근거가 상위에 배치되는 정도
  • 조문 시점 정확도: 기준일에 유효한 버전을 선택한 비율
  • 인용 해결률: 관련 법령 배열이 canonical 조문에 연결된 비율
  • 근거 충실도: 답변 문장이 제공된 원문에서 확인되는 비율

대용량 성능 원칙

대화형 검색 속도를 유지하려면 모든 후보를 끝까지 확장하지 않습니다.

  • 플래너가 만든 검색어 수에 상한을 둡니다.
  • 쿼리별 법령·문서 후보 수를 제한합니다.
  • 그래프는 상위 조문과 상위 문서 일부에서만 확장합니다.
  • 그래프 후보를 이미 랭킹된 전문검색 풀과 메모리에서 교차합니다.
  • 전체 본문은 최종 상위 문서에 대해서만 복원합니다.
  • 동일 질의 계획과 필터 조합은 짧은 TTL 캐시로 재사용합니다.
예시 빠른 검색 한도
검색어 최대 4개
문서 후보 쿼리당 30건
법령 후보 쿼리당 20건
그래프 시작 정점 상위 5개
간선 유형별 이웃 최대 50건
전체 원문 복원 최대 4건
근거 보강 검색 최대 1회

성능 추적에는 계획 시간, SQL 검색 시간, 그래프 시간, 원문 복원 시간, 전체 시간을 각각 기록합니다. 전체 시간만 보면 병목이 모델 호출인지 UQA 쿼리인지 원문 파일 읽기인지 구분하기 어렵다.

완성 체크리스트

  • 수집 대상이 법률군과 자료 유형 카탈로그로 선언되어 있습니다.
  • 상태 확인과 Dry-run만으로 예정 명령과 쓰기 범위를 확인할 수 있습니다.
  • 공식 원문, SHA-256, 수집 이력, 실패 ID가 분리되어 보관됩니다.
  • 목록 체크포인트는 상세 원문·UQA·그래프 완료 후에만 전진합니다.
  • 세법령은 법령군·버전·조·항·호·목 구조로 정규화됩니다.
  • canonical 조문 ID와 특정 MST의 version ID가 구분됩니다.
  • 판례·해석례는 공통 문서 스키마와 고정 식별자를 가집니다.
  • 해결된 인용과 미해결 인용이 모두 근거와 함께 보존됩니다.
  • UQA 테이블에 구조화 필드와 전체 원문이 함께 저장됩니다.
  • 초기 대량 적재와 증분 적재 경로가 분리되어 있습니다.
  • GIN 인덱스와 Bayesian BM25 검색이 동작합니다.
  • 분류·연도 필터가 본문 검색과 독립적으로 동작합니다.
  • 문서·조문·쟁점 관계가 tax_knowledge 그래프에 연결됩니다.
  • 그래프의 외부 문자열 ID와 내부 64비트 ID가 안정적으로 매핑됩니다.
  • 검색 API가 계획, 기준일, 일치 조문, 그래프 경로, 원문 출처를 반환합니다.
  • 답변 전 상위 문서와 조문의 전체 원문을 복원합니다.
  • 증분 업데이트가 단일 writer, 트랜잭션, 단계별 검증을 거칩니다.
  • 대표 질의 세트로 검색 품질과 시점 정확도를 반복 측정합니다.

이 구조를 갖추면 새로운 세법령과 판례·해석례를 같은 파이프라인에 계속 추가할 수 있습니다. 검색 품질을 개선할 때도 원본 재수집 없이 정규화 규칙, 전문검색 가중치, 그래프 관계를 각각 조정할 수 있습니다.