Merge remote-tracking branch 'origin/sub_laptop_4' into main_laptop_1
This commit is contained in:
@@ -114,12 +114,25 @@ export const listStructures = async (): Promise<TemplateInfo[]> =>
|
||||
export const createStructure = (column: string): Promise<TemplateDoc> =>
|
||||
call(`/structures/${enc(column)}`, { method: "POST" });
|
||||
|
||||
/** `{열 id: 도번}` — 집계표 도면 줄(없는 열은 키 없음 = 「없음」) */
|
||||
export const fetchStructureNumbers = (): Promise<Record<string, string>> =>
|
||||
/** 한 열의 번호 — 도번 `SD` · 표번 `SC` + 같은 6자리 [종류][분류1][분류2](없는 쪽은 빈 글) */
|
||||
export interface StructureNumbers {
|
||||
도번: string;
|
||||
표번: string;
|
||||
}
|
||||
|
||||
/** `{열 id: {도번, 표번}}` — 구조물 열만(글 열 · 없는 열은 키 없음 = 「없음」) */
|
||||
export const fetchStructureNumbers = (): Promise<Record<string, StructureNumbers>> =>
|
||||
call("/structures/numbers");
|
||||
|
||||
/** `{열 id: 표번}` — 좌측 산출근거 줄(도번과 같은 규칙 · 「표-01」 꼴) */
|
||||
export const fetchBasisNumbers = (): Promise<Record<string, string>> => call("/basis/numbers");
|
||||
/** 다른 열(`from`) 구조물 도면 내용을 받는 열(`column`)로 가져옴 — 도번 · 열은 받는 쪽 그대로 */
|
||||
export const copyStructure = async (column: string, from: string): Promise<void> => {
|
||||
await call(`/structures/${enc(column)}/copy?from=${enc(from)}`, { method: "POST" });
|
||||
};
|
||||
|
||||
/** 다른 열(`from`) 산출근거 내용을 받는 열(`column`)로 가져옴 — 표번 · 열은 받는 쪽 그대로 */
|
||||
export const copyBasis = async (column: string, from: string): Promise<void> => {
|
||||
await call(`/basis/${enc(column)}/copy?from=${enc(from)}`, { method: "POST" });
|
||||
};
|
||||
|
||||
/* --- 프로젝트 층 단추 다섯 --- */
|
||||
const project = (id: string, tail: string): string => `/projects/${enc(id)}/templates/${tail}`;
|
||||
|
||||
@@ -7,7 +7,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
@@ -39,13 +39,20 @@ def get_structure_numbers() -> dict:
|
||||
|
||||
@router.post("/structures/{column}")
|
||||
def post_structure(column: str) -> dict:
|
||||
"""구조물 도면 새로 — 열 id 하나 · 도번 자동 · A1 도각 + 빈 산출근거 표 · 이미 있으면 409."""
|
||||
"""구조물 도면 새로 — 열 id 하나 · 도번 자동 · 빈 산출근거도 · 이미 있으면 409."""
|
||||
return _call(store.create_structure, column)
|
||||
|
||||
|
||||
@router.get("/basis/numbers")
|
||||
def get_basis_numbers() -> dict:
|
||||
return _call(store.basis_numbers)
|
||||
@router.post("/structures/{column}/copy")
|
||||
def post_structure_copy(column: str, source: str = Query(alias="from")) -> dict:
|
||||
"""다른 열 구조물 도면 내용을 가져옴 — 도번 · 열은 받는 쪽 그대로."""
|
||||
return _call(store.copy_structure, column, source)
|
||||
|
||||
|
||||
@router.post("/basis/{column}/copy")
|
||||
def post_basis_copy(column: str, source: str = Query(alias="from")) -> dict:
|
||||
"""다른 열 산출근거 내용을 가져옴 — 표번 · 열은 받는 쪽 그대로."""
|
||||
return _call(store.copy_basis, column, source)
|
||||
|
||||
|
||||
@router.get("/templates/{kind}/{name}")
|
||||
|
||||
@@ -20,8 +20,10 @@ 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) · 도각 없는 전용 화면
|
||||
# 산출근거 = 같은 열 id 로 따로 한 파일(`basis/<열 id>.json` · 스프레드시트 통합문서)
|
||||
# 번호 = 도번 SD · 표번 SC + 6자리 [종류][분류1][분류2](제목행 = 열 `머리` 세 층 · 없는 층 00) —
|
||||
# 한 열의 두 번호는 같은 6자리 · 한 번 준 번호는 파일에 남아 안 바뀜(PLAN 12장)
|
||||
STRUCTURE_TABLE = "구조물집계표"
|
||||
_BASIS_HEAD = ("공종", "규격", "산출 내역", "단위", "수량(m당)")
|
||||
_BASIS_HEAD_STYLE = {
|
||||
@@ -30,8 +32,7 @@ _BASIS_HEAD_STYLE = {
|
||||
"세로": "center",
|
||||
"테두리": {side: {"선": "thin"} for side in ("위", "아래", "왼", "오른")},
|
||||
}
|
||||
_NUMBER = re.compile(r"^구-(\d+)$")
|
||||
_TABLE_NUMBER = re.compile(r"^표-(\d+)$")
|
||||
_CODE = re.compile(r"^S[DC](\d{6})$")
|
||||
_BAD_NAME = re.compile(r'[\\/:*?"<>|\x00-\x1f]|\.\.')
|
||||
# 도각 없는 빈 CAD 문서의 기본 도면층 — 없으면 B07 CAD 가 못 엶("Cannot read properties
|
||||
# of undefined (reading 'length')") · CAD 쪽도 없으면 새로 채우지만(2026-09-27 sub1 고침)
|
||||
@@ -93,8 +94,8 @@ 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` 가 다시 잠금을 잡으므로 안에서 부르면 죽음).
|
||||
구조물집계표를 저장하면 — 새 구조물 열의 빈 구조물 도면 · 산출근거를 번호와 같이 만들고
|
||||
지운 열(또는 구조물이 아니게 된 열)의 두 파일을 지움(잠금 밖에서 · 안에서 부르면 죽음).
|
||||
산출근거(basis)는 서버가 같은 TS 엔진을 Node 로 돌려 `계산값` 을 새로 적음
|
||||
(브라우저 값 버림 · 5장 ②)."""
|
||||
path = _path(kind, name)
|
||||
@@ -108,7 +109,8 @@ def write(kind: str, name: str, version: str, doc: Any) -> dict[str, Any]:
|
||||
atomic_write_json(path, doc)
|
||||
info = _info(kind, path)
|
||||
if kind == "table" and name == STRUCTURE_TABLE:
|
||||
backfill_missing_structures()
|
||||
backfill_missing_structures() # 지우기 앞 — 같은 저장의 새 열이 지운 번호를 안 받게
|
||||
prune_removed_structures()
|
||||
return info
|
||||
|
||||
|
||||
@@ -130,46 +132,131 @@ def _structures() -> list[tuple[str, Any]]:
|
||||
return [(p.stem, json.loads(p.read_bytes())) for p in sorted(folder.glob("[!.]*.json"))]
|
||||
|
||||
|
||||
def structure_numbers() -> dict[str, str | None]:
|
||||
"""`{열 id: 도번}` — 집계표 도면 줄이 한 번에 받음."""
|
||||
return {
|
||||
name: (doc.get("도번") if isinstance(doc, dict) else None) for name, doc in _structures()
|
||||
}
|
||||
def _code(doc: Any, key: str) -> str | None:
|
||||
"""문서의 새 번호 6자리 — 옛 `구-01` · `표-01` · 없음은 None."""
|
||||
found = _CODE.match(str(doc.get(key) or "")) if isinstance(doc, dict) else None
|
||||
return found.group(1) if found else None
|
||||
|
||||
|
||||
def _number(doc: Any) -> int:
|
||||
found = _NUMBER.match(str(doc.get("도번") or "")) if isinstance(doc, dict) else None
|
||||
return int(found.group(1)) if found else 0
|
||||
def _codes() -> dict[str, str]:
|
||||
"""`{열 id: 6자리}` — 구조물 도면 도번 먼저 · 없으면 산출근거 표번."""
|
||||
codes = {name: c for name, doc in _basis_docs() if (c := _code(doc, "표번"))}
|
||||
codes.update({name: c for name, doc in _structures() if (c := _code(doc, "도번"))})
|
||||
return codes
|
||||
|
||||
|
||||
def structure_numbers() -> dict[str, dict[str, str]]:
|
||||
"""`{열 id: {도번, 표번}}` — 집계표 도면 · 산출근거 줄 · 좌측 줄이 한 번에 받음
|
||||
(없는 쪽은 빈 글)."""
|
||||
out: dict[str, dict[str, str]] = {}
|
||||
for name, doc in _structures():
|
||||
out.setdefault(name, {"도번": "", "표번": ""})["도번"] = str(doc.get("도번") or "")
|
||||
for name, doc in _basis_docs():
|
||||
out.setdefault(name, {"도번": "", "표번": ""})["표번"] = str(doc.get("표번") or "")
|
||||
return out
|
||||
|
||||
|
||||
def _heads(col: dict[str, Any]) -> tuple[str | None, str | None, str | None]:
|
||||
"""제목행 세 층(종류 · 분류1 · 분류2) — 빈 층은 None."""
|
||||
parts = [*(col.get("머리") or []), None, None, None][:3]
|
||||
clean = [str(p).strip() if p is not None else "" for p in parts]
|
||||
return (clean[0] or None, clean[1] or None, clean[2] or None)
|
||||
|
||||
|
||||
def _structure_columns() -> list[dict[str, Any]]:
|
||||
"""번호 · 파일을 받는 열 — 꼴=수(NO · 측점 · 비고 같은 글 열은 구조물 아님)."""
|
||||
table = read("table", STRUCTURE_TABLE)["문서"]
|
||||
return [
|
||||
col
|
||||
for col in table.get("열", [])
|
||||
if isinstance(col, dict) and col.get("id") and col.get("꼴") == "수"
|
||||
]
|
||||
|
||||
|
||||
def _column_ids() -> list[str]:
|
||||
table = read("table", STRUCTURE_TABLE)["문서"]
|
||||
return [col["id"] for col in table.get("열", []) if isinstance(col, dict) and col.get("id")]
|
||||
return [col["id"] for col in _structure_columns()]
|
||||
|
||||
|
||||
def _pick_code(col: dict[str, Any], taken: dict[str, str], heads: dict[str, tuple]) -> str:
|
||||
"""새 열 번호 — 같은 종류(· 분류1)가 이미 번호를 가졌으면 그 두 자리를 이어 받고 · 없으면 그 층
|
||||
가장 큰 번호 + 1 · 빈 층은 00. 같은 6자리가 이미 있으면(같은 제목행 · 빈 분류2 겹침) 분류2 를
|
||||
그 무리 가장 큰 번호 + 1 로. `taken` = 이미 번호 있는 열 `{열 id: 6자리}` ·
|
||||
`heads` = 열 id → 제목행."""
|
||||
kind, first, second = _heads(col)
|
||||
codes = list(taken.values())
|
||||
mates = [(heads[cid], code) for cid, code in taken.items() if cid in heads]
|
||||
|
||||
def next_of(prefix: str, at: slice) -> str:
|
||||
found = [int(c[at]) for c in codes if c.startswith(prefix)]
|
||||
return f"{max(found, default=0) + 1:02d}"
|
||||
|
||||
k = next((c[:2] for h, c in mates if h[0] == kind), None) or next_of("", slice(0, 2))
|
||||
if first is None:
|
||||
a = "00"
|
||||
else:
|
||||
same = (c[2:4] for h, c in mates if h[:2] == (kind, first) and c[:2] == k)
|
||||
a = next(same, None) or next_of(k, slice(2, 4))
|
||||
b = "00" if second is None else next_of(k + a, slice(4, 6))
|
||||
if k + a + b in codes:
|
||||
b = next_of(k + a, slice(4, 6))
|
||||
return k + a + b
|
||||
|
||||
|
||||
def _new_code(column: str) -> str:
|
||||
"""이 열이 쓸 6자리 — 짝 파일에 이미 있으면 그것 · 없으면 새로(잠금 안에서 부름)."""
|
||||
taken = _codes()
|
||||
if column in taken:
|
||||
return taken[column]
|
||||
cols = _structure_columns()
|
||||
col = next((c for c in cols if c["id"] == column), {"id": column})
|
||||
return _pick_code(col, taken, {c["id"]: _heads(c) for c in cols})
|
||||
|
||||
|
||||
def create_structure(column: str) -> dict[str, Any]:
|
||||
"""열 id 하나의 구조물 도면 새로 — 도번 = 있는 것 중 가장 큰 번호 + 1 · 이미 있으면 409.
|
||||
"""구조물 열 하나의 구조물 도면 새로 — 도번 SD + 6자리(산출근거와 같은 6자리) · 이미 있으면 409.
|
||||
산출근거 파일이 없으면 빈 통합문서도 같이 만듦."""
|
||||
path = _path("structure", column)
|
||||
if column not in _column_ids():
|
||||
raise StoreError(404, f"{STRUCTURE_TABLE}에 없는 열 「{column}」")
|
||||
raise StoreError(404, f"{STRUCTURE_TABLE}에 없는 구조물 열 「{column}」")
|
||||
with _LOCK:
|
||||
if path.is_file():
|
||||
raise StoreError(409, f"이미 있는 구조물 도면 「{column}」")
|
||||
number = max((_number(doc) for _name, doc in _structures()), default=0) + 1
|
||||
doc = {
|
||||
"양식": "구조물도면",
|
||||
"종류": "structure",
|
||||
"판": 1,
|
||||
"열": column,
|
||||
"도번": f"구-{number:02d}",
|
||||
"도면": {"format": 6, "entities": [], "layers": [dict(_BLANK_LAYER)]},
|
||||
}
|
||||
atomic_write_json(path, doc)
|
||||
atomic_write_json(path, {**_blank_structure(column), "도번": f"SD{_new_code(column)}"})
|
||||
ensure_basis(column)
|
||||
return read("structure", column)
|
||||
|
||||
|
||||
def _blank_structure(column: str) -> dict[str, Any]:
|
||||
"""빈 구조물 도면(도번은 부르는 쪽이 붙임) — 도각 없음 · 기본 도면층 하나."""
|
||||
return {
|
||||
"양식": "구조물도면",
|
||||
"종류": "structure",
|
||||
"판": 1,
|
||||
"열": column,
|
||||
"도면": {"format": 6, "entities": [], "layers": [dict(_BLANK_LAYER)]},
|
||||
}
|
||||
|
||||
|
||||
def copy_structure(column: str, source: str) -> dict[str, Any]:
|
||||
"""다른 열 구조물 도면 내용을 가져옴 — 도번 · 열은 받는 쪽 그대로(두 열은 따로 움직임)."""
|
||||
return _copy("structure", column, source, ("열", "도번"))
|
||||
|
||||
|
||||
def copy_basis(column: str, source: str) -> dict[str, Any]:
|
||||
"""다른 열 산출근거 내용을 가져옴 — 표번 · 열은 받는 쪽 그대로 · 계산값은 서버가 다시 풂."""
|
||||
return _copy("basis", column, source, ("열", "표번"))
|
||||
|
||||
|
||||
def _copy(kind: str, column: str, source: str, keep: tuple[str, ...]) -> dict[str, Any]:
|
||||
target = read(kind, column)["문서"]
|
||||
doc = {**read(kind, source)["문서"], **{key: target.get(key) for key in keep}}
|
||||
if kind == "basis":
|
||||
doc = _server_values({k: v for k, v in doc.items() if k != "계산값"})
|
||||
with _LOCK:
|
||||
atomic_write_json(_path(kind, column), doc)
|
||||
return _info(kind, _path(kind, column))
|
||||
|
||||
|
||||
# ── 산출근거 ──────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -183,7 +270,7 @@ def _col_letters(index: int) -> str:
|
||||
|
||||
|
||||
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
|
||||
@@ -192,7 +279,7 @@ def basis_doc(column: str, old: Any = None) -> dict[str, Any]:
|
||||
" ".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), []
|
||||
heads, columns = [], []
|
||||
cells: dict[str, Any] = {
|
||||
f"{_col_letters(i)}1": {"값": head, "서식": 1} for i, head in enumerate(heads) if head
|
||||
}
|
||||
@@ -216,7 +303,7 @@ def basis_doc(column: str, old: Any = None) -> dict[str, Any]:
|
||||
"종류": "통합문서",
|
||||
"판": 1,
|
||||
"열": column,
|
||||
"서식": [{}, _BASIS_HEAD_STYLE],
|
||||
"서식": [{}, _BASIS_HEAD_STYLE] if heads else [{}],
|
||||
"시트": [sheet],
|
||||
"활성": "s1",
|
||||
}
|
||||
@@ -227,40 +314,80 @@ def _basis_docs() -> list[tuple[str, Any]]:
|
||||
return [(p.stem, json.loads(p.read_bytes())) for p in sorted(folder.glob("[!.]*.json"))]
|
||||
|
||||
|
||||
def _table_number(doc: Any) -> int:
|
||||
found = _TABLE_NUMBER.match(str(doc.get("표번") or "")) if isinstance(doc, dict) else None
|
||||
return int(found.group(1)) if found else 0
|
||||
|
||||
|
||||
def ensure_basis(column: str, old: Any = None) -> bool:
|
||||
"""산출근거 파일이 없으면 만듦(식 없는 새 문서라 `계산값` 없음 · 첫 [저장] 때 서버가 적음).
|
||||
표번 = 도번과 같은 규칙(있는 것 중 가장 큰 번호 + 1) · 만든 뒤로는 안 바뀜."""
|
||||
표번 = SC + 구조물 도면과 같은 6자리 · 만든 뒤로는 안 바뀜."""
|
||||
path = _path("basis", column)
|
||||
with _LOCK:
|
||||
if path.is_file():
|
||||
return False
|
||||
number = max((_table_number(doc) for _name, doc in _basis_docs()), default=0) + 1
|
||||
doc = {**basis_doc(column, old), "표번": f"표-{number:02d}"}
|
||||
doc = {**basis_doc(column, old), "표번": f"SC{_new_code(column)}"}
|
||||
atomic_write_json(path, doc)
|
||||
return True
|
||||
|
||||
|
||||
def basis_numbers() -> dict[str, str | None]:
|
||||
"""`{열 id: 표번}` — 좌측 산출근거 줄이 한 번에 받음.
|
||||
표번이 없는 옛 파일은 여기서 만나는 순서로 채움(도번 규칙과 같음)."""
|
||||
docs = _basis_docs()
|
||||
missing = [name for name, doc in docs if not (isinstance(doc, dict) and doc.get("표번"))]
|
||||
if missing:
|
||||
with _LOCK:
|
||||
docs = _basis_docs() # 잠금 안에서 다시 읽음 — 그 사이 누가 채웠을 수 있음
|
||||
number = max((_table_number(doc) for _name, doc in docs), default=0)
|
||||
for name, doc in docs:
|
||||
if isinstance(doc, dict) and not doc.get("표번"):
|
||||
number += 1
|
||||
doc["표번"] = f"표-{number:02d}"
|
||||
atomic_write_json(_path("basis", name), doc)
|
||||
docs = _basis_docs()
|
||||
return {name: (doc.get("표번") if isinstance(doc, dict) else None) for name, doc in docs}
|
||||
def _strip_default_head(doc: Any) -> bool:
|
||||
"""옛 빈 산출근거의 1 행 기본 머리(공종 · 규격 · 산출 내역 · 단위 · 수량(m당))를 지움 —
|
||||
다섯 칸이 모두 그 글일 때만(사용자가 고친 머리는 둠) · 머리 서식만 쓰였으면 서식 표도 비움."""
|
||||
sheets = doc.get("시트") if isinstance(doc, dict) else None
|
||||
cells = sheets[0].get("칸") if isinstance(sheets, list) and sheets else None
|
||||
keys = [f"{_col_letters(i)}1" for i in range(len(_BASIS_HEAD))]
|
||||
if not isinstance(cells, dict) or [(cells.get(k) or {}).get("값") for k in keys] != list(
|
||||
_BASIS_HEAD
|
||||
):
|
||||
return False
|
||||
for k in keys:
|
||||
del cells[k]
|
||||
if not any(isinstance(c, dict) and c.get("서식") for s in sheets for c in s["칸"].values()):
|
||||
doc["서식"] = [{}]
|
||||
return True
|
||||
|
||||
|
||||
def prune_removed_structures() -> list[str]:
|
||||
"""집계표에서 지운 열(또는 구조물이 아니게 된 열)의 구조물 도면 · 산출근거를 같이 지움 —
|
||||
번호는 비워 둠(다른 열 번호를 안 당김)."""
|
||||
keep = set(_column_ids())
|
||||
gone: list[str] = []
|
||||
with _LOCK:
|
||||
for kind in ("structure", "basis"):
|
||||
for path in sorted((FOLDER / kind).glob("[!.]*.json")):
|
||||
if path.stem not in keep:
|
||||
path.unlink()
|
||||
gone.append(f"{kind}/{path.stem}")
|
||||
return gone
|
||||
|
||||
|
||||
def renumber() -> dict[str, list[str]]:
|
||||
"""한 번 — 옛 번호(구-NN · 표-NN · 없음)를 새 번호로(열 차례 = 종류 · 분류1 · 분류2 차례) ·
|
||||
옛 빈 산출근거 기본 머리 줄 지움 · 구조물 아닌 열 파일 지움 · 빠진 파일 만듦.
|
||||
내용은 그대로 · 새 번호가 이미 있으면 그대로(두 번 돌려도 같음)."""
|
||||
gone = prune_removed_structures()
|
||||
changed: list[str] = []
|
||||
made: list[str] = []
|
||||
with _LOCK:
|
||||
cols = _structure_columns()
|
||||
heads = {c["id"]: _heads(c) for c in cols}
|
||||
taken = _codes()
|
||||
for col in cols:
|
||||
if col["id"] not in taken:
|
||||
taken[col["id"]] = _pick_code(col, taken, heads)
|
||||
docs = {"structure": dict(_structures()), "basis": dict(_basis_docs())}
|
||||
for col in cols:
|
||||
cid = col["id"]
|
||||
for kind, key, head in (("structure", "도번", "SD"), ("basis", "표번", "SC")):
|
||||
number = f"{head}{taken[cid]}"
|
||||
doc = docs[kind].get(cid)
|
||||
if doc is None:
|
||||
blank = _blank_structure(cid) if kind == "structure" else basis_doc(cid)
|
||||
atomic_write_json(_path(kind, cid), {**blank, key: number})
|
||||
made.append(f"{kind}/{cid}")
|
||||
continue
|
||||
stripped = kind == "basis" and _strip_default_head(doc)
|
||||
if doc.get(key) != number or stripped:
|
||||
doc[key] = number
|
||||
atomic_write_json(_path(kind, cid), doc)
|
||||
changed.append(f"{kind}/{cid}")
|
||||
return {"지움": gone, "바꿈": changed, "새로": made}
|
||||
|
||||
|
||||
def _server_values(doc: Any) -> dict[str, Any]:
|
||||
@@ -288,8 +415,8 @@ def migrate_basis() -> dict[str, list[str]]:
|
||||
|
||||
|
||||
def backfill_missing_structures() -> list[str]:
|
||||
"""구조물집계표 열마다 빈 구조물 도면 · 산출근거가 있게 — 없는 열만 열 차례로 새로
|
||||
만듦(도번 이어서).
|
||||
"""구조물 열마다 빈 구조물 도면 · 산출근거가 있게 — 없는 열만 열 차례로 새로
|
||||
만듦(그 무리 다음 번호).
|
||||
그 사이 남이 만들었거나(409) 열 규칙에 안 맞으면(404) 그 열만 건너뜀."""
|
||||
have = {name for name, _ in _structures()}
|
||||
made: list[str] = []
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
fetchStructureNumbers,
|
||||
readTemplate,
|
||||
type StructureDoc,
|
||||
type StructureNumbers,
|
||||
} from "./M02_MasterTemplete_Api_Fetch";
|
||||
import {
|
||||
mountDrawingTemplate,
|
||||
@@ -48,15 +49,23 @@ const fill = (text: string, value: string): string => text.replace("{value}", va
|
||||
/** 스프레드시트는 산출근거 카드를 열 때만 받음 */
|
||||
const loadSpreadsheet = () => import("../A00_Common/spreadsheet/spreadsheet");
|
||||
|
||||
/** 집계표 열 — 머리 글만 씀 */
|
||||
/** 집계표 열 — 머리 글(제목행 종류 · 분류1 · 분류2) · 꼴만 씀 */
|
||||
export interface StructureColumn {
|
||||
id: string;
|
||||
머리: (string | null)[];
|
||||
꼴?: string | null;
|
||||
}
|
||||
|
||||
/** 열 머리 글 — 층을 ` · ` 로 이음(빈 층은 건너뜀 · 다 비면 열 id) */
|
||||
/** 구조물 열 = 꼴=수 — NO · 측점 · 비고 같은 글 열은 도면 · 산출근거 · 번호가 없음(서버와 같은 규칙) */
|
||||
export const isStructureColumn = (col: StructureColumn): boolean => col.꼴 === "수";
|
||||
|
||||
/** 구조물 이름 + 변수값 — 제목행 층을 빈칸으로 이음(빈 층은 건너뜀 · 다 비면 열 id) */
|
||||
export const columnHead = (col: StructureColumn): string =>
|
||||
col.머리.filter((part): part is string => !!part && part.trim() !== "").join(" · ") || col.id;
|
||||
col.머리.filter((part): part is string => !!part && part.trim() !== "").join(" ") || col.id;
|
||||
|
||||
/** 번호가 맨 앞 — 「SD010101 돌기슭막이 H=1.0」(번호가 없으면 이름만) */
|
||||
export const structureLabel = (number: string, col: StructureColumn): string =>
|
||||
[number, columnHead(col)].filter(Boolean).join(" ");
|
||||
|
||||
export interface StructurePanelOptions {
|
||||
/** 시스템 관리자만 저장함(구조물 도면은 서버가 미리 만들어 둠) */
|
||||
@@ -80,7 +89,7 @@ export interface StructurePanelHandle {
|
||||
}
|
||||
|
||||
export function createStructurePanel(opts: StructurePanelOptions): StructurePanelHandle {
|
||||
let numbers: Record<string, string> = {};
|
||||
let numbers: Record<string, StructureNumbers> = {};
|
||||
let column: string | null = null;
|
||||
let columnLabel = "";
|
||||
let doc: StructureDoc | null = null;
|
||||
@@ -98,7 +107,7 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane
|
||||
className: "m02-struct__card ui-collapsible ui-sidebar-section ui-sidebar-card",
|
||||
children: [
|
||||
el("h3", { className: "ui-collapsible__title", text: TEXT.drawingInfo }),
|
||||
el("div", { className: "m02-struct__head", children: [columnName, number, edit] }),
|
||||
el("div", { className: "m02-struct__head", children: [number, columnName, edit] }),
|
||||
previewHost,
|
||||
],
|
||||
});
|
||||
@@ -162,7 +171,7 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane
|
||||
/** 미리보기 · 산출근거 — 문서가 없으면 없음 글(서버가 열마다 미리 만들어 둬 보통은 없음) */
|
||||
const paintBody = (mine: number): void => {
|
||||
drop();
|
||||
number.textContent = doc?.도번 || numbers[column ?? ""] || TEXT.noNumber;
|
||||
number.textContent = doc?.도번 || numbers[column ?? ""]?.도번 || TEXT.noNumber;
|
||||
edit.hidden = !opts.isAdmin;
|
||||
basisNo.textContent = basisNumber || TEXT.noTableNumber;
|
||||
editBasis.hidden = !opts.isAdmin;
|
||||
@@ -196,7 +205,7 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane
|
||||
column = col.id;
|
||||
columnLabel = columnHead(col);
|
||||
columnName.textContent = columnLabel;
|
||||
number.textContent = numbers[col.id] ?? TEXT.noNumber;
|
||||
number.textContent = numbers[col.id]?.도번 || TEXT.noNumber;
|
||||
doc = null;
|
||||
basisDoc = null;
|
||||
basisNumber = "";
|
||||
@@ -217,9 +226,11 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane
|
||||
paintBody(mine);
|
||||
}
|
||||
|
||||
/** 글 열(NO · 측점 · 비고)은 구조물이 아니라 컨테이너를 닫음 */
|
||||
const show = (colId: string | null, columns: StructureColumn[]): void => {
|
||||
if (colId === column) return;
|
||||
void load(colId ? (columns.find((c) => c.id === colId) ?? null) : null);
|
||||
const col = columns.find((c) => c.id === colId && isStructureColumn(c)) ?? null;
|
||||
if ((col?.id ?? null) === column) return;
|
||||
void load(col);
|
||||
};
|
||||
|
||||
edit.addEventListener("click", async () => {
|
||||
@@ -238,7 +249,7 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane
|
||||
|
||||
return {
|
||||
root,
|
||||
numbers: () => numbers,
|
||||
numbers: () => Object.fromEntries(Object.entries(numbers).map(([id, n]) => [id, n.도번])),
|
||||
reload: async () => {
|
||||
numbers = await fetchStructureNumbers().catch(() => ({}));
|
||||
},
|
||||
|
||||
@@ -244,13 +244,13 @@ export function createMain(isAdmin: boolean, onSaved?: () => void): MainHandle {
|
||||
)
|
||||
: got.문서;
|
||||
if (mine !== seq) return;
|
||||
// 구조물 도면 · 산출근거는 열 id 로 열리니 머리에 도번 · 표번을 함께 보임
|
||||
// 구조물 도면 · 산출근거는 열 id 로 열리니 머리 맨 앞에 도번 · 표번(「SD010101 돌기슭막이 …」)
|
||||
if (next.kind === "structure") {
|
||||
const number = (got.문서 as StructureDoc)?.도번;
|
||||
if (number) title.textContent = `${number} · ${next.label ?? next.name}`;
|
||||
if (number) title.textContent = `${number} ${next.label ?? next.name}`;
|
||||
} else if (next.kind === "basis") {
|
||||
const number = (got.문서 as { 표번?: string })?.표번;
|
||||
if (number) title.textContent = `${number} · ${next.label ?? next.name}`;
|
||||
if (number) title.textContent = `${number} ${next.label ?? next.name}`;
|
||||
}
|
||||
await mount(shown, next);
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_UI_Side.ts
|
||||
* 마스터 템플릿 좌측 패널 — 시스템 양식 목록(표/도면/구조물 도면/산출근거) · 새로 · 본떠 · 지우기.
|
||||
* 서버 길은 계약 `6_계약.md`. 구조물 도면 · 산출근거 줄은 집계표 열마다 하나씩 —
|
||||
* 서버가 미리 만들어 둠(도번·표번 항상 있음 · 같은 번호 매기기 규칙).
|
||||
* 서버 길은 계약 `6_계약.md`. 구조물 도면 · 산출근거 줄은 집계표 구조물 열(꼴=수)마다 하나씩 —
|
||||
* 서버가 미리 만들어 둠(도번 SD · 표번 SC + 같은 6자리) · 줄 글은 번호가 맨 앞(「SD010101 돌기슭막이 …」).
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
@@ -18,16 +18,22 @@ import { t as L } from "@ui/ui_template_locale";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import {
|
||||
deleteTemplate,
|
||||
fetchBasisNumbers,
|
||||
fetchStructureNumbers,
|
||||
listTemplates,
|
||||
readTemplate,
|
||||
saveTemplate,
|
||||
StaleError,
|
||||
type Kind,
|
||||
type StructureNumbers,
|
||||
type TemplateInfo,
|
||||
} from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { STRUCTURE_TABLE, columnHead, type StructureColumn } from "./M02_MasterTemplete_Structure";
|
||||
import {
|
||||
STRUCTURE_TABLE,
|
||||
columnHead,
|
||||
isStructureColumn,
|
||||
structureLabel,
|
||||
type StructureColumn,
|
||||
} from "./M02_MasterTemplete_Structure";
|
||||
import { canEdit, type Selection } from "./M02_MasterTemplete_UI_Main";
|
||||
|
||||
const KINDS: { kind: Kind; label: () => string }[] = [
|
||||
@@ -83,8 +89,7 @@ export function buildSide(opt: SideOptions): SideHandle {
|
||||
const layer = "system" as const;
|
||||
let items: TemplateInfo[] = [];
|
||||
let columns: StructureColumn[] = [];
|
||||
let numbers: Record<string, string> = {};
|
||||
let basisNumbers: Record<string, string> = {};
|
||||
let numbers: Record<string, StructureNumbers> = {};
|
||||
let seq = 0;
|
||||
|
||||
let picked: { kind: Kind; name: string } | null = null;
|
||||
@@ -153,41 +158,27 @@ export function buildSide(opt: SideOptions): SideHandle {
|
||||
}
|
||||
}
|
||||
|
||||
/** 구조물 도면 줄 — 집계표 열마다 하나(서버가 미리 만들어 둬 도번은 항상 있음) */
|
||||
function paintStructure(): void {
|
||||
structureBody.replaceChildren(
|
||||
/** 구조물 도면 · 산출근거 줄 — 구조물 열마다 하나 · 「번호 이름 변수값」(번호가 없으면 「없음」) */
|
||||
function paintRows(body: HTMLElement, kind: "structure" | "basis"): void {
|
||||
body.replaceChildren(
|
||||
...(columns.length
|
||||
? columns.map((col) => {
|
||||
const num = numbers[col.id] ?? L("M02_StructureNone");
|
||||
const num =
|
||||
(kind === "structure" ? numbers[col.id]?.도번 : numbers[col.id]?.표번) ||
|
||||
L("M02_StructureNone");
|
||||
const b = el("button", {
|
||||
className: `m02-side__item ui-sidebar-row${isPicked("structure", col.id) ? " is-active" : ""}`,
|
||||
text: `${columnHead(col)} · ${num}`,
|
||||
className: `m02-side__item ui-sidebar-row${isPicked(kind, col.id) ? " is-active" : ""}`,
|
||||
text: structureLabel(num, col),
|
||||
attrs: { type: "button" },
|
||||
});
|
||||
b.addEventListener("click", () => void selectColumn(col));
|
||||
return b;
|
||||
})
|
||||
: [el("p", { className: "m02-side__empty", text: L("M02_NoTemplates") })]),
|
||||
);
|
||||
}
|
||||
|
||||
/** 산출근거 줄 — 구조물 도면과 같은 열 목록 · 표번은 도번과 같은 규칙(서버가 미리 만들어 둠) */
|
||||
function paintBasis(): void {
|
||||
basisBody.replaceChildren(
|
||||
...(columns.length
|
||||
? columns.map((col) => {
|
||||
const num = basisNumbers[col.id] ?? L("M02_StructureNone");
|
||||
const b = el("button", {
|
||||
className: `m02-side__item ui-sidebar-row${isPicked("basis", col.id) ? " is-active" : ""}`,
|
||||
text: `${columnHead(col)} · ${num}`,
|
||||
attrs: { type: "button" },
|
||||
});
|
||||
b.addEventListener("click", () => void selectBasisColumn(col));
|
||||
b.addEventListener("click", () => void selectColumn(col, kind));
|
||||
return b;
|
||||
})
|
||||
: [el("p", { className: "m02-side__empty", text: L("M02_NoTemplates") })]),
|
||||
);
|
||||
}
|
||||
const paintStructure = (): void => paintRows(structureBody, "structure");
|
||||
const paintBasis = (): void => paintRows(basisBody, "basis");
|
||||
|
||||
function paintButtons(): void {
|
||||
const editable = canEdit(layer, opt.isAdmin);
|
||||
@@ -204,25 +195,11 @@ export function buildSide(opt: SideOptions): SideHandle {
|
||||
paintButtons();
|
||||
}
|
||||
|
||||
async function selectColumn(col: StructureColumn): Promise<void> {
|
||||
if (!isPicked("structure", col.id) && !(await opt.confirmLeave())) return;
|
||||
async function selectColumn(col: StructureColumn, kind: "structure" | "basis"): Promise<void> {
|
||||
if (!isPicked(kind, col.id) && !(await opt.confirmLeave())) return;
|
||||
picked = null;
|
||||
opt.onOpen({
|
||||
layer,
|
||||
projectId: null,
|
||||
kind: "structure",
|
||||
name: col.id,
|
||||
label: columnHead(col),
|
||||
});
|
||||
paintStructure();
|
||||
paintBasis();
|
||||
paintButtons();
|
||||
}
|
||||
|
||||
async function selectBasisColumn(col: StructureColumn): Promise<void> {
|
||||
if (!isPicked("basis", col.id) && !(await opt.confirmLeave())) return;
|
||||
picked = null;
|
||||
opt.onOpen({ layer, projectId: null, kind: "basis", name: col.id, label: columnHead(col) });
|
||||
// 제목은 편집 화면이 문서 번호를 앞에 붙임(「SD010101 돌기슭막이 …」)
|
||||
opt.onOpen({ layer, projectId: null, kind, name: col.id, label: columnHead(col) });
|
||||
paintStructure();
|
||||
paintBasis();
|
||||
paintButtons();
|
||||
@@ -230,18 +207,16 @@ export function buildSide(opt: SideOptions): SideHandle {
|
||||
|
||||
async function refreshStructure(): Promise<void> {
|
||||
try {
|
||||
const [table, nums, basisNums] = await Promise.all([
|
||||
const [table, nums] = await Promise.all([
|
||||
readTemplate(layer, "table", STRUCTURE_TABLE, null),
|
||||
fetchStructureNumbers(),
|
||||
fetchBasisNumbers(),
|
||||
]);
|
||||
columns = ((table.문서 as { 열?: StructureColumn[] }).열 ?? []) as StructureColumn[];
|
||||
const all = ((table.문서 as { 열?: StructureColumn[] }).열 ?? []) as StructureColumn[];
|
||||
columns = all.filter(isStructureColumn);
|
||||
numbers = nums;
|
||||
basisNumbers = basisNums;
|
||||
} catch (error) {
|
||||
columns = [];
|
||||
numbers = {};
|
||||
basisNumbers = {};
|
||||
showToast(L("M02_LoadFailed").replace("{value}", why(error)), "error");
|
||||
}
|
||||
paintStructure();
|
||||
|
||||
Reference in New Issue
Block a user