knowledge(마스터): 소요량·계수·로직에 구분·상세구분 — 표·줄마다 칸 · 갈래 목록은 서버가 줌

- 장 파일 137 개의 표 3,028 · 로직 1,351 줄에 구분(원문 + 부문 · 「건설품셈 공통」) · 상세구분(「03장 토공사」) 칸을 직접 둠 · 파일 머리의 부문·차례는 목록 차례로 남김
- 열 차례 — 표는 키 · 원문번호 · 구분 · 상세구분 · 이름 · 기준 · 출처 · 비고 · 조건 · 범위규칙 · 값칸 · 줄 · 주 · 그룹(맨 뒤) · 로직은 소유 다음에 비고
- 서버 — GET /subs?group= 이 구분·상세구분 목록(장 차례대로) · GET /tables 가 파일을 가로질러 구분·상세구분·찾기로 거르고 쪽 나눔 · GET /logics 도 구분·상세구분으로 거름 · 새 줄은 서버가 갈래를 채움 · 죽은 book 칸 걷어냄
- check_master 가 구분·상세구분이 파일 머리·이름과 맞는지 봄 · adopt 가 빌더 줄에 갈래를 찍어 되돌리기 시험 글자까지 같음 · _틀.md · 화면 계약 · 화면 트리 · 시험 같이 고침

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgUhN55Z3BCYhUJTjwTNfj
This commit is contained in:
2026-09-20 16:30:48 +09:00
co-authored by Claude Opus 5
parent 9de61eb251
commit 10418ccbd4
154 changed files with 8227 additions and 8046 deletions
+86 -24
View File
@@ -127,8 +127,22 @@ def refs_of(rows_: list[dict]) -> dict[str, str]:
return out
def subs(file: str) -> dict:
"""하위 거름 목록 — 인력 「구분」 · 기계 「세부분류」 묶음(머리에 등록된 것) · 그 밖은 빈 목록."""
def subs(file: str = "", group: str = "") -> dict:
"""하위 거름 목록 — 한 테이블 파일은 머리 묶음(인력 「구분」 · 기계 「세부분류」) ·
장 그룹(소요량 · 계수 · 로직)은 장 파일에서 모음(구분 = 원문 + 부문 · 상세구분 = 장 · 차례대로)."""
if group:
if group not in cm.mf.GROUPS:
raise StoreError(404, f"없는 그룹 「{group}")
found: dict[str, dict] = {}
for info in files_of(group):
name = mk.CHAPTER.match(info["file"])
if not name:
continue
data = read(info["file"])[0]
kind = " ".join(x for x in (data.get("원문"), data.get("부문")) if x)
slot = found.setdefault(kind, {"name": kind, "book": data.get("원문"), "details": []})
slot["details"].append(mk.detail_of(info["file"]))
return {"file": "", "version": "", "slot": "구분", "subs": list(found.values())}
data, version = read(file)
slot = next((s for s in ("구분", "세부분류") if isinstance(data.get(s), dict)), "")
return {
@@ -172,19 +186,51 @@ def rows(
}
def tables(file: str, q: str) -> dict:
data, version = read(file)
if items_key(data) != "":
raise StoreError(400, f"표형 파일 아님 「{file}")
heads = ("", "원문번호", "이름", "기준", "출처", "조건", "값칸")
_TABLE_HEADS = ("", "원문번호", "구분", "상세구분", "이름", "기준", "출처", "조건", "값칸")
def tables(
file: str = "",
group: str = "",
sub: str = "",
detail: str = "",
q: str = "",
page: int = 1,
size: int = 30,
) -> dict:
"""표 목록 — 파일 하나 또는 그룹 전체(구분 · 상세구분 · 찾기로 거름 · 장 차례대로 · 쪽 나눔)."""
if file:
names = [file]
elif group in cm.mf.TABLE_GROUPS:
names = [f["file"] for f in files_of(group)]
else:
raise StoreError(404, f"없는 표형 그룹 「{group}")
hits, version = [], ""
for name in names:
data, version = read(name)
if items_key(data) != "":
raise StoreError(400, f"표형 파일 아님 「{name}")
for t in data[""]:
if sub and t.get("구분") != sub:
continue
if detail and t.get("상세구분") != detail:
continue
if _hit(t, q):
hits.append(
{
"file": name,
**{k: t.get(k) for k in _TABLE_HEADS},
"count": len(t.get("") or []),
}
)
page, size = max(page, 1), min(max(size, 1), 500)
return {
"file": file,
"version": version,
"tables": [
{**{k: t.get(k) for k in heads}, "count": len(t.get("") or [])}
for t in data[""]
if _hit(t, q)
],
"version": version if file else "",
"total": len(hits),
"page": page,
"size": size,
"tables": hits[(page - 1) * size : page * size],
}
@@ -206,14 +252,15 @@ def _logic_files(files: dict[str, dict]):
yield name, data
def logics(book: str, chapter: str, q: str, blocked: int | None) -> list[dict]:
def logics(sub: str, detail: str, q: str, blocked: int | None) -> list[dict]:
"""로직 목록 — 구분 · 상세구분 · 찾기로 거름(장 차례대로 · 파일을 가로지름)."""
files = cm.load(folder=FOLDER)
whole = cm.mf.Master(files)
out = []
for name, data in sorted(_logic_files(files), key=lambda f: f[1].get("차례") or 0):
if (book and data.get("원문") != book) or (chapter and chapter_of(name, data) != chapter):
continue
for row in data.get("") or []:
if (sub and row.get("구분") != sub) or (detail and row.get("상세구분") != detail):
continue
if not _hit(row, q):
continue
reasons = cm.mf.check_logic(whole, row)[0]
@@ -222,9 +269,10 @@ def logics(book: str, chapter: str, q: str, blocked: int | None) -> list[dict]:
out.append(
{
"file": name,
"book": data.get("원문"),
"chapter": chapter_of(name, data),
**{k: row.get(k) for k in ("", "원문번호", "이름", "결과단위", "출처")},
**{
k: row.get(k)
for k in ("", "원문번호", "구분", "상세구분", "이름", "결과단위", "출처")
},
"blocked": bool(reasons),
"reasons": reasons,
}
@@ -232,8 +280,8 @@ def logics(book: str, chapter: str, q: str, blocked: int | None) -> list[dict]:
return out
def logic(book: str, key: str) -> dict:
"""로직 하나 — 키는 전체에서 하나(`book` 은 화면 호환으로만 받음)."""
def logic(key: str) -> dict:
"""로직 하나 — 키는 전체에서 하나."""
files = cm.load(folder=FOLDER)
for name, data in _logic_files(files):
for row in data.get("") or []:
@@ -349,7 +397,7 @@ def _swap(files: dict, key: str, row: dict, file: str | None) -> str:
return new
def calc(book: str, key: str, inputs: dict, row: dict | None = None, file: str | None = None):
def calc(key: str, inputs: dict, row: dict | None = None, file: str | None = None):
given = {k: _dec(v) for k, v in inputs.items()}
files = cm.load(folder=FOLDER)
if row is not None:
@@ -382,6 +430,16 @@ def _dec(v):
return v
def _chapter_cols(file: str, data: dict) -> dict:
"""장 파일에 더하는 줄의 갈래 — 구분 = 원문 + 부문 · 상세구분 = 「NN장 장 제목」."""
if not mk.CHAPTER.match(file):
return {}
return {
"구분": " ".join(x for x in (data.get("원문"), data.get("부문")) if x),
"상세구분": mk.detail_of(file),
}
def _apply(data: dict, changes: list[dict], file: str, book: dict) -> None:
"""고침 · 더함(키는 대장의 다음 번호 · 원문번호 없으면 빈 글) · 지움 — 키는 바꾸지 않음."""
items = data.setdefault(items_key(data), [])
@@ -398,8 +456,12 @@ def _apply(data: dict, changes: list[dict], file: str, book: dict) -> None:
raise StoreError(400, f"{file} · row 없음")
if op == "add":
number = str(row.get("원문번호") or "")
rest = {k: v for k, v in row.items() if k not in ("", "원문번호")}
items.append({"": mk.issue(book, tid, number, file), "원문번호": number, **rest})
cols = _chapter_cols(file, data)
drop = ("", "원문번호", *cols)
rest = {k: v for k, v in row.items() if k not in drop}
items.append(
{"": mk.issue(book, tid, number, file), "원문번호": number, **cols, **rest}
)
elif op == "edit":
if str(row.get("")) != key:
raise StoreError(400, f"{file} · 키는 바꾸지 않음 「{key}")