From e4e2dc2aace9432bc5b1d956d5c63b94616c25b5 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 28 Sep 2026 22:24:08 +0900 Subject: [PATCH] =?UTF-8?q?=EF=BB=BFfix(M02):=20=EC=A1=B0=ED=95=A9=20?= =?UTF-8?q?=EA=B0=88=EB=9D=BC=20=EB=82=B4=EA=B8=B0=20=EA=B0=99=EC=9D=80=20?= =?UTF-8?q?=EB=82=B4=EC=9A=A9=20=ED=8C=90=EC=A0=95=EC=97=90=20=EC=B0=A8?= =?UTF-8?q?=EB=A1=80=20=C2=B7=20=EB=A9=94=EB=AA=A8=20=ED=8F=AC=ED=95=A8=20?= =?UTF-8?q?=C2=B7=20=EC=84=9C=EB=B2=84=20=EA=B1=B0=EC=A0=88=20=EA=B9=8C?= =?UTF-8?q?=EB=8B=AD=20=EA=B7=B8=EB=8C=80=EB=A1=9C=20=EC=95=8C=EB=A6=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 일위대가 조합: 담은 로직 차례 · 메모까지 같아야 있는 키(원래 키 먼저) — 차례 · 메모만 바꿔도 새 조합 - 자재 조합: 담은 자재 차례까지 비교 - M02 Api_Fetch: detail 문장 · errors 목록 · 입력 검사 목록을 알림 글로(HTTP 422 대신) - 새 조합 자동 이름: 원래 이름 끝 「 (n)」 뗌(겹친 (2) (2) 막음) - test_m02_combo_branch.py 새 규칙으로 고침 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019Kyw9qnkJ94pC1A4PfpDtA --- .../M02_MasterTemplete_Api_Fetch.ts | 18 ++++++++---- .../M02_MasterTemplete_Branch.py | 27 ++++++++++++----- .../M02_MasterTemplete_Combo.ts | 9 ++++-- resources/tester/test_m02_combo_branch.py | 29 ++++++++++++++----- 4 files changed, 60 insertions(+), 23 deletions(-) diff --git a/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts b/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts index dd9d713d..ee81f566 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts @@ -38,15 +38,21 @@ async function call(path: string, init: RequestInit = {}): Promise { ...init, }); const body = (await response.json().catch(() => ({}))) as { detail?: unknown } & T; - if (response.status === 409) - throw new StaleError(typeof body.detail === "string" ? body.detail : "409"); - if (!response.ok) { - const detail = body.detail; - throw new Error(typeof detail === "string" ? detail : `HTTP ${response.status}`); - } + if (response.status === 409) throw new StaleError(reason(body.detail, response.status)); + if (!response.ok) throw new Error(reason(body.detail, response.status)); return body; } +/** 서버가 막은 까닭 그대로 — 문장 · `{errors: [...]}` · 입력 검사 목록(`[{msg}]`) */ +function reason(detail: unknown, status: number): string { + if (typeof detail === "string") return detail; + const list = Array.isArray(detail) ? detail : (detail as { errors?: unknown } | null)?.errors; + const lines = Array.isArray(list) + ? list.map((x) => (typeof x === "string" ? x : String((x as { msg?: unknown })?.msg ?? ""))) + : []; + return lines.filter(Boolean).join(" · ") || `HTTP ${status}`; +} + const enc = encodeURIComponent; const q = (projectId: string | null): string => (projectId ? `?project_id=${enc(projectId)}` : ""); const head = (layer: Layer): string => diff --git a/M02_MasterTemplete/M02_MasterTemplete_Branch.py b/M02_MasterTemplete/M02_MasterTemplete_Branch.py index d708e92d..578cfccf 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Branch.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Branch.py @@ -3,8 +3,8 @@ · 일위대가 조합(UA · `일위대가조합.json`) — 새 줄은 M01 조합 저장소가 만듦(부르기만) · 자재 조합(UM · `자재조합.json`) — M01 마스터 저장(`save`)으로 더함(키 = 대장 다음 번호) -조합은 여러 열 · 모든 프로젝트가 같이 씀 — 제자리 고치지 않음. 담은 것의 집합(차례 · 메모 무시)이 -이미 있는 조합과 같으면 새로 안 만들고 그 키 · 새 줄의 본뜬키 = 원래 키 · 소유 현장. +조합은 여러 열 · 모든 프로젝트가 같이 씀 — 제자리 고치지 않음. 담은 것(차례 · 메모까지)이 +이미 있는 조합과 같으면 새로 안 만들고 그 키(원래 키 먼저) · 새 줄의 본뜬키 = 원래 키 · 소유 현장. """ from __future__ import annotations @@ -41,14 +41,14 @@ def _branch( field: str, file: str, slot: str, - ident: str, + ident: tuple[str, ...], items: list[dict], source: str, make: Callable[[str, list[dict]], dict], name: str, ) -> dict[str, Any]: """집계표 판 검사 → 같은 내용 찾기 · 없으면 `make(이름, 줄들)` 로 새 줄 → 집계표 그 열 `field`. - `slot` = 담은 칸(담은로직 · 담은자재) · `ident` = 담은 줄의 알맹이 칸(로직 · 코드).""" + `slot` = 담은 칸(담은로직 · 담은자재) · `ident` = 담은 줄에서 견주는 칸(로직 · 메모 / 코드).""" table = store.read("table", store.STRUCTURE_TABLE) if version and table["판"] != version: raise store.StoreError(409, {"stale": [store.STRUCTURE_TABLE], "판": table["판"]}) @@ -58,8 +58,10 @@ def _branch( if not isinstance(items, list) or not items: raise store.StoreError(400, "담은 것이 비었음") - def body(got) -> frozenset[str]: - return frozenset(str(x.get(ident) or "") for x in got or [] if isinstance(x, dict)) + def body(got) -> tuple: # 차례대로 · 빈 메모 = null + return tuple( + tuple(str(x.get(k) or "") for k in ident) for x in got or [] if isinstance(x, dict) + ) rows = _m01(m01.read, file)[0].get("줄") or [] same = [r for r in rows if body(r.get(slot)) == body(items)] @@ -102,7 +104,16 @@ def branch_combo( name = name.strip() or str(was.get("이름") or source) return _branch( - column, version, "일위대가", combo_store.FILE, "담은로직", "로직", items, source, make, name + column, + version, + "일위대가", + combo_store.FILE, + "담은로직", + ("로직", "메모"), + items, + source, + make, + name, ) @@ -147,5 +158,5 @@ def branch_material_combo( default = " ".join(h for h in store._heads(heads) if h) if heads else column name = name.strip() or str(was.get("이름") or default or column) return _branch( - column, version, "자재조합", MATERIAL_FILE, "담은자재", "코드", items, source, make, name + column, version, "자재조합", MATERIAL_FILE, "담은자재", ("코드",), items, source, make, name ) diff --git a/M02_MasterTemplete/M02_MasterTemplete_Combo.ts b/M02_MasterTemplete/M02_MasterTemplete_Combo.ts index 80dddf90..79dec703 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Combo.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Combo.ts @@ -116,8 +116,13 @@ export function askBranch(message: string, defaultName: string): Promise +/** 새 조합 이름 자동 — 원래 이름(끝 「 (n)」 뗌 · 서버가 새로 붙임) · 구조물 이름(한쪽이 다른 쪽을 + * 이미 품으면 긴 쪽 하나만) */ +export const branchName = ( + named: string, + label: string, + original = named.replace(/ \(\d+\)$/, ""), +): string => !original || label.includes(original) ? label || original : !label || original.includes(label) diff --git a/resources/tester/test_m02_combo_branch.py b/resources/tester/test_m02_combo_branch.py index 92ab78c1..47a31906 100644 --- a/resources/tester/test_m02_combo_branch.py +++ b/resources/tester/test_m02_combo_branch.py @@ -2,7 +2,8 @@ M02 견본 표(`write_sample_table`) + M01 마스터 사본(조합 줄은 비워 시작)으로만 봄. · 일위대가(UA) — 새로 만듦(본뜬키 = 원래 키) · 이 열만 새 키 · 다른 열은 옛 키 · 원래 조합 그대로 · - SS 장부 맞춤 · 담은 로직 집합(차례 · 메모 무시)이 같으면 있는 키 · 또 고치면 또 새로 · 같은 이름 (2) + SS 장부 맞춤 · 담은 로직(차례 · 메모까지)이 같으면 있는 키(되돌리면 원래 키) · 차례 · 메모만 + 바꿔도 새로 · 또 고치면 또 새로 · 같은 이름 (2) · 자재 조합(UM) — 같은 규칙 · 처음 만드는 열(본뜬키 없음) · 읽기 · 집계표 판 어긋남 = 409 · 조합 안 만듦 · 분류 더하기로 복사한 열(`본뜬열`) — [저장] 때 원본 키 · 세 파일 내용을 새 번호로 받음 @@ -75,7 +76,9 @@ def _source(client) -> str: def _branch(client, column, source, keys, name="", 판="", want=200): - body = {"본뜬키": source, "담은로직": [{"로직": k} for k in keys], "이름": name, "판": 판} + """`keys` = 로직 키 또는 (로직, 메모)""" + lines = [{"로직": k[0], "메모": k[1]} if isinstance(k, tuple) else {"로직": k} for k in keys] + body = {"본뜬키": source, "담은로직": lines, "이름": name, "판": 판} res = client.post(f"/api/m02/structures/{column}/combo", json=body) assert res.status_code == want, res.text return res.json() @@ -119,17 +122,27 @@ def test_새로_만듦_이_열만_새_키_원래_조합_그대로(client): assert combo_store.combo(got["키"])["combo"]["본뜬키"] == ua -def test_같은_로직_집합이면_있는_키_또_고치면_또_새로_같은_이름은_2(client): +def test_차례_메모까지_같으면_있는_키_또_고치면_또_새로_같은_이름은_2(client): ua = _source(client) - same = _branch(client, "c1", ua, [B, A]) # 차례만 바꿈 = 원래 조합 + same = _branch(client, "c1", ua, [(A, ""), (B, None)]) # 빈 메모 = null → 원래 조합 assert (same["키"], same["새로만듦"]) == (ua, False) + order = _branch(client, "c1", ua, [B, A]) # 차례만 바꿈 = 새로 + assert order["새로만듦"] is True and order["키"] != ua + got = combo_store.combo(order["키"])["combo"] + assert ([x["로직"] for x in got["담은로직"]], got["본뜬키"]) == ([B, A], ua) + memo = _branch(client, "c1", order["키"], [B, (A, "사면 쪽")]) # 메모만 바꿈 = 또 새로 + assert memo["새로만듦"] is True and memo["키"] not in (ua, order["키"]) + assert combo_store.combo(memo["키"])["combo"]["담은로직"][1]["메모"] == "사면 쪽" + back = _branch(client, "c1", memo["키"], [A, B]) # 원래와 똑같이 되돌림 = 원래 키 + assert (back["키"], back["새로만듦"]) == (ua, False) + assert _cols(client)["c1"] == ua and _ss(client)["c1"] == ua first = _branch(client, "rv", ua, [A, B, C], "갈래") again = _branch(client, "rv", first["키"], [A, C], "갈래") # 갈라 낸 것을 또 고침 = 또 새로 assert again["새로만듦"] is True and again["키"] not in (ua, first["키"]) assert again["이름"] == "갈래 (2)" assert combo_store.combo(again["키"])["combo"]["본뜬키"] == first["키"] - back = _branch(client, "c1", ua, [C, A, B]) # 다른 열이 같게 고침 = 있는 키 - assert (back["키"], back["새로만듦"]) == (first["키"], False) + twin = _branch(client, "c1", ua, [A, B, C]) # 다른 열이 똑같이 고침 = 있는 키 + assert (twin["키"], twin["새로만듦"]) == (first["키"], False) assert _cols(client)["c1"] == first["키"] and _ss(client)["c1"] == first["키"] @@ -157,8 +170,10 @@ def test_자재_조합_처음_만듦_같은_내용이면_있는_키_고치면_ row = client.get(f"/api/m02/material-combos/{first['키']}").json() assert tuple(row) == ("키", "이름", "담은자재", "본뜬키", "소유", "비고") assert (row["본뜬키"], row["소유"], row["담은자재"]) == (None, "현장", [M1, M2]) - same = _material(client, "c1", "", [M2, M1]) # 차례만 다름 = 있는 키 + same = _material(client, "c1", "", [M1, M2]) # 같은 차례 = 있는 키 assert (same["키"], same["새로만듦"]) == (first["키"], False) + order = _material(client, "c1", first["키"], [M2, M1], "모래 먼저") # 차례만 다름 = 새로 + assert order["새로만듦"] is True and order["키"] != first["키"] fork = _material(client, "c1", first["키"], [M1], "시멘트만") assert fork["새로만듦"] is True and fork["키"] != first["키"] assert client.get(f"/api/m02/material-combos/{fork['키']}").json()["본뜬키"] == first["키"]