|
| 1 | +/** |
| 2 | + * 판례 검색범위 옵트인 (#167) — 제목검색이 1건만 맞아도 끝나서 법리 탐색 recall 이 낮던 문제. |
| 3 | + * 기본 동작은 그대로 두고 search="both"(판례명+본문)와 소량 적중 안내를 더한다. |
| 4 | + */ |
| 5 | +import { describe, expect, it, vi } from "vitest" |
| 6 | +import type { LawApiClient } from "../lib/api-client.js" |
| 7 | +import { searchPrecedentsStructured } from "./precedent-search-core.js" |
| 8 | +import { renderPrecedentSearchResult } from "./precedents.js" |
| 9 | + |
| 10 | +type Request = { extraParams: Record<string, string> } |
| 11 | + |
| 12 | +const item = (id: string, title: string) => |
| 13 | + `<prec><판례일련번호>${id}</판례일련번호><사건명>${title}</사건명>` + |
| 14 | + `<사건번호>2020다${id}</사건번호><선고일자>20240101</선고일자></prec>` |
| 15 | +const page = (total: number, ...items: string[]) => |
| 16 | + `<PrecSearch><totalCnt>${total}</totalCnt><page>1</page>${items.join("")}</PrecSearch>` |
| 17 | + |
| 18 | +// 실측(2026-10-05) 모양: "학원강사 근로자" 제목검색 1건, 본문검색 70건 |
| 19 | +const titleXml = page(1, item("1", "학원강사를 근로자수에 포함")) |
| 20 | +const bodyXml = page(70, item("1", "학원강사를 근로자수에 포함"), item("2", "퇴직금"), item("3", "임금")) |
| 21 | + |
| 22 | +function api() { |
| 23 | + const fetchApi = vi.fn(async (request: Request) => request.extraParams.search === "2" ? bodyXml : titleXml) |
| 24 | + return { fetchApi, client: { fetchApi } as unknown as LawApiClient } |
| 25 | +} |
| 26 | + |
| 27 | +describe('search="both": 판례명+본문 합침', () => { |
| 28 | + it("두 검색을 모두 돌려 판례ID로 중복을 빼고 제목 적중을 앞에 둔다", async () => { |
| 29 | + const { fetchApi, client } = api() |
| 30 | + const result = await searchPrecedentsStructured(client, { query: "학원강사 근로자", search: "both", display: 20, page: 1 }) |
| 31 | + expect(fetchApi).toHaveBeenCalledTimes(2) |
| 32 | + expect(result.hits.map(hit => [hit.id, hit.searchMode])).toEqual([["1", 1], ["2", 2], ["3", 2]]) |
| 33 | + expect(result.fallbackUsed).toBe(false) |
| 34 | + }) |
| 35 | + |
| 36 | + it("렌더가 범위별 건수와 건마다 어느 검색에서 맞았는지 보인다", async () => { |
| 37 | + const text = renderPrecedentSearchResult( |
| 38 | + await searchPrecedentsStructured(api().client, { query: "학원강사 근로자", search: "both", display: 20, page: 1 }) |
| 39 | + ) |
| 40 | + expect(text).toContain("제목검색 1건 · 본문검색 70건") |
| 41 | + expect(text).toMatch(/\[1\] 학원강사를 근로자수에 포함\n(?:.*\n)*? 적중: 제목검색/) |
| 42 | + expect(text).toMatch(/\[2\] 퇴직금\n(?:.*\n)*? 적중: 본문검색/) |
| 43 | + }) |
| 44 | + |
| 45 | + it("사건번호만 넣으면 both 여도 exact 조회 한 번으로 끝난다", async () => { |
| 46 | + const { fetchApi, client } = api() |
| 47 | + await searchPrecedentsStructured(client, { query: "2020다1", search: "both", display: 20, page: 1 }) |
| 48 | + expect(fetchApi).toHaveBeenCalledTimes(1) |
| 49 | + expect(fetchApi.mock.calls[0][0].extraParams.nb).toBe("2020다1") |
| 50 | + }) |
| 51 | +}) |
| 52 | + |
| 53 | +describe("search 문자열 입력 (search_decisions options 는 스키마 검증 없이 넘어온다)", () => { |
| 54 | + it('"2" 는 본문검색으로 보낸다', async () => { |
| 55 | + const { fetchApi, client } = api() |
| 56 | + await searchPrecedentsStructured(client, { query: "학원강사 근로자", search: "2" as never, display: 20, page: 1 }) |
| 57 | + expect(fetchApi.mock.calls[0][0].extraParams.search).toBe("2") |
| 58 | + }) |
| 59 | +}) |
| 60 | + |
| 61 | +describe("제목검색 소량 적중 안내", () => { |
| 62 | + const hint = /본문검색도 보세요/ |
| 63 | + |
| 64 | + it("제목검색이 3건 이하로 끝나면 search 옵션을 안내한다", async () => { |
| 65 | + const text = renderPrecedentSearchResult( |
| 66 | + await searchPrecedentsStructured(api().client, { query: "학원강사 근로자", display: 20, page: 1 }) |
| 67 | + ) |
| 68 | + expect(text).toMatch(hint) |
| 69 | + expect(text).toContain('options={search:"both"}') |
| 70 | + }) |
| 71 | + |
| 72 | + it("제목검색 적중이 많거나 both·본문검색이면 안내하지 않는다", async () => { |
| 73 | + const many = { fetchApi: async () => page(4, item("1", "가"), item("2", "나"), item("3", "다"), item("4", "라")) } as unknown as LawApiClient |
| 74 | + for (const [client, search] of [[many, undefined], [api().client, "both"], [api().client, 2]] as const) { |
| 75 | + const text = renderPrecedentSearchResult( |
| 76 | + await searchPrecedentsStructured(client, { query: "학원강사 근로자", search, display: 20, page: 1 }) |
| 77 | + ) |
| 78 | + expect(text).not.toMatch(hint) |
| 79 | + } |
| 80 | + }) |
| 81 | +}) |
0 commit comments