Merge remote-tracking branch 'origin/dev' into sub_laptop_4
충돌 49개 구조물 json + Store.py + test_m02_structure.py — dev 쪽 산출근거 분리(basis/) 구조를 따르고 내 쪽 도면.layers 기본 도면층을 얹어 합침. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EUypcnp5d1gU2aeKh9F2H7
This commit is contained in:
@@ -14,14 +14,22 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from common_util import common_util_spreadsheet as spreadsheet
|
||||
from common_util.common_util_json import atomic_write_json
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
|
||||
FOLDER: Path = Path(__file__).resolve().parent.parent / "resources" / "master_template"
|
||||
KINDS = layers.KINDS # 시험은 FOLDER 를 사본으로 바꿈
|
||||
# 구조물 도면 = 구조물집계표 열 id 마다 하나(파일 이름 = 열 id) · 도각 없는 전용 화면(그린 만큼이 설계 영역)
|
||||
# 산출근거 = 같은 열 id 로 따로 한 파일(`basis/<열 id>.json` · 스프레드시트 통합문서)
|
||||
STRUCTURE_TABLE = "구조물집계표"
|
||||
_BASIS_HEAD = (("work", "공종"), ("spec", "규격"), ("detail", "산출 내역"), ("unit", "단위"))
|
||||
_BASIS_HEAD = ("공종", "규격", "산출 내역", "단위", "수량(m당)")
|
||||
_BASIS_HEAD_STYLE = {
|
||||
"굵게": True,
|
||||
"가로": "center",
|
||||
"세로": "center",
|
||||
"테두리": {side: {"선": "thin"} for side in ("위", "아래", "왼", "오른")},
|
||||
}
|
||||
_NUMBER = re.compile(r"^구-(\d+)$")
|
||||
_BAD_NAME = re.compile(r'[\\/:*?"<>|\x00-\x1f]|\.\.')
|
||||
# 도각 없는 빈 CAD 문서의 기본 도면층 — 없으면 B07 CAD 가 못 엶("Cannot read properties
|
||||
@@ -84,10 +92,14 @@ def _check_skeleton(kind: str, doc: Any) -> None:
|
||||
|
||||
def write(kind: str, name: str, version: str, doc: Any) -> dict[str, Any]:
|
||||
"""`version` 이 빈 글이면 새로 만듦(이미 있으면 409) · 아니면 그 판일 때만 덮어씀.
|
||||
구조물집계표를 저장하면 — 새 열의 빈 구조물 도면도 같이 만듦(잠금 밖에서 · `create_structure`
|
||||
가 다시 잠금을 잡으므로 안에서 부르면 죽음)."""
|
||||
구조물집계표를 저장하면 — 새 열의 빈 구조물 도면 · 산출근거도 같이 만듦(잠금 밖에서 ·
|
||||
`create_structure` 가 다시 잠금을 잡으므로 안에서 부르면 죽음).
|
||||
산출근거(basis)는 서버가 같은 TS 엔진을 Node 로 돌려 `계산값` 을 새로 적음
|
||||
(브라우저 값 버림 · 5장 ②)."""
|
||||
path = _path(kind, name)
|
||||
_check_skeleton(kind, doc)
|
||||
if kind == "basis":
|
||||
doc = _server_values(doc)
|
||||
with _LOCK:
|
||||
have = version_of(path.read_bytes()) if path.is_file() else ""
|
||||
if have != (version or ""):
|
||||
@@ -129,16 +141,17 @@ def _number(doc: Any) -> int:
|
||||
return int(found.group(1)) if found else 0
|
||||
|
||||
|
||||
def create_structure(column: str) -> dict[str, Any]:
|
||||
"""열 id 하나의 구조물 도면 새로 — 도번 = 있는 것 중 가장 큰 번호 + 1 · 이미 있으면 409."""
|
||||
path = _path("structure", column)
|
||||
def _column_ids() -> list[str]:
|
||||
table = read("table", STRUCTURE_TABLE)["문서"]
|
||||
if column not in {col.get("id") for col in table.get("열", []) if isinstance(col, dict)}:
|
||||
return [col["id"] for col in table.get("열", []) if isinstance(col, dict) and col.get("id")]
|
||||
|
||||
|
||||
def create_structure(column: str) -> dict[str, Any]:
|
||||
"""열 id 하나의 구조물 도면 새로 — 도번 = 있는 것 중 가장 큰 번호 + 1 · 이미 있으면 409.
|
||||
산출근거 파일이 없으면 빈 통합문서도 같이 만듦."""
|
||||
path = _path("structure", column)
|
||||
if column not in _column_ids():
|
||||
raise StoreError(404, f"{STRUCTURE_TABLE}에 없는 열 「{column}」")
|
||||
basis = [{"id": key, "머리": [label], "단위": None, "꼴": "글"} for key, label in _BASIS_HEAD]
|
||||
basis.append({"id": "qty", "머리": ["수량(m당)"], "단위": None, "꼴": "수"})
|
||||
# 빈 줄 몇 개를 미리 둠 — 「줄 더하기」 안 눌러도 바로 입력하게
|
||||
basis_rows = [{"id": str(i), "값": {}} for i in range(1, 4)]
|
||||
with _LOCK:
|
||||
if path.is_file():
|
||||
raise StoreError(409, f"이미 있는 구조물 도면 「{column}」")
|
||||
@@ -150,21 +163,107 @@ def create_structure(column: str) -> dict[str, Any]:
|
||||
"열": column,
|
||||
"도번": f"구-{number:02d}",
|
||||
"도면": {"format": 6, "entities": [], "layers": [dict(_BLANK_LAYER)]},
|
||||
"산출근거": {"양식": "산출근거", "종류": "표", "판": 1, "열": basis, "줄": basis_rows},
|
||||
}
|
||||
atomic_write_json(path, doc)
|
||||
ensure_basis(column)
|
||||
return read("structure", column)
|
||||
|
||||
|
||||
# ── 산출근거 ──────────────────────────────────────────
|
||||
|
||||
|
||||
def _col_letters(index: int) -> str:
|
||||
letters = ""
|
||||
index += 1
|
||||
while index:
|
||||
index, rest = divmod(index - 1, 26)
|
||||
letters = chr(65 + rest) + letters
|
||||
return letters
|
||||
|
||||
|
||||
def basis_doc(column: str, old: Any = None) -> dict[str, Any]:
|
||||
"""빈 산출근거 통합문서 — 1 행 머리(공종 · 규격 · 산출 내역 · 단위 · 수량(m당)).
|
||||
`old` = 옛 구조물 문서 `산출근거` 표(열 · 줄) — 머리 글 · 칸 값 · 열 폭을 옮김
|
||||
(옛 열 식은 없어 안 옮김)."""
|
||||
columns = old.get("열") if isinstance(old, dict) and isinstance(old.get("열"), list) else None
|
||||
if columns:
|
||||
heads = [
|
||||
" ".join(str(h) for h in (c.get("머리") or []) if h) or c.get("id", "") for c in columns
|
||||
]
|
||||
else:
|
||||
heads, columns = list(_BASIS_HEAD), []
|
||||
cells: dict[str, Any] = {
|
||||
f"{_col_letters(i)}1": {"값": head, "서식": 1} for i, head in enumerate(heads) if head
|
||||
}
|
||||
for r, row in enumerate(old.get("줄", []) if columns else [], start=2):
|
||||
values = row.get("값", {}) if isinstance(row, dict) else {}
|
||||
for i, col in enumerate(columns):
|
||||
value = values.get(col.get("id"))
|
||||
if value not in (None, ""):
|
||||
cells[f"{_col_letters(i)}{r}"] = {"값": value}
|
||||
widths = ((old or {}).get("보기") or {}).get("열너비") or {} if columns else {}
|
||||
# 옛 폭은 px · 새 폭은 엑셀 글자 단위(숫자 폭 7px 어림)
|
||||
info = {
|
||||
_col_letters(i): {"폭": round(widths[c["id"]] / 7, 1)}
|
||||
for i, c in enumerate(columns)
|
||||
if isinstance(widths.get(c.get("id")), (int, float))
|
||||
}
|
||||
sheet: dict[str, Any] = {"id": "s1", "이름": "산출근거", "칸": cells}
|
||||
if info:
|
||||
sheet["열"] = info
|
||||
return {
|
||||
"종류": "통합문서",
|
||||
"판": 1,
|
||||
"열": column,
|
||||
"서식": [{}, _BASIS_HEAD_STYLE],
|
||||
"시트": [sheet],
|
||||
"활성": "s1",
|
||||
}
|
||||
|
||||
|
||||
def ensure_basis(column: str, old: Any = None) -> bool:
|
||||
"""산출근거 파일이 없으면 만듦(식 없는 새 문서라 `계산값` 없음 · 첫 [저장] 때 서버가 적음)."""
|
||||
path = _path("basis", column)
|
||||
with _LOCK:
|
||||
if path.is_file():
|
||||
return False
|
||||
atomic_write_json(path, basis_doc(column, old))
|
||||
return True
|
||||
|
||||
|
||||
def _server_values(doc: Any) -> dict[str, Any]:
|
||||
try:
|
||||
return spreadsheet.with_server_values(doc)
|
||||
except spreadsheet.RecalcError as e:
|
||||
raise StoreError(503, str(e)) from e
|
||||
|
||||
|
||||
def migrate_basis() -> dict[str, list[str]]:
|
||||
"""한 번 — 옛 구조물 문서 안 `산출근거` 표를 `basis/<열 id>.json` 으로 옮기고
|
||||
구조물 문서에서 뺌 · 그 뒤 산출근거가 없는 집계표 열은 빈 통합문서.
|
||||
두 번 돌려도 같음(있는 파일은 안 덮음)."""
|
||||
moved: list[str] = []
|
||||
for name, doc in _structures():
|
||||
if not isinstance(doc, dict) or "산출근거" not in doc:
|
||||
continue
|
||||
ensure_basis(name, doc["산출근거"])
|
||||
rest = {key: value for key, value in doc.items() if key != "산출근거"}
|
||||
with _LOCK:
|
||||
atomic_write_json(_path("structure", name), rest)
|
||||
moved.append(name)
|
||||
made = [column for column in _column_ids() if ensure_basis(column)]
|
||||
return {"옮김": moved, "새로": made}
|
||||
|
||||
|
||||
def backfill_missing_structures() -> list[str]:
|
||||
"""구조물집계표 열마다 빈 구조물 도면이 있게 — 없는 열만 열 차례로 새로 만듦(도번 이어서).
|
||||
"""구조물집계표 열마다 빈 구조물 도면 · 산출근거가 있게 — 없는 열만 열 차례로 새로
|
||||
만듦(도번 이어서).
|
||||
그 사이 남이 만들었거나(409) 열 규칙에 안 맞으면(404) 그 열만 건너뜀."""
|
||||
table = read("table", STRUCTURE_TABLE)["문서"]
|
||||
have = {name for name, _ in _structures()}
|
||||
made: list[str] = []
|
||||
for col in table.get("열", []):
|
||||
cid = col.get("id") if isinstance(col, dict) else None
|
||||
if not cid or cid in have:
|
||||
for cid in _column_ids():
|
||||
if cid in have:
|
||||
ensure_basis(cid)
|
||||
continue
|
||||
try:
|
||||
create_structure(cid)
|
||||
|
||||
Reference in New Issue
Block a user