From 16de9eed8bccd27d006709ae8a660c4e4dc35fc1 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 28 Sep 2026 21:56:51 +0900 Subject: [PATCH] =?UTF-8?q?fix(M02):=2016=EC=9E=A5=20=EC=A0=90=EA=B2=80=20?= =?UTF-8?q?=E2=80=94=20=EC=A7=80=EC=9A=B4=20=EC=97=B4=20=EB=B2=88=ED=98=B8?= =?UTF-8?q?=20=EC=9E=AC=EC=82=AC=EC=9A=A9=20=EB=A7=89=EC=9D=8C=20=C2=B7=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EC=B0=BD=20=EC=BB=A4=EC=84=9C=20=C2=B7=20?= =?UTF-8?q?Enter=20=EB=A7=8C=EB=93=A4=EA=B8=B0=20=C2=B7=20=EB=B0=94?= =?UTF-8?q?=EC=9D=B8=EB=94=A9=20=EC=97=B4=20=EC=86=90=20=EC=9E=85=EB=A0=A5?= =?UTF-8?q?=20=EB=A7=89=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 지운 구조물 열의 도번 8자리를 조합 장부 지운번호에 남겨 새 열이 다시 안 받음(다음 저장에서도) - 공용 창 틀: 몸의 첫 입력 칸에 커서(붙기 전 focus 가 안 먹던 것) - 좌측 새로 · 본떠 만들기 이름 창에서 Enter = 만들기 - 표: 바인딩 · 계산 열은 손 입력 열 단추 꺼짐(켜도 프로젝트에서 잠김) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011vX53nJGC8ucBRFJns2n7B --- .../M02_MasterTemplete_Store.py | 38 +++++++++++++++---- .../M02_MasterTemplete_UI_Side.ts | 4 +- resources/tester/test_m02_structure.py | 15 ++++++++ ui_template/sheet/ui_template_sheet.ts | 5 ++- ui_template/ui_template_modal.ts | 3 +- 5 files changed, 55 insertions(+), 10 deletions(-) diff --git a/M02_MasterTemplete/M02_MasterTemplete_Store.py b/M02_MasterTemplete/M02_MasterTemplete_Store.py index 30ded48a..04fe316d 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Store.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Store.py @@ -210,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: @@ -246,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]: @@ -342,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 @@ -387,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"] @@ -467,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 diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts index e1bbec82..cffc043f 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts @@ -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(); }, }); }; diff --git a/resources/tester/test_m02_structure.py b/resources/tester/test_m02_structure.py index 14d93e01..7989dcbd 100644 --- a/resources/tester/test_m02_structure.py +++ b/resources/tester/test_m02_structure.py @@ -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() diff --git a/ui_template/sheet/ui_template_sheet.ts b/ui_template/sheet/ui_template_sheet.ts index 0dace05e..086840de 100644 --- a/ui_template/sheet/ui_template_sheet.ts +++ b/ui_template/sheet/ui_template_sheet.ts @@ -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?.손); }; diff --git a/ui_template/ui_template_modal.ts b/ui_template/ui_template_modal.ts index ee92cc82..da32f088 100644 --- a/ui_template/ui_template_modal.ts +++ b/ui_template/ui_template_modal.ts @@ -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("input, textarea, select") ?? dialog).focus(); return { close }; }