scvcoder commited on
Commit
c3ed3a0
·
verified ·
1 Parent(s): 98d8583

fix: 사례 URL을 camelCase(nttNo/nttId)로 교정 — 사례 카드 '원문 페이지 열기' 활성화

Browse files

privacy.go.kr/front/case/view.do GET endpoint은 camelCase 파라미터(nttNo, nttId)만 인식. snake_case(ntt_id, nttno)는 default 사례로 폴백되어 모든 사례 링크가 동일한 잘못된 페이지로 이동하던 문제 수정.

- data/cases.sqlite: 1,745건 detail_url snake_case → camelCase
- src/kpaa/retrieval/retriever.py: case Excerpt metadata에 absolute_url() 채움 (이전엔 빈 문자열)
- src/kpaa/cases/scraper.py: API URL_ADDR 무시하고 camelCase로 직접 구성 (재스크래이프 시 회귀 방지)
- src/kpaa/cases/models.py: detail_url 필드 주석 업데이트

효과: 참고한 자료 패널에서 사례 카드 하단에 법조문과 동일한 '원문 페이지 열기 ↗' 링크 노출.

data/cases.sqlite CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:a84162716f015395c4aa07d962d4e5441095840e2a7c19e07eff77a2822123aa
3
- size 14213120
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:63232b3922ec028677debdaf5588869ad077ed8560ad7af74d81294d3c4a59ee
3
+ size 14217216
src/kpaa/cases/models.py CHANGED
@@ -25,7 +25,7 @@ class Case(BaseModel):
25
  reg_dt: str # YYYYMMDD
26
  case_year: str # YYYY
27
  source_note: str # ETC_CN
28
- detail_url: str # /front/case/view.do?ntt_id=...&nttno=...
29
  chunk_context: str = "" # Anthropic Contextual Retrieval prefix — 인덱싱 시 body 앞에 prepend (답변 출력은 body만)
30
 
31
  def citation(self) -> str:
 
25
  reg_dt: str # YYYYMMDD
26
  case_year: str # YYYY
27
  source_note: str # ETC_CN
28
+ detail_url: str # /front/case/view.do?nttNo=...&nttId=... (camelCase 필수, snake_case는 default로 폴백)
29
  chunk_context: str = "" # Anthropic Contextual Retrieval prefix — 인덱싱 시 body 앞에 prepend (답변 출력은 body만)
30
 
31
  def citation(self) -> str:
src/kpaa/cases/scraper.py CHANGED
@@ -72,9 +72,17 @@ def _to_case(doc: dict[str, Any]) -> Case | None:
72
  body_raw = doc.get("NTT_CN_FULL") or doc.get("NTT_CN") or ""
73
  if not body_raw:
74
  return None
 
 
 
 
 
 
 
 
75
  return Case(
76
- ntt_no=str(doc.get("NTT_NO") or "").strip(),
77
- ntt_id=str(doc.get("NTT_ID") or "").strip(),
78
  title=str(doc.get("NTT_SJ") or "").strip(),
79
  summary=_decode_body(str(doc.get("NTT_CN") or "")),
80
  body=_decode_body(str(body_raw)),
@@ -86,7 +94,7 @@ def _to_case(doc: dict[str, Any]) -> Case | None:
86
  reg_dt=str(doc.get("REG_DT") or "").strip(),
87
  case_year=str(doc.get("CASE_YEAR") or "").strip(),
88
  source_note=str(doc.get("ETC_CN") or "").strip(),
89
- detail_url=str(doc.get("URL_ADDR") or "").strip(),
90
  )
91
 
92
 
 
72
  body_raw = doc.get("NTT_CN_FULL") or doc.get("NTT_CN") or ""
73
  if not body_raw:
74
  return None
75
+ ntt_no = str(doc.get("NTT_NO") or "").strip()
76
+ ntt_id = str(doc.get("NTT_ID") or "").strip()
77
+ # API의 URL_ADDR 필드는 snake_case(ntt_id, nttno)를 주는데
78
+ # 서버 view.do는 camelCase(nttId, nttNo)만 인식한다 (snake_case는 default 페이지로 폴백).
79
+ # 직접 camelCase로 구성한다.
80
+ detail_url = (
81
+ f"/front/case/view.do?nttNo={ntt_no}&nttId={ntt_id}" if ntt_id and ntt_no else ""
82
+ )
83
  return Case(
84
+ ntt_no=ntt_no,
85
+ ntt_id=ntt_id,
86
  title=str(doc.get("NTT_SJ") or "").strip(),
87
  summary=_decode_body(str(doc.get("NTT_CN") or "")),
88
  body=_decode_body(str(body_raw)),
 
94
  reg_dt=str(doc.get("REG_DT") or "").strip(),
95
  case_year=str(doc.get("CASE_YEAR") or "").strip(),
96
  source_note=str(doc.get("ETC_CN") or "").strip(),
97
+ detail_url=detail_url,
98
  )
99
 
100
 
src/kpaa/retrieval/retriever.py CHANGED
@@ -106,15 +106,11 @@ _PREC_BODY_LIMIT = 8000
106
 
107
 
108
  def _related_laws_excerpts() -> list[Excerpt]:
