Merge remote-tracking branch 'origin/dev' into sub_laptop_3
This commit is contained in:
@@ -27,6 +27,13 @@ KINDS = layers.KINDS # 시험은 FOLDER 를 사본으로 바꿈
|
||||
# (제목행 = 열 `머리` 네 층 · 없는 층 00) — 한 열의 세 번호는 같은 8자리 ·
|
||||
# 한 번 준 번호는 파일에 남아 안 바뀜(PLAN 13장)
|
||||
STRUCTURE_TABLE = "구조물집계표"
|
||||
# 프로그램이 이름으로 부르는 양식 — 지우면 그 기능이 죽음(B07 이름은 `B07_DesignDetail_Engine_*`)
|
||||
IN_USE = {
|
||||
("table", STRUCTURE_TABLE): "구조물 도면 · 면적산출 · 재료산출 · 조합 장부가 씀",
|
||||
("drawing", "00_template_A1"): "B07 도면 도각",
|
||||
("drawing", "00_template_compass"): "B07 방위표",
|
||||
("drawing", "00_template_cover"): "B07 표지",
|
||||
}
|
||||
# (종류, 문서 번호 칸, 머리 글자, 시트 이름) — 시트 이름이 None 이면 구조물 도면
|
||||
PARTS = (
|
||||
("structure", "도번", "SD", None),
|
||||
@@ -138,6 +145,8 @@ def write(kind: str, name: str, version: str, doc: Any) -> dict[str, Any]:
|
||||
|
||||
def delete(kind: str, name: str, version: str | None = None) -> None:
|
||||
path = _path(kind, name)
|
||||
if (kind, name) in IN_USE:
|
||||
raise StoreError(409, f"쓰는 양식이라 못 지움 「{name}」 — {IN_USE[(kind, name)]}")
|
||||
with _LOCK:
|
||||
if not path.is_file():
|
||||
raise StoreError(404, f"없는 양식 「{name}」")
|
||||
@@ -201,13 +210,19 @@ def _column_ids() -> list[str]:
|
||||
return [col["id"] for col in _structure_columns()]
|
||||
|
||||
|
||||
def _pick_code(col: dict[str, Any], taken: dict[str, str], heads: dict[str, tuple]) -> str:
|
||||
def _pick_code(
|
||||
col: dict[str, Any],
|
||||
taken: dict[str, str],
|
||||
heads: dict[str, tuple],
|
||||
retired: list[str] | None = None,
|
||||
) -> str:
|
||||
"""새 열 번호 — 층마다 같은 무리(그 층까지 제목행이 같은 열)가 이미 번호를 가졌으면 그 두 자리를
|
||||
이어 받고 · 없으면 그 층 가장 큰 번호 + 1 · 빈 층(종류 밖)은 00 · 맨 아래 층(분류3)은 늘 다음
|
||||
번호. 같은 8자리가 이미 있으면(같은 제목행 · 빈 분류3 겹침) 맨 아래 층을 그 무리 다음 번호로.
|
||||
`taken` = 이미 번호 있는 열 `{열 id: 8자리}` · `heads` = 열 id → 제목행."""
|
||||
`taken` = 이미 번호 있는 열 `{열 id: 8자리}` · `heads` = 열 id → 제목행 ·
|
||||
`retired` = 지운 열의 번호(다시 안 줌 — 「가장 큰 번호」 · 겹침 셈에만 듦)."""
|
||||
parts = _heads(col)
|
||||
codes = list(taken.values())
|
||||
codes = [*taken.values(), *(retired or [])]
|
||||
mates = [(heads[cid], code) for cid, code in taken.items() if cid in heads]
|
||||
|
||||
def next_of(prefix: str) -> str:
|
||||
@@ -237,7 +252,13 @@ def _new_code(column: str) -> str:
|
||||
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})
|
||||
return _pick_code(col, taken, {c["id"]: _heads(c) for c in cols}, _retired())
|
||||
|
||||
|
||||
def _retired() -> list[str]:
|
||||
"""지운 열의 8자리 — 조합 장부 `지운번호`(한 번 준 번호는 다시 안 줌)."""
|
||||
path = FOLDER / COMBO_FILE
|
||||
return json.loads(path.read_bytes()).get("지운번호", []) if path.is_file() else []
|
||||
|
||||
|
||||
def _blank(kind: str, column: str) -> dict[str, Any]:
|
||||
@@ -333,12 +354,20 @@ def prune_removed_structures() -> list[str]:
|
||||
번호는 비워 둠(다른 열 번호를 안 당김)."""
|
||||
keep = set(_column_ids())
|
||||
gone: list[str] = []
|
||||
codes: set[str] = set()
|
||||
with _LOCK:
|
||||
for kind in _KEY:
|
||||
for kind, key, _, _ in PARTS:
|
||||
for path in sorted((FOLDER / kind).glob("[!.]*.json")):
|
||||
if path.stem not in keep:
|
||||
code = _code(json.loads(path.read_bytes()), key)
|
||||
codes.update([code] if code else [])
|
||||
path.unlink()
|
||||
gone.append(f"{kind}/{path.stem}")
|
||||
if codes: # 지운 번호는 장부에 남겨 새 열이 다시 안 받게
|
||||
book = FOLDER / COMBO_FILE
|
||||
doc = json.loads(book.read_bytes()) if book.is_file() else {}
|
||||
doc["지운번호"] = sorted(codes | set(doc.get("지운번호", [])))
|
||||
atomic_write_json(book, doc)
|
||||
return gone
|
||||
|
||||
|
||||
@@ -378,9 +407,10 @@ def renumber() -> dict[str, list[str]]:
|
||||
cols = _structure_columns()
|
||||
heads = {c["id"]: _heads(c) for c in cols}
|
||||
taken = _codes()
|
||||
retired = _retired()
|
||||
for col in cols:
|
||||
if col["id"] not in taken:
|
||||
taken[col["id"]] = _pick_code(col, taken, heads)
|
||||
taken[col["id"]] = _pick_code(col, taken, heads, retired)
|
||||
docs = {kind: dict(_docs(kind)) for kind in _KEY}
|
||||
for col in cols:
|
||||
cid = col["id"]
|
||||
@@ -458,7 +488,10 @@ def sync_combos() -> dict[str, Any]:
|
||||
keys[col["id"]] = f"{_COMBO_HEAD}{after:06d}"
|
||||
after += 1
|
||||
rows.append(_combo_row(keys[col["id"]], col, numbers.get(col["id"], {})))
|
||||
doc = {"그룹": "구조물집계표조합", "판": 1, "비고": _COMBO_NOTE, "다음": after, "줄": rows}
|
||||
doc = {"그룹": "구조물집계표조합", "판": 1, "비고": _COMBO_NOTE, "다음": after}
|
||||
if old.get("지운번호"):
|
||||
doc["지운번호"] = old["지운번호"] # 지운 열의 도번 8자리 — 다시 안 줌
|
||||
doc["줄"] = rows
|
||||
if doc != old:
|
||||
atomic_write_json(path, doc)
|
||||
return doc
|
||||
|
||||
@@ -272,8 +272,10 @@ export function buildSide(opt: SideOptions): SideHandle {
|
||||
ok.disabled = false;
|
||||
}
|
||||
});
|
||||
name.input.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter" && !event.isComposing) ok.click();
|
||||
});
|
||||
body.append(name.root, ok);
|
||||
name.input.focus();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -86,6 +86,22 @@ def test_copy_is_independent_and_seeds_new_project(client, tmp_path, monkeypatch
|
||||
assert [r["이름"] for r in client.get("/api/m02/templates").json()] == ["원본"]
|
||||
|
||||
|
||||
def test_in_use_templates_not_deleted(client):
|
||||
"""프로그램이 이름으로 부르는 양식은 못 지움 — 이름은 B07 엔진 상수와 같음."""
|
||||
from B07_DesignDetail import B07_DesignDetail_Engine_Cad_Cover as cover
|
||||
from B07_DesignDetail import B07_DesignDetail_Engine_Template as tpl
|
||||
|
||||
names = {tpl.A1_TEMPLATE, tpl.COMPASS_TEMPLATE, cover.COVER_TEMPLATE}
|
||||
assert {n for k, n in store.IN_USE if k == "drawing"} == names
|
||||
doc = {"format": 6, "entities": [{"type": "line"}]}
|
||||
for kind, name in store.IN_USE:
|
||||
body = doc if kind == "drawing" else {"열": []}
|
||||
client.put(f"/api/m02/templates/{kind}/{name}", json={"판": "", "문서": body})
|
||||
got = client.delete(f"/api/m02/templates/{kind}/{name}")
|
||||
assert got.status_code == 409 and "쓰는 양식" in got.json()["detail"]
|
||||
assert client.get(f"/api/m02/templates/{kind}/{name}").status_code == 200
|
||||
|
||||
|
||||
def test_empty_doc_rejected_and_file_untouched(client):
|
||||
good = {"format": 6, "entities": [{"type": "line"}]}
|
||||
made = client.put("/api/m02/templates/drawing/도", json={"판": "", "문서": good})
|
||||
|
||||
@@ -187,6 +187,21 @@ def test_집계표_저장_때_새_열은_만들고_지운_열은_같이_지움(c
|
||||
assert numbers["pv_y"] == _num("02010101") # pv_b 무리 안 분류3 다음 번호
|
||||
|
||||
|
||||
def test_지운_열_번호는_다음_저장에도_다시_안_줌(client):
|
||||
"""무리 맨 끝 열(pv_jt 02010400)을 지워 저장한 뒤 새 열을 더해도 04 를 다시 안 받음."""
|
||||
store.renumber()
|
||||
url = "/api/m02/templates/table/구조물집계표"
|
||||
got = client.get(url).json()
|
||||
got["문서"]["열"] = [c for c in got["문서"]["열"] if c["id"] != "pv_jt"]
|
||||
assert client.put(url, json={"판": got["판"], "문서": got["문서"]}).status_code == 200
|
||||
assert client.get("/api/m02/structure-combos").json()["지운번호"] == ["02010400"]
|
||||
got = client.get(url).json()
|
||||
new = {"id": "pv_z", "머리": ["콘크리트포장", "T=(두께마다)", "Z", None], "꼴": "수"}
|
||||
got["문서"]["열"].append(new)
|
||||
assert client.put(url, json={"판": got["판"], "문서": got["문서"]}).status_code == 200
|
||||
assert client.get("/api/m02/structures/numbers").json()["pv_z"] == _num("02010500")
|
||||
|
||||
|
||||
def test_다른_열_도면을_복사해_가져옴(client):
|
||||
store.renumber()
|
||||
src = client.get("/api/m02/templates/structure/rv").json()
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
.ui-sheet__hint {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
color: var(--color-text-secondary); /* muted 는 작은 글에 대비 3:1 — 모자람 */
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
deleteColumn,
|
||||
deleteRow,
|
||||
isBlankPick,
|
||||
isBoundColumn,
|
||||
isCalcColumn,
|
||||
KEY_PRICE,
|
||||
KEY_UNIT,
|
||||
navRows,
|
||||
@@ -168,7 +170,8 @@ export function createSheet(host: HTMLElement, input: SheetDoc, opts: SheetOptio
|
||||
const col = state.sel ? colOf(state.sel.c) : undefined;
|
||||
if (opts.mode === "master") syncLevels(!!col);
|
||||
colDelete.disabled = !col || state.doc.열.length <= 1;
|
||||
colHand.disabled = !col;
|
||||
// 바인딩 · 계산 열은 손 입력이 안 먹음(프로젝트에서 잠김) — 켜지 못하게 · 이미 켜진 것은 끌 수 있게
|
||||
colHand.disabled = !col || (!col.손 && (isBoundColumn(col) || isCalcColumn(col)));
|
||||
colHand.classList.toggle("is-on", !!col?.손);
|
||||
};
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ export function openModal(opts: ModalOptions): { close: () => void } {
|
||||
opts.mount(body, close);
|
||||
document.body.append(backdrop);
|
||||
dialog.tabIndex = -1;
|
||||
dialog.focus();
|
||||
// 몸의 첫 입력 칸에 커서 — mount 안의 focus() 는 붙기 전이라 안 먹음
|
||||
(body.querySelector<HTMLElement>("input, textarea, select") ?? dialog).focus();
|
||||
return { close };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user