mirror of
https://github.com/NomaDamas/k-skill.git
synced 2026-06-24 02:04:11 +00:00
Align court auction lookup with monthly site search
The court auction notice page posts a YYYYMM search key from its 조회 button and returns a month of rows. Keep day inputs as a compatibility filter over the monthly response and normalize the current nested detail payload shape. Constraint: courtauction.go.kr has no public API and blocks bursty automated calls. Rejected: querying every day independently | the upstream search surface is month-based and day calls return false empty results. Confidence: high Scope-risk: narrow Directive: Preserve the site-observed YYYYMM notice search contract unless the PGJ143M01 XHR changes again. Tested: npm --workspace packages/court-auction-notice-search test; npm run ci; live 서울중앙지방법원 2026-05 notice/detail smoke lookup. Not-tested: PR CI after push. Co-authored-by: OmX <omx@oh-my-codex.dev>
This commit is contained in:
parent
dff4d1c323
commit
dd2172b520
9 changed files with 130 additions and 34 deletions
5
.changeset/fix-court-auction-month-search.md
Normal file
5
.changeset/fix-court-auction-month-search.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"court-auction-notice-search": patch
|
||||
---
|
||||
|
||||
Fix sale notice search to post the court site month key (`YYYYMM`) and filter exact-day requests locally; normalize the current nested notice-detail response shape and HTML-formatted prices.
|
||||
|
|
@ -39,7 +39,7 @@ metadata:
|
|||
|
||||
## Inputs
|
||||
|
||||
- `date` — 매각기일 (YYYY-MM-DD 또는 YYYYMMDD). 필수.
|
||||
- `date` — 매각기일 월(YYYY-MM 또는 YYYYMM) 또는 특정일(YYYY-MM-DD 또는 YYYYMMDD). 필수. 실제 사이트 검색 버튼은 월(YYYYMM) 단위로 조회하므로 특정일 입력은 월 조회 후 해당 일자만 필터링한다.
|
||||
- `courtCode` — 법원사무소코드 (예: `B000210` = 서울중앙지방법원). 비우면 전체. `getCourtCodes()` 또는 `codes courts` 로 받아온다.
|
||||
- `bidType` — `date` (= 기일입찰, code 000331) 또는 `period` (= 기간입찰, code 000332). 빈값이면 둘 다.
|
||||
- `caseNumber` — 사건번호. `2024타경100001` 형식 권장. `2024-100001` 도 받아서 `2024타경100001` 로 정규화한다.
|
||||
|
|
@ -147,7 +147,7 @@ court-auction-notice-search codes courts --pretty | head -40
|
|||
court-auction-notice-search codes bid-types --pretty
|
||||
|
||||
# 3. 매각공고 목록
|
||||
court-auction-notice-search notices --date 2026-04-27 --court-code B000210 --bid-type date --pretty
|
||||
court-auction-notice-search notices --date 2026-04 --court-code B000210 --bid-type date --pretty
|
||||
|
||||
# 4. 매각공고 상세 — list 응답의 row 의 raw 필드를 그대로 detail 호출에 사용한다.
|
||||
# (CLI 단발 호출에서는 list -> detail 으로 결과를 파이프할 수 있도록 jq 등을 함께 사용)
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
court-auction-notice-search -h
|
||||
court-auction-notice-search codes courts --pretty | head -40
|
||||
court-auction-notice-search codes bid-types --pretty
|
||||
court-auction-notice-search notices --date 2026-04-27 --court-code B000210 --bid-type date --pretty
|
||||
court-auction-notice-search notices --date 2026-04 --court-code B000210 --bid-type date --pretty
|
||||
court-auction-notice-search case --court-code B000210 --case-number "2024타경100001" --pretty
|
||||
```
|
||||
|
||||
|
|
@ -50,7 +50,7 @@ const {
|
|||
} = require("court-auction-notice-search");
|
||||
|
||||
const notices = await searchSaleNotices({
|
||||
date: "2026-04-27",
|
||||
date: "2026-04", // 월 전체 조회. 일자 입력은 같은 월 조회 후 해당일만 필터링
|
||||
courtCode: "B000210",
|
||||
bidType: "date"
|
||||
});
|
||||
|
|
@ -73,7 +73,7 @@ const caseInfo = await getCaseByCaseNumber({
|
|||
|
||||
| 목적 | 메소드 + 경로 | request body |
|
||||
| --- | --- | --- |
|
||||
| 매각공고 목록 | `POST /pgj/pgj143/selectRletDspslPbanc.on` | `{"dma_srchDspslPbanc":{"srchYmd","cortOfcCd","bidDvsCd","srchBtnYn":"Y"}}` |
|
||||
| 매각공고 목록 | `POST /pgj/pgj143/selectRletDspslPbanc.on` | `{"dma_srchDspslPbanc":{"srchYmd","cortOfcCd","bidDvsCd","srchBtnYn":"Y"}}` (`srchYmd`는 사이트 검색 버튼과 동일하게 `YYYYMM`) |
|
||||
| 매각공고 상세 | `POST /pgj/pgj143/selectRletDspslPbancDtl.on` | `{"dma_srchGnrlPbanc":{"cortOfcCd","dspslDxdyYmd","jdbnCd",...}}` |
|
||||
| 사건 단건 | `POST /pgj/pgj15A/selectAuctnCsSrchRslt.on` | `{"dma_srchCsDtlInf":{"cortOfcCd","csNo"}}` |
|
||||
| 법원사무소 코드 | `POST /pgj/pgjComm/selectCortOfcCdLst.on` | `{}` |
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ console.log(courts.items.find((c) => c.name === "서울중앙지방법원"));
|
|||
// { code: "B000210", name: "서울중앙지방법원", branchName: "서울중앙지방법원" }
|
||||
|
||||
const notices = await searchSaleNotices({
|
||||
date: "2026-04-27",
|
||||
date: "2026-04", // 월 전체 조회. "2026-04-27"처럼 일자를 주면 같은 월을 조회한 뒤 해당 일자만 필터링
|
||||
courtCode: "B000210",
|
||||
bidType: "date" // "기일입찰" / "000331" 도 모두 받음
|
||||
});
|
||||
|
|
@ -76,14 +76,14 @@ if (caseInfo.found) {
|
|||
court-auction-notice-search -h
|
||||
court-auction-notice-search codes courts --pretty
|
||||
court-auction-notice-search codes bid-types --pretty
|
||||
court-auction-notice-search notices --date 2026-04-27 --court-code B000210 --bid-type date --pretty
|
||||
court-auction-notice-search notices --date 2026-04 --court-code B000210 --bid-type date --pretty
|
||||
court-auction-notice-search case --court-code B000210 --case-number "2024타경100001" --pretty
|
||||
```
|
||||
|
||||
## Public API
|
||||
|
||||
- `searchSaleNotices({ date, courtCode?, bidType?, includeRaw?, client? })`
|
||||
- `date`: `"YYYY-MM-DD"` 또는 `"YYYYMMDD"` (필수)
|
||||
- `date`: `"YYYY-MM"`/`"YYYYMM"` 또는 `"YYYY-MM-DD"`/`"YYYYMMDD"` (필수). 실제 사이트 검색 버튼은 월(`YYYYMM`) 단위로 조회하므로, 일자를 주면 같은 월을 조회한 뒤 해당 매각기일만 필터링한다
|
||||
- `courtCode`: `"B000210"` 형식 또는 `""`(전체)
|
||||
- `bidType`: `"date"` / `"period"` / `"기일입찰"` / `"기간입찰"` / `"000331"` / `"000332"` / `""`
|
||||
- returns `{ requestedDate, requestedCourtCode, requestedBidType, count, items[] }`
|
||||
|
|
@ -133,7 +133,7 @@ discovery 시 직접 캡처한 사이트 내부 endpoint:
|
|||
|
||||
| 목적 | 메소드 + 경로 | request body 핵심 키 |
|
||||
| --- | --- | --- |
|
||||
| 매각공고 목록 | `POST /pgj/pgj143/selectRletDspslPbanc.on` | `dma_srchDspslPbanc.{srchYmd, cortOfcCd, bidDvsCd, srchBtnYn:"Y"}` |
|
||||
| 매각공고 목록 | `POST /pgj/pgj143/selectRletDspslPbanc.on` | `dma_srchDspslPbanc.{srchYmd, cortOfcCd, bidDvsCd, srchBtnYn:"Y"}` — `srchYmd`는 사이트 검색 버튼과 동일하게 `YYYYMM` 월 단위 |
|
||||
| 매각공고 상세 | `POST /pgj/pgj143/selectRletDspslPbancDtl.on` | `dma_srchGnrlPbanc.{cortOfcCd, dspslDxdyYmd, jdbnCd, ...}` |
|
||||
| 사건 단건 | `POST /pgj/pgj15A/selectAuctnCsSrchRslt.on` | `dma_srchCsDtlInf.{cortOfcCd, csNo}` |
|
||||
| 법원사무소 | `POST /pgj/pgjComm/selectCortOfcCdLst.on` | `{}` |
|
||||
|
|
|
|||
|
|
@ -36,6 +36,31 @@ function toYmd(input, label) {
|
|||
return compact;
|
||||
}
|
||||
|
||||
function toNoticeSearchDate(input, label) {
|
||||
if (input === null || input === undefined || input === "") {
|
||||
throw new Error(`${label} is required (YYYY-MM, YYYYMM, YYYY-MM-DD, or YYYYMMDD)`);
|
||||
}
|
||||
|
||||
const value = String(input).trim();
|
||||
const compact = value.replace(/[^0-9]/g, "");
|
||||
if (/^\d{6}$/.test(compact)) {
|
||||
return { queryYmd: compact, exactYmd: null };
|
||||
}
|
||||
if (/^\d{8}$/.test(compact)) {
|
||||
return { queryYmd: compact.slice(0, 6), exactYmd: compact };
|
||||
}
|
||||
|
||||
throw new Error(`${label} must be YYYY-MM, YYYYMM, YYYY-MM-DD or YYYYMMDD, got "${input}"`);
|
||||
}
|
||||
|
||||
function formatCompactMonth(value) {
|
||||
return `${value.slice(0, 4)}-${value.slice(4, 6)}`;
|
||||
}
|
||||
|
||||
function formatCompactDate(value) {
|
||||
return `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6, 8)}`;
|
||||
}
|
||||
|
||||
function normalizeCaseNumber(input) {
|
||||
if (input === null || input === undefined) {
|
||||
throw new Error("caseNumber is required (e.g. 2024타경100001)");
|
||||
|
|
@ -88,7 +113,7 @@ function ensureClient(client, options) {
|
|||
}
|
||||
|
||||
async function searchSaleNotices(params = {}) {
|
||||
const date = toYmd(params.date, "date");
|
||||
const searchDate = toNoticeSearchDate(params.date, "date");
|
||||
const courtCodeRaw =
|
||||
params.courtCode === undefined || params.courtCode === null ? "" : String(params.courtCode).trim();
|
||||
const courtCode = courtCodeRaw === "" ? "" : ensureCourtCode(courtCodeRaw);
|
||||
|
|
@ -97,7 +122,9 @@ async function searchSaleNotices(params = {}) {
|
|||
const client = ensureClient(params.client, params);
|
||||
const body = {
|
||||
dma_srchDspslPbanc: {
|
||||
srchYmd: date,
|
||||
// The PGJ143M01 "검색" button posts a month key (YYYYMM), not a day key.
|
||||
// Day-level API compatibility is preserved by filtering the returned month rows below.
|
||||
srchYmd: searchDate.queryYmd,
|
||||
cortOfcCd: courtCode,
|
||||
bidDvsCd: bidTypeCode,
|
||||
srchBtnYn: "Y"
|
||||
|
|
@ -105,14 +132,27 @@ async function searchSaleNotices(params = {}) {
|
|||
};
|
||||
|
||||
const raw = await client.postJson("notices", body);
|
||||
return normalizeNoticeListResponse(raw, {
|
||||
requestedDate: `${date.slice(0, 4)}-${date.slice(4, 6)}-${date.slice(6, 8)}`,
|
||||
const normalized = normalizeNoticeListResponse(raw, {
|
||||
requestedDate: searchDate.exactYmd
|
||||
? formatCompactDate(searchDate.exactYmd)
|
||||
: formatCompactMonth(searchDate.queryYmd),
|
||||
requestedMonth: formatCompactMonth(searchDate.queryYmd),
|
||||
requestedCourtCode: courtCode || null,
|
||||
requestedBidType: bidTypeCode
|
||||
? { code: bidTypeCode, name: describeBidTypeCode(bidTypeCode) }
|
||||
: null,
|
||||
includeRaw: params.includeRaw !== false
|
||||
});
|
||||
|
||||
if (searchDate.exactYmd) {
|
||||
normalized.items = normalized.items.filter((item) => {
|
||||
const rawYmd = item.raw && item.raw.dspslDxdyYmd ? String(item.raw.dspslDxdyYmd) : "";
|
||||
return rawYmd === searchDate.exactYmd;
|
||||
});
|
||||
normalized.count = normalized.items.length;
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function pickNoticeKeys(notice) {
|
||||
|
|
|
|||
|
|
@ -8,13 +8,29 @@ function nullIfBlank(value) {
|
|||
return trimmed === "" ? null : trimmed;
|
||||
}
|
||||
|
||||
function stripHtml(value) {
|
||||
if (value === null || value === undefined) return null;
|
||||
const text = String(value)
|
||||
.replace(/<img\b[^>]*>/gi, " ")
|
||||
.replace(/<br\s*\/?\s*>/gi, " ")
|
||||
.replace(/<[^>]+>/g, " ")
|
||||
.replace(/ /gi, " ")
|
||||
.replace(/&/gi, "&")
|
||||
.replace(/</gi, "<")
|
||||
.replace(/>/gi, ">")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
return text === "" ? null : text;
|
||||
}
|
||||
|
||||
function parseAmount(value) {
|
||||
if (value === null || value === undefined) return null;
|
||||
const trimmed = String(value).trim();
|
||||
if (trimmed === "") return null;
|
||||
const cleaned = trimmed.replace(/[, ]/g, "").replace(/원$/, "");
|
||||
if (cleaned === "" || cleaned === "-") return null;
|
||||
const num = Number(cleaned);
|
||||
const stripped = stripHtml(value);
|
||||
if (!stripped) return null;
|
||||
if (stripped === "-") return null;
|
||||
const amountMatch = stripped.match(/\d{1,3}(?:,\d{3})+|\d+/);
|
||||
if (!amountMatch) return null;
|
||||
const num = Number(amountMatch[0].replace(/[, ]/g, ""));
|
||||
return Number.isFinite(num) ? num : null;
|
||||
}
|
||||
|
||||
|
|
@ -49,6 +65,7 @@ function normalizeNoticeListResponse(rawPayload, options = {}) {
|
|||
|
||||
return {
|
||||
requestedDate: options.requestedDate || null,
|
||||
requestedMonth: options.requestedMonth || null,
|
||||
requestedCourtCode: options.requestedCourtCode || null,
|
||||
requestedBidType: options.requestedBidType || null,
|
||||
count: items.length,
|
||||
|
|
@ -98,9 +115,23 @@ function collectSaleTimes(row) {
|
|||
|
||||
function normalizeNoticeDetailResponse(rawPayload, options = {}) {
|
||||
const data = rawPayload && typeof rawPayload === "object" ? rawPayload.data : null;
|
||||
const meta = data && typeof data.dma_srchGnrlPbanc === "object" ? data.dma_srchGnrlPbanc : {};
|
||||
const resultData = data && typeof data.result === "object" ? data.result : null;
|
||||
const nestedInput = resultData && typeof resultData.inputData === "object" ? resultData.inputData : null;
|
||||
const meta =
|
||||
nestedInput ||
|
||||
(data && typeof data.dma_srchGnrlPbanc === "object" ? data.dma_srchGnrlPbanc : {});
|
||||
const nestedPbanc =
|
||||
resultData &&
|
||||
resultData.dspslPbanc &&
|
||||
typeof resultData.dspslPbanc.pbancInfo === "object"
|
||||
? resultData.dspslPbanc.pbancInfo
|
||||
: null;
|
||||
const list =
|
||||
data && Array.isArray(data.dlt_gnrlPbancLst) ? data.dlt_gnrlPbancLst : [];
|
||||
nestedPbanc && Array.isArray(nestedPbanc.lst)
|
||||
? nestedPbanc.lst
|
||||
: data && Array.isArray(data.dlt_gnrlPbancLst)
|
||||
? data.dlt_gnrlPbancLst
|
||||
: [];
|
||||
|
||||
const includeRaw = options.includeRaw !== false;
|
||||
|
||||
|
|
@ -110,7 +141,7 @@ function normalizeNoticeDetailResponse(rawPayload, options = {}) {
|
|||
bidStartDate: formatYmd(meta.bidBgngYmd),
|
||||
bidEndDate: formatYmd(meta.bidEndYmd),
|
||||
judgeDeptCode: nullIfBlank(meta.jdbnCd),
|
||||
judgeDeptName: nullIfBlank(meta.cortAuctnJdbnNm),
|
||||
judgeDeptName: nullIfBlank(meta.cortAuctnJdbnNm) || nullIfBlank(nestedPbanc && nestedPbanc.chargDept),
|
||||
judgeDeptPhone: nullIfBlank(meta.jdbnTelno),
|
||||
salePlace: nullIfBlank(meta.dspslPlcNm),
|
||||
saleTimes: collectSaleTimes(meta),
|
||||
|
|
@ -126,7 +157,9 @@ function normalizeNoticeDetailResponse(rawPayload, options = {}) {
|
|||
items
|
||||
};
|
||||
if (includeRaw) {
|
||||
result.raw = { dma_srchGnrlPbanc: { ...meta } };
|
||||
result.raw = nestedPbanc
|
||||
? { inputData: { ...meta }, pbancInfo: { ...nestedPbanc } }
|
||||
: { dma_srchGnrlPbanc: { ...meta } };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
@ -134,13 +167,13 @@ function normalizeNoticeDetailResponse(rawPayload, options = {}) {
|
|||
function normalizeNoticeDetailRow(rawRow, includeRaw) {
|
||||
const row = ensureRow(rawRow);
|
||||
const out = {
|
||||
caseNumber: nullIfBlank(row.csNo),
|
||||
caseNumber: stripHtml(row.csNo),
|
||||
itemSeq: nullIfBlank(row.dspslSeq),
|
||||
usage: nullIfBlank(row.usgNm),
|
||||
address: nullIfBlank(row.st),
|
||||
usage: stripHtml(row.usgNm),
|
||||
address: stripHtml(row.st),
|
||||
appraisedPrice: parseAmount(row.aeeEvlAmt),
|
||||
minimumSalePrice: parseAmount(row.lwsDspslPrc),
|
||||
remarks: nullIfBlank(row.dspslRmk)
|
||||
remarks: stripHtml(row.dspslRmk)
|
||||
};
|
||||
if (includeRaw) {
|
||||
out.raw = { ...row };
|
||||
|
|
@ -319,6 +352,7 @@ module.exports = {
|
|||
normalizeCourtCodesResponse,
|
||||
normalizeCaseDetailResponse,
|
||||
parseAmount,
|
||||
stripHtml,
|
||||
formatYmd,
|
||||
formatHm
|
||||
};
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ test("CLI rejects --date with an obviously invalid format", () => {
|
|||
{ encoding: "utf8" }
|
||||
);
|
||||
assert.notEqual(result.status, 0);
|
||||
assert.match(result.stderr, /must be YYYY-MM-DD or YYYYMMDD/);
|
||||
assert.match(result.stderr, /must be YYYY-MM, YYYYMM, YYYY-MM-DD or YYYYMMDD/);
|
||||
});
|
||||
|
||||
test("CLI prints usage and exits non-zero on unknown command", () => {
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ test("describeBidTypeCode returns the Korean name", () => {
|
|||
assert.equal(describeBidTypeCode(""), "");
|
||||
});
|
||||
|
||||
test("searchSaleNotices builds the expected request body and normalizes the response", async () => {
|
||||
test("searchSaleNotices posts the month key used by the site search button and normalizes the response", async () => {
|
||||
const client = makeFakeClient((endpoint) => {
|
||||
assert.equal(endpoint, "notices");
|
||||
return loadFixture("notices-sample.json");
|
||||
|
|
@ -83,7 +83,7 @@ test("searchSaleNotices builds the expected request body and normalizes the resp
|
|||
assert.equal(client.calls.length, 1);
|
||||
assert.deepEqual(client.calls[0].body, {
|
||||
dma_srchDspslPbanc: {
|
||||
srchYmd: "20260427",
|
||||
srchYmd: "202604",
|
||||
cortOfcCd: "B000210",
|
||||
bidDvsCd: "000331",
|
||||
srchBtnYn: "Y"
|
||||
|
|
@ -92,20 +92,33 @@ test("searchSaleNotices builds the expected request body and normalizes the resp
|
|||
|
||||
assert.equal(result.count, 2);
|
||||
assert.equal(result.requestedDate, "2026-04-27");
|
||||
assert.equal(result.requestedMonth, "2026-04");
|
||||
assert.equal(result.requestedCourtCode, "B000210");
|
||||
assert.deepEqual(result.requestedBidType, { code: "000331", name: "기일입찰" });
|
||||
assert.equal(result.items[0].caseNumber, undefined);
|
||||
assert.equal(result.items[0].noticeId, "REAL_ID_2026042701");
|
||||
});
|
||||
|
||||
test("searchSaleNotices accepts compact YYYYMMDD dates and rejects garbage", async () => {
|
||||
const client = makeFakeClient(() => loadFixture("notices-empty.json"));
|
||||
await searchSaleNotices({ date: "20260427", client });
|
||||
assert.equal(client.calls[0].body.dma_srchDspslPbanc.srchYmd, "20260427");
|
||||
test("searchSaleNotices accepts compact dates/months and filters exact day requests", async () => {
|
||||
const client = makeFakeClient(() => loadFixture("notices-sample.json"));
|
||||
|
||||
const exactDay = await searchSaleNotices({ date: "20260427", client });
|
||||
assert.equal(client.calls[0].body.dma_srchDspslPbanc.srchYmd, "202604");
|
||||
assert.equal(exactDay.count, 2);
|
||||
|
||||
const emptyDay = await searchSaleNotices({ date: "2026-04-28", client });
|
||||
assert.equal(client.calls[1].body.dma_srchDspslPbanc.srchYmd, "202604");
|
||||
assert.equal(emptyDay.count, 0);
|
||||
|
||||
const month = await searchSaleNotices({ date: "2026-04", client });
|
||||
assert.equal(client.calls[2].body.dma_srchDspslPbanc.srchYmd, "202604");
|
||||
assert.equal(month.requestedDate, "2026-04");
|
||||
assert.equal(month.requestedMonth, "2026-04");
|
||||
assert.equal(month.count, 2);
|
||||
|
||||
await assert.rejects(
|
||||
() => searchSaleNotices({ date: "not-a-date", client }),
|
||||
/must be YYYY-MM-DD or YYYYMMDD/
|
||||
/must be YYYY-MM, YYYYMM, YYYY-MM-DD or YYYYMMDD/
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ test("parseAmount strips commas/won/spaces and rejects non-numeric", () => {
|
|||
assert.equal(parseAmount("1,500,000,000"), 1500000000);
|
||||
assert.equal(parseAmount("1,500,000,000원"), 1500000000);
|
||||
assert.equal(parseAmount(" 850000000 "), 850000000);
|
||||
assert.equal(
|
||||
parseAmount('<img src="/images/number_01.gif" alt="첫번째"> 9,600,000<br>입찰시간(10:00)<br>'),
|
||||
9600000
|
||||
);
|
||||
assert.equal(parseAmount(""), null);
|
||||
assert.equal(parseAmount("-"), null);
|
||||
assert.equal(parseAmount("not-a-number"), null);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue