feat(M01): 준용 고르면 값 따라옴 · 인력 상태 거름 · 유가전력 구분·상세구분 거름

- 인력 준용 고르기 — 값 칸이 고른 직종 값으로 바뀌어 노랑 · 비우면 원래 값
- 저장 때 서버가 준용 직종 값을 다시 읽어 값 칸에 채움(화면 값 불신)
- 인력 왼쪽 패널 「상태」 드롭다운(목록은 서버) · 재료 유가전력 2단 거름
- 시험 2개 더함

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WRFJmmhPi4exAzHgPZHMT
This commit is contained in:
2026-09-20 20:03:59 +09:00
co-authored by Claude Sonnet 5
parent 572fb8e5ee
commit 4b1eeec524
8 changed files with 171 additions and 40 deletions
+34 -1
View File
@@ -126,6 +126,9 @@ def refs_of(rows_: list[dict]) -> dict[str, str]:
return out
_STATES = ("공표", "산정", "미공표", "미확보", "추정") # 인력 「상태」 거름 차례
def subs(file: str = "", group: str = "") -> dict:
"""하위 거름 목록 — 한 테이블 파일은 머리 묶음(인력 「구분」 · 기계 「세부분류」) ·
장 그룹(소요량 · 계수 · 로직)은 장 파일에서 모음(구분 = 원문 + 부문 · 상세구분 = 장 · 차례대로)."""
@@ -144,12 +147,26 @@ def subs(file: str = "", group: str = "") -> dict:
return {"file": "", "version": "", "slot": "구분", "subs": list(found.values())}
data, version = read(file)
slot = next((s for s in ("구분", "세부분류") if isinstance(data.get(s), dict)), "")
lines = data.get(items_key(data)) or []
has_state = any("상태" in r for r in lines)
def details_of(name: str, head: dict) -> list:
"""머리에 상세구분이 없으면 줄에서 모음(재료_유가전력 — 지역 · 계약종별)."""
if head.get("상세구분"):
return head["상세구분"]
return list(
dict.fromkeys(
r["상세구분"] for r in lines if r.get("구분") == name and r.get("상세구분")
)
)
return {
"file": file,
"version": version,
"slot": slot,
"states": list(_STATES) if has_state else [],
"subs": [
{"name": name, "book": head.get("원문"), "details": head.get("상세구분") or []}
{"name": name, "book": head.get("원문"), "details": details_of(name, head)}
for name, head in (data.get(slot) or {}).items()
],
}
@@ -163,6 +180,7 @@ def rows(
unlinked: bool = False,
sub: str = "",
detail: str = "",
state: str = "",
) -> dict:
data, version = read(file)
hits = [r for r in data.get(items_key(data)) or [] if _hit(r, q)]
@@ -170,6 +188,8 @@ def rows(
hits = [r for r in hits if sub in (r.get("구분"), r.get("세부분류"))]
if detail: # 인력 상세구분
hits = [r for r in hits if r.get("상세구분") == detail]
if state: # 인력 상태
hits = [r for r in hits if r.get("상태") == state]
if unlinked: # 품셈재료 — 아직 못 이은 줄
hits = [r for r in hits if not r.get("연결")]
page, size = max(page, 1), min(max(size, 1), 500)
@@ -452,6 +472,17 @@ def _chapter_cols(file: str, data: dict) -> dict:
}
def _follow(row: dict, items: list[dict], file: str) -> dict:
"""인력 줄의 준용 = 그 직종의 값을 다시 읽어 값 칸에 채움(화면이 보낸 값을 믿지 않음)."""
ref = row.get("준용")
if file != "인력.json" or not isinstance(ref, str) or not _REF_KEY.match(ref):
return row
src = next((r for r in items if r.get("") == ref), None)
if src is None or src.get("") is None:
return row
return {**row, "": src[""]}
def _apply(data: dict, changes: list[dict], file: str, book: dict) -> None:
"""고침 · 더함(키는 대장의 다음 번호 · 원문번호 없으면 빈 글) · 지움 — 키는 바꾸지 않음."""
items = data.setdefault(items_key(data), [])
@@ -466,6 +497,8 @@ def _apply(data: dict, changes: list[dict], file: str, book: dict) -> None:
raise StoreError(404, f"{file} · 없는 키 「{key}")
if op != "delete" and not isinstance(row, dict):
raise StoreError(400, f"{file} · row 없음")
if op != "delete":
row = _follow(row, items, file)
if op == "add":
number = str(row.get("원문번호") or "")
cols = _chapter_cols(file, data)