feat(safety): B-2 KOSHA 사망사고 속보 수집기 (callApiId=1040)
data.go.kr 15119137 활용신청 전파 완료 → news_api02/getNews_api02 라이브.
collect_fatal_accidents: arno dedup(kosha-fatal|{arno}) + material_type=incident/
jurisdiction=KR + license=kogl. contents=HTML → _clean_html, published_date =
arno 접두 8자리(YYYYMMDD 등록일, 2019~ 라이브 전수 동형 검증). 첨부 API·business
필드 없는 별 채널(1040). run() 일일 잡(06:40 KST) 튜플 합류 — 소스별 실패 격리 유지.
순수 헬퍼 _fatal_fields + fixture 테스트(tests/test_kosha_fatal.py).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+1
@@ -0,0 +1 @@
|
||||
{"header": {"resultCode": "00", "resultMsg": "NORMAL_CODE"}, "body": {"pageNo": 1, "totalCount": 2845, "numOfRows": 3, "items": {"item": [{"contents": "<p><img src='https://portal.kosha.or.kr/api/compn24/auth/stdtboard/getImage.do?bbsId=B2025021314108&pstNo=20260611111536KIZXJ8&bbsAtcflNo=E0802000030001' style='width: 931px;' data-filename='6-9 부산 사상구.jpg' data-tboard-img-cvrt='Y'></p><p><br></p><p>2026. 6. 9. (화), 12:22경부산 사상구 소재 아파트에서</p><p><br></p><p>재해자가 2명이 실외기 설치 작업 중</p><p><br></p><p>베란다 난간이 파손되며 바닥으로 떨어짐</p><p><br></p><p>(사망 2명)</p><p>※ 위 내용은 신고 및 현재 파악된 내용으로 조사결과에 따라 변경될 수 있습니다.</p><div><br></div>", "keyword": "[6/9, 부산 사상구] 실외기 설치 작업 중 베란다 난간이 파손되어 떨어짐", "arno": "20260611111536KIZXJ8"}, {"contents": "<p><br><img src='https://portal.kosha.or.kr/api/compn24/auth/stdtboard/getImage.do?bbsId=B2025021314108&pstNo=20260611111355OZSS9T&bbsAtcflNo=E0802000030001' style='width: 931px;' data-filename='서 울관악구.jpg' data-tboard-img-cvrt='Y'></p><p><br></p><p>2026. 6. 9. (화), 17:26경서울 관악구 철도 공사 현장에서</p><p><br></p><p>재해자가 수직형 케이블 거치대 설치 준비 작업 중</p><p><br></p><p>개구부로 떨어짐(사망 1명)</p><p><br></p><p>※ 위 내용은 신고 및 현재 파악된 내용으로 조사결과에 따라 변경될 수 있습니다.</p><div><br></div><p></p>", "keyword": "[6/9, 서울 관악구] 수직형 케이블 거치대 설치 준비 중 개구부로 떨어짐", "arno": "20260611111355OZSS9T"}, {"contents": "<p><img src='https://portal.kosha.or.kr/api/compn24/auth/stdtboard/getImage.do?bbsId=B2025021314108&pstNo=202606111110595AR9QY&bbsAtcflNo=E0802000030001' style='width: 931px;' data-filename='5-14 전남 광양시.jpg' data-tboard-img-cvrt='Y'><br></p><p><br></p><p>2026. 5. 14. (목), 16:51경전남 광양시 소재 화학물질 제조사업장에서</p><p><br></p><p>재해자가 정제설비 내부에서 플랜지 해체 작업 중</p><p><br></p><p>고온 응축수가 쏟아져 화상을 입음(사망 1명)※ 위 내용은 신고 및 현재 파악된 내용으로 조사결과에 따라 변경될 수 있습니다.<br></p>", "keyword": "[5/14, 전남 광양시] 플랜지 해체 작업 중 고온 응축수가 쏟아져 화상", "arno": "202606111110595AR9QY"}]}}}
|
||||
@@ -0,0 +1,67 @@
|
||||
"""B-2 KOSHA 사망사고 속보(callApiId=1040) — 순수 파서 fixture 테스트 (plan safety-library-1).
|
||||
|
||||
fixture = 2026-06-13 data.go.kr 라이브 박제 (serviceKey 응답 본문 미포함 확인,
|
||||
tests/fixtures/kosha_fatal_response.json). _fatal_fields/_items 는 순수 함수라 DB/httpx
|
||||
호출 없이 검증 — [[feedback_external_api_fixture_first]].
|
||||
"""
|
||||
|
||||
import json
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
from workers.kosha_collector import _fatal_fields, _items
|
||||
|
||||
FIXTURE = Path(__file__).parent / "fixtures" / "kosha_fatal_response.json"
|
||||
|
||||
|
||||
def _payload() -> dict:
|
||||
return json.loads(FIXTURE.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def test_items_envelope_parse():
|
||||
"""body.items.item 봉투 파싱 — 재해사례와 동일 envelope."""
|
||||
items = _items(_payload())
|
||||
assert len(items) == 3
|
||||
assert all({"arno", "keyword", "contents"} <= set(it) for it in items)
|
||||
|
||||
|
||||
def test_fatal_fields_basic_mapping():
|
||||
item = _items(_payload())[0]
|
||||
f = _fatal_fields(item)
|
||||
assert f is not None
|
||||
assert f["arno"] == "20260611111536KIZXJ8"
|
||||
assert f["title"].startswith("[6/9, 부산 사상구]")
|
||||
# HTML 태그 + 이미지 서버 URL 노이즈 완전 제거 (검색/임베딩 본문 정화)
|
||||
assert "<" not in f["text"]
|
||||
assert "portal.kosha.or.kr" not in f["text"]
|
||||
assert "data-filename" not in f["text"]
|
||||
# 본문 텍스트는 보존
|
||||
assert "(사망 2명)" in f["text"]
|
||||
assert "베란다 난간" in f["text"]
|
||||
# published_date = arno 접두 8자리(KST 등록일), reg_dt = 14자리 등록시각 원문
|
||||
assert f["published_date"] == date(2026, 6, 11)
|
||||
assert f["reg_dt"] == "20260611111536"
|
||||
|
||||
|
||||
def test_fatal_fields_all_three_items_well_formed():
|
||||
for item in _items(_payload()):
|
||||
f = _fatal_fields(item)
|
||||
assert f is not None
|
||||
assert f["published_date"] == date(2026, 6, 11) # 3건 모두 06-11 등록
|
||||
assert f["reg_dt"] is not None
|
||||
assert f["text"] and "<" not in f["text"]
|
||||
|
||||
|
||||
def test_fatal_fields_skips_missing_required():
|
||||
assert _fatal_fields({"arno": "20260611111536XX", "contents": "x"}) is None # keyword 부재
|
||||
assert _fatal_fields({"keyword": "제목만", "contents": "x"}) is None # arno 부재
|
||||
assert _fatal_fields({"arno": " ", "keyword": " ", "contents": "x"}) is None # 공백뿐
|
||||
|
||||
|
||||
def test_fatal_fields_malformed_arno_date_is_fail_quiet():
|
||||
# arno 접두가 8자리 날짜로 안 풀리면 published_date/reg_dt = None (보조 축이라 fail-quiet)
|
||||
f = _fatal_fields({"arno": "ABC123", "keyword": "제목", "contents": "<p>본문</p>"})
|
||||
assert f is not None
|
||||
assert f["published_date"] is None
|
||||
assert f["reg_dt"] is None
|
||||
assert f["text"] == "본문"
|
||||
Reference in New Issue
Block a user