feat(M01): 조달 찾기 확정 모양 · 품셈재료 가격 연결 · 건설노임 준용 직종 고르기

- 고르기 API GET /api/m01/pick (나라장터자재 · 시중물가 · 공표 직종) — 조달 찾기 옛 API 대체
- 시중물가 조달›출처 「나라장터:<열쇠>」 연결 · 끊기 뒤 변화 없음 · 조달 값·기준 칸
- 품셈재료 「가격 연결」 모달(이름·규격 미리 찾기 · 관급 표시 · 후보 조건 · 끊기) · 못 이은 줄만 보기
- 건설노임 미공표 줄 「준용」 직종 고르기

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WRFJmmhPi4exAzHgPZHMT
This commit is contained in:
2026-09-19 23:53:28 +09:00
co-authored by Claude Sonnet 5
parent 796b0f8efc
commit d2a6646fc4
10 changed files with 367 additions and 135 deletions
+43 -13
View File
@@ -92,9 +92,11 @@ def files_of(group: str) -> list[dict]:
return out
def rows(file: str, page: int, size: int, q: str) -> dict:
def rows(file: str, page: int, size: int, q: str, unlinked: bool = False) -> dict:
data, version = read(file)
hits = [r for r in data.get(items_key(data)) or [] if _hit(r, q)]
if unlinked: # 품셈재료 — 아직 못 이은 줄
hits = [r for r in hits if not r.get("연결")]
page, size = max(page, 1), min(max(size, 1), 500)
return {
"file": file,
@@ -227,19 +229,47 @@ def elements(group: str, q: str, limit: int) -> dict:
return {"total": len(hits), "items": hits[: min(max(limit, 1), 200)]}
def procurement(q: str, limit: int) -> dict:
"""조달 찾기 창 — 나라장터자재 + 시중물가 안의 조달 줄에서 이름·규격 찾기 · `ref` = 「나라장터:<열쇠>」."""
words = q.lower().split()
_PICK = {
"procure": "재료_나라장터자재.json",
"price": "재료_시중물가.json",
"job": "인력_건설노임.json",
}
def _squash(text) -> str:
return "".join(str(text or "").lower().split())
def _lowest(value) -> object:
"""시중물가 줄 — 시중 다섯 칸 · 조달 값 가운데 낮은 값 · 그 밖은 `값` 그대로."""
if not isinstance(value, dict):
return value
nums = [v for v in value["시중"].values() if isinstance(v, (int, float))]
supply = value["조달"].get("")
return min(nums + ([supply] if supply is not None else []), default=None)
def pick(kind: str, q: str, limit: int) -> dict:
"""고르기 창 — `procure` 나라장터자재(ref = 나라장터:<열쇠>) · `price` 시중물가(관급 조달 줄 포함 · ref = 열쇠)
· `job` 값 있는 건설노임 직종(ref = 열쇠) · 이름·규격 낱말이 모두 들어간 줄."""
if kind not in _PICK:
raise StoreError(404, f"없는 고르기 「{kind}")
words = [_squash(w) for w in q.split()]
hits = []
for book, only_procured in (("나라장터자재", False), ("시중물가", True)):
data, _ = read(f"재료_{book}.json")
for row in data.get("") or []:
if only_procured and "조달" not in row:
continue
text = f"{row.get('이름', '')} {row.get('규격', '')}".lower()
if all(w in text for w in words):
brief = {k: row.get(k) for k in ("이름", "규격", "단위", "")}
hits.append({"ref": f"{book}:{row.get('열쇠')}", **brief})
for row in read(_PICK[kind])[0].get("") or []:
if kind == "job" and row.get("") is None:
continue
text = _squash(f"{row.get('이름')} {row.get('규격')}")
if all(w in text for w in words):
key = str(row.get("열쇠"))
hits.append(
{
"ref": f"나라장터:{key}" if kind == "procure" else key,
**{k: row.get(k) for k in ("이름", "규격", "단위")},
"": _lowest(row.get("")),
"관급": kind == "price" and row[""]["조달"].get("") is not None,
}
)
return {"total": len(hits), "items": hits[: min(max(limit, 1), 200)]}