109
- """data/related_laws.yaml정적 리스트를 Excerpt로 변환.
110
 
111
  `관련_법령` intent 매칭 시 retriever가 라이브 검색 대신 이 리스트를 그대로
112
  컨텍스트에 박는다. 본 챗봇 scope는 개인정보보호법이지만, 사용자에게 *관련
113
  법령의 존재*를 안내해 적절한 곳에서 추가 확인하도록 돕는 용도.
114
-
115
- yaml 두 형식 모두 지원:
116
- - 레거시 list (수동 작성): name/short/description/url
117
- - 신규 dict (자동 갱신, privacy.go.kr 스크래이프): items/[name/kind/kind_label/url/source]
118
  """
119
  from kpaa.related_laws import load_items
120
 
@@ -324,10 +320,10 @@ async def _fetch_cases(
324
  "year": h.case_year,
325
  "type": h.type_label,
326
  "source_note": h.source_note,
327
- # privacy.go.kr view.do GET은 SPA 동작이ntt_id/nttno
328
- # deep-link를 무시하고 항상 동일 페이지를 반환(라 검증
329
- # 2026-04-29). 잘못된 사례로 동하지 않게 url 제거.
330
- "url": "",
331
  },
332
  sort_priority=0,
333
  recency_score=_recency_score(_yyyy_mmdd_to_year(h.case_year or h.reg_dt)),
@@ -786,7 +782,7 @@ async def _fetch_admin_rules(
786
  def _seed_three_tier() -> tuple[str, str | None, str | None]:
787
  """본법 + 시행령 + 시행규칙 MST 묶음 lookup.
788
 
789
- related_laws.yaml 에서 *개인정보 보호법* 패밀리만 골라낸다. KPAA 시점
790
  (2026-05) 개인정보보호법은 시행규칙이 없어 None 으로 떨어지는 게 정상.
791
 
792
  반환: (본법_mst, 시행령_mst | None, 시행규칙_mst | None)
@@ -799,7 +795,7 @@ def _seed_three_tier() -> tuple[str, str | None, str | None]:
799
 
800
  items = _rl.load_items()
801
  except Exception as e:
802
- logger.warning("_seed_three_tier related_laws.yaml 로드 실패: %s", e)
803
  return primary_mst, None, None
804
  for it in items:
805
  name = str(it.get("name", ""))
@@ -932,7 +928,7 @@ def _primary_law_id_lock() -> asyncio.Lock:
932
  async def _resolve_primary_law_id(client: KoreanLawClient) -> str | None:
933
  """본법의 *법령ID* 를 lazy resolve. 실패 시 None.
934
 
935
- KPAA 시점(2026-05) related_laws.yaml 에는 mst 만 저장되어 있어 lawId 는
936
  별도 lookup 필요. get_text 응답의 LawText.law_id 에서 회수 후 캐시.
937
 
938
  Resolve 전략 (순서대로 시도):
 
106
 
107
 
108
  def _related_laws_excerpts() -> list[Excerpt]:
109
+ """data/related_laws.jsonlportal_corpus 라인을 Excerpt 로 변환.
110
 
111
  `관련_법령` intent 매칭 시 retriever가 라이브 검색 대신 이 리스트를 그대로
112
  컨텍스트에 박는다. 본 챗봇 scope는 개인정보보호법이지만, 사용자에게 *관련
113
  법령의 존재*를 안내해 적절한 곳에서 추가 확인하도록 돕는 용도.
 
 
 
 
114
  """
115
  from kpaa.related_laws import load_items
116
 
 
320
  "year": h.case_year,
321
  "type": h.type_label,
322
  "source_note": h.source_note,
323
+ # privacy.go.kr view.do GET은 camelCase 미터(nttNo, nttId)만
324
+ # 인식 (snake_case는 default 사례로 폴백). detail_urlcamelCase
325
+ # 포맷이면 absolute_url()정상 deep-link를 반환.
326
+ "url": h.absolute_url() if h.detail_url else "",
327
  },
328
  sort_priority=0,
329
  recency_score=_recency_score(_yyyy_mmdd_to_year(h.case_year or h.reg_dt)),
 
782
  def _seed_three_tier() -> tuple[str, str | None, str | None]:
783
  """본법 + 시행령 + 시행규칙 MST 묶음 lookup.
784
 
785
+ related_laws.jsonl 에서 *개인정보 보호법* 패밀리만 골라낸다. KPAA 시점
786
  (2026-05) 개인정보보호법은 시행규칙이 없어 None 으로 떨어지는 게 정상.
787
 
788
  반환: (본법_mst, 시행령_mst | None, 시행규칙_mst | None)
 
795
 
796
  items = _rl.load_items()
797
  except Exception as e:
798
+ logger.warning("_seed_three_tier related_laws.jsonl 로드 실패: %s", e)
799
  return primary_mst, None, None
800
  for it in items:
801
  name = str(it.get("name", ""))
 
928
  async def _resolve_primary_law_id(client: KoreanLawClient) -> str | None:
929
  """본법의 *법령ID* 를 lazy resolve. 실패 시 None.
930
 
931
+ KPAA 시점(2026-05) related_laws.jsonl 에는 mst 만 저장되어 있어 lawId 는
932
  별도 lookup 필요. get_text 응답의 LawText.law_id 에서 회수 후 캐시.
933
 
934
  Resolve 전략 (순서대로 시도):