fix(M02): 조합 갈라 내기 같은 내용 판정에 차례 · 메모 포함 · 서버 거절 까닭 그대로 알림
- 일위대가 조합: 담은 로직 차례 · 메모까지 같아야 있는 키(원래 키 먼저) — 차례 · 메모만 바꿔도 새 조합 - 자재 조합: 담은 자재 차례까지 비교 - M02 Api_Fetch: detail 문장 · errors 목록 · 입력 검사 목록을 알림 글로(HTTP 422 대신) - 새 조합 자동 이름: 원래 이름 끝 「 (n)」 뗌(겹친 (2) (2) 막음) - test_m02_combo_branch.py 새 규칙으로 고침 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Kyw9qnkJ94pC1A4PfpDtA
This commit is contained in:
@@ -38,15 +38,21 @@ async function call<T>(path: string, init: RequestInit = {}): Promise<T> {
|
||||
...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 =>
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -116,8 +116,13 @@ export function askBranch(message: string, defaultName: string): Promise<string
|
||||
});
|
||||
}
|
||||
|
||||
/** 새 조합 이름 자동 — 원래 이름 · 구조물 이름(한쪽이 다른 쪽을 이미 품으면 긴 쪽 하나만) */
|
||||
export const branchName = (original: string, label: string): string =>
|
||||
/** 새 조합 이름 자동 — 원래 이름(끝 「 (n)」 뗌 · 서버가 새로 붙임) · 구조물 이름(한쪽이 다른 쪽을
|
||||
* 이미 품으면 긴 쪽 하나만) */
|
||||
export const branchName = (
|
||||
named: string,
|
||||
label: string,
|
||||
original = named.replace(/ \(\d+\)$/, ""),
|
||||
): string =>
|
||||
!original || label.includes(original)
|
||||
? label || original
|
||||
: !label || original.includes(label)
|
||||
|
||||
@@ -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["키"]
|
||||
|
||||
Reference in New Issue
Block a user