fix(M02 실무자료): 361-1 지역만 다른 자재 = 전국 — 줄 글 지역 · (전국) 품목 · 없으면 키 없는 전국 자리표 · 고르기 창 지역 변형 접음 · 전국 되돌림 · 만들기 본뜬.전국자재

- 맞춤: 줄 글 지역 = 그 지역 변형(꼬리표 「지역 — 줄 글」) · 지역 없는 변형((전국) 품목 먼저) · 없으면(레미콘) 「전국」 자리표(바탕 이름(전국)-관급/사급 · 규격 · 단위 · 키 · 단가 없음 · 꼬리표 「전국 — 지역은 프로젝트에서」) · 옛 「못 가림 — N개 중 첫째」 없앰
- 후보 = 그 품목 변형 모두(`변형`) · 고르기 창 1위 / 전국 / 지역 변형(접힘) · 대상 꼴 `{전국: true}`
- 미리 셈 · 만들기: 전국 대상은 서버가 줄에서 다시 셈 · 일위대가 자재 칸이 M01 키만 받아 사전조합 `본뜬.전국자재` 에 적고 빠진 줄 까닭 · 장부엔 안 남김 · 만들기 몸 꼴에 `전국` 칸
- 시험: 전국 · 줄 글 지역 · (전국) 품목 · 고르기 지역 → 전국 되돌림 · 만들기 결과 · 화면 도우미

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte
This commit is contained in:
2026-10-16 23:03:56 +09:00
co-authored by Claude Opus 5.5
parent 8799cf0c97
commit a2c2ad92de
14 changed files with 266 additions and 32 deletions
@@ -148,6 +148,7 @@ let out0 = null;
toTarget: [
check.toTarget({ 호표: 46, 이름: "고화매트", 입력값: {}, 배: 2.3 }),
check.toTarget({ 자재: "MT1", 이름: "레미콘", 입력값: {}, 배: 1, 몫: null }),
check.toTarget({ 전국: true, 이름: "레미콘(전국)-관급", 입력값: {}, 배: 1, 몫: null }),
],
newest: [a(), b(), c()],
dropped: (check.dropDraft(key), store.has(key)),
@@ -200,6 +201,25 @@ let out0 = null;
material: choose
.candGroups({ 후보: [{ 자재: "M1" }, { 자재: "M2" }] }, true)
.map((x) => x.제목),
// 361-1 지역만 다른 자재 — 1위 전국 · 지역 변형 접힘 · 고르면 그 지역 · 전국 되돌림
national: (() => {
const all = [
{ 전국: true, 이름: "레미콘(전국)-관급", 규격: "25-21-120", 단위: "㎥" },
{ 자재: "M1", 이름: "레미콘(서울)-사급", 변형: true },
{ 자재: "M2", 이름: "레미콘(관급)-강원도(강릉시)", 변형: true },
];
const g = choose.candGroups({ 후보: all }, true);
const id = (c) => c.자재 ?? (c.전국 ? "전국" : "");
return {
groups: g.map((x) => [x.제목, x.접힘, x.후보.map(id)]),
text: choose.candText(all[0], new Set()),
region: choose.pickedTargets([{ c: all[2], 몫: null }]),
back: choose.pickedTargets([{ c: all[0], 몫: null }]),
hit: choose
.candGroups({ 후보: [all[2], all[0], all[1]] }, true)
.map((x) => [x.제목, x.후보.map(id)]),
};
})(),
pick: [
choose.canPickMaterial({ 갈래: "단가산출", 단가: { 재료비: 10 } }),
choose.canPickMaterial({ 갈래: "단가산출", 단가: { 재료비: null } }),
+28
View File
@@ -146,6 +146,34 @@ def test_자재_세_칸_로직_자재_두_번_막음(client):
assert any(m["까닭"] == hb.TWICE and m["차례"] == 0 for m in got["빠진줄"])
def test_전국_자재_키_없이_본뜬에(client):
"""361-1 — 「전국」 자재 = 자재 칸(M01 키만) 아니고 `본뜬.전국자재` · 이름 · 규격 · 단위 =
서버가 줄에서 다시 셈(몸은 이름 칸 못 받음) · 빠진 줄 까닭 · 장부엔 안 남김 · 자리표 없는 줄 = 400."""
assert _post(client).status_code == 200
line = {"차례": 0, "대상": [{"전국": True, "배": 1}]} # 레미콘 별도계상 줄
got = _build(client, 1, [line])
mine = layers.personal_dir(7, 42)
doc = _doc(mine, got["참조"]["키"])
assert doc["자재"] == [] and not any(m.get("코드") for m in doc["자재"])
qty = hs.read(mine, "HP-000001")["호표"][0]["줄"][0]["수량"]
assert doc["본뜬"]["전국자재"] == [
{
"이름": "레미콘(전국)-관급",
"규격": "25-21-120",
"단위": "㎥",
"수량": {"손": qty},
"별도계상": "별도계상",
"자재구분": "관급",
}
]
assert [m["까닭"] for m in got["빠진줄"] if m["차례"] == 0] == [hb.NATIONAL]
assert got["장부"]["씀"] == 0
_build(client, 1, [{"차례": 1, "대상": [{"전국": True, "배": 1}]}], want=400) # 로직 줄
both = [{"차례": 0, "대상": [{"전국": True, "자재": "MT004258", "배": 1}]}]
_build(client, 1, both, want=400)
_build(client, 1, [{"차례": 0, "대상": [{"전국": True, "입력값": {"x": 1}}]}], want=400)
def test_몸_없음_재검사_400(client):
assert _post(client).status_code == 200
one = {"로직": "GF000101", "배": 1}
+3 -1
View File
@@ -92,7 +92,9 @@ def test_끝까지_확인한_줄만_별칭(client):
assert {p["로직"] for p in two} == {"GC000206", "GC000207"} # 다른 점 → 사용자가 체크
body = body + [{"차례": rebar["차례"], "대상": two}]
made = _build(client, "HP-000001", no, body)
assert made["다시씀"] is False and made["장부"]["씀"] == len(body)
national = [x for x in body if any(t.get("전국") for t in x.get("대상", []))] # 361-1
assert national and all(x["대상"] == [{"전국": True, "배": 1}] for x in national)
assert made["다시씀"] is False and made["장부"]["씀"] == len(body) - len(national)
assert all(m["까닭"] for m in made["빠진줄"]) # 확인 안 함 · % 줄
doc = _doc(made["참조"])
logics = [x for x in doc["담은로직"] if x.get("로직")]
+73 -4
View File
@@ -115,11 +115,25 @@ def test_새_상태_체크_제안(client):
assert remicon["품"]["같음"]["콘크리트공"] and remicon["품"]["엑셀"]["인력"]["콘크리트공"]
plywood = first["합판거푸집"]
assert plywood["상태"] == "맞음" and set(plywood["갈리는칸"]) == {"측압", "폼타이규격"}
mat = first["레미콘(25-21-120)"] # 358-2 — 「별도계상」 글 = 표시 · 지역 변형 = 첫째 + 꼬리표
mat = first["레미콘(25-21-120)"] # 358-2 · 361-1 — 「별도계상」 표시 · 지역 변형만 = 전국
assert mat["상태"] == "맞음" and mat["체크"] is True and "별도계상" in mat["꼬리표"]
assert mat["대상"][0]["규격"].endswith("25-21-120") and "레미콘" in mat["대상"][0]["이름"]
assert any(t.startswith("지역 · 관급/사급 못 가림") for t in mat["꼬리표"])
assert all(c["규격"].endswith("25-21-120") for c in mat["후보"]) # 변형 = 고를 수 있게
assert mat["대상"][0] | {"환산": None} == {
"전국": True,
"이름": "레미콘(전국)-관급",
"규격": "25-21-120",
"단위": "㎥",
"입력값": {},
"못읽음": [],
"출처": {},
"환산": None,
"몫": None,
"배": 1,
} # 특정 지역 키 없음
assert "전국 — 지역은 프로젝트에서" in mat["꼬리표"]
assert not any("못 가림" in t for t in mat["꼬리표"])
assert mat["후보"][0]["전국"] and len(mat["후보"]) == 129 # 전국 + 지역 변형 128(접어 고름)
assert all(c["변형"] and c["자재"] for c in mat["후보"][1:])
assert all(c["규격"].endswith("25-21-120") for c in mat["후보"])
dig = _hopyo(client, 3)["구조물터파기(인력100%)"]
assert dig["상태"] == "다른 점" and dig["체크"] is False
assert dig["대상"][0]["로직"] == "GC000083" and len(dig["후보"]) == 5
@@ -330,6 +344,61 @@ def test_북부청_자재_별도계상_맞음_늚(client):
assert mortar["후보"][0]["자재"] == "MT004251"
# ── 지역만 다른 자재 = 전국(361-1) ─────────────────────────────────────
REMICON = {"이름": "레미콘(25-21-120)", "규격": "별도계상", "단위": "㎥", "갈래": "별도계상"}
H_BEAM = {"이름": "H 형강", "규격": "SS275, 100*100*6*8mm", "단위": "Ton", "갈래": "자재"}
def test_전국_지역_줄글_전국품목():
from M02_MasterTemplete import M02_Hopyo_Material as hmat
rows, tags = hmat.sure(hmat.clean(REMICON)[0]) # 지역 낱말 없음 · (전국) 품목 없음
assert rows[0]["one"] == {
"전국": True,
"이름": "레미콘(전국)-관급",
"규격": "25-21-120",
"단위": "㎥",
}
assert tags == [hmat.NATIONAL_TAG] and all("자재" in r["one"] for r in rows[1:])
paid = {**REMICON, "규격": "", "갈래": "자재", "단가": {"재료비": 90000}}
assert hmat.national(paid)["이름"] == "레미콘(전국)-사급" # 재료비 줄 = 사급 꼬리
named = {**REMICON, "비고": "강원도 강릉시"} # 줄 글에 지역 = 그 지역(원문)
rows, tags = hmat.sure(hmat.clean(named)[0])
assert rows[0]["one"]["이름"] == "레미콘(관급)-강원도(강릉시)" and tags == [hmat.REGION_TAG]
assert hmat.national(named) is None
rows, tags = hmat.sure(hmat.clean(H_BEAM)[0]) # (전국) 품목 있음 = 그것
assert rows[0]["one"]["자재"] == "MT001694" and tags == []
assert hmat.national(H_BEAM) is None
seoul = hmat.sure(hmat.clean({**H_BEAM, "비고": "서울"})[0])[0][0]["one"]
assert seoul["이름"] == "H 형강(서울)"
def test_전국_고르기_지역_되돌림(client):
_ready(client)
no, at = (
1,
next(
x["차례"]
for x in client.get(f"{URL.format('personal')}/HP-000001/1").json()["호표"]["줄"]
if x["이름"] == "레미콘(25-21-120)"
),
)
path = f"{URL.format('personal')}/HP-000001/{no}/guess"
seoul = client.post(path, json={"차례": at, "대상": [{"자재": "MT004258", "배": 1}]})
assert seoul.status_code == 200 and seoul.json()["대상"][0]["자재"] == "MT004258"
back = client.post(path, json={"차례": at, "대상": [{"전국": True, "배": 1}]})
assert back.status_code == 200, back.text
got = back.json()["대상"][0]
assert got["전국"] and "자재" not in got and got["이름"] == "레미콘(전국)-관급"
other = next( # 전국 자리표 없는 줄(로직 줄) = 400
x["차례"]
for x in client.get(f"{URL.format('personal')}/HP-000001/1").json()["호표"]["줄"]
if x["이름"] == "레미콘타설(장비사용)"
)
bad = client.post(path, json={"차례": other, "대상": [{"전국": True, "배": 1}]})
assert bad.status_code == 400
# ── 단계 대조 보임(358-3) — 목록 수 = 확인 창 수 · 연결 두 칸 · 목록 열 때 뒤 셈 ─────────────
def test_목록_수_확인_창_수_한_규칙(client):
_ready(client)
+15 -1
View File
@@ -129,7 +129,11 @@ def test_확인_창_체크한_줄만_몸():
assert got["sure"] == [1, 2] # [맞음 · 별칭 일괄 체크]
assert got["compare"] == "콘크리트공 0.1091 → 0.1091 ✓ · 굴착기 0.29 → 0.31 ≠"
assert got["compareMany"] == "굴착기 0.1594 → 0.0889 ≠" # 같은 장비 여러 줄 = 합쳐 한 줄
assert got["toTarget"] == [{"호표": 46, "배": 2.3}, {"자재": "MT1", "배": 1}]
assert got["toTarget"] == [
{"호표": 46, "배": 2.3},
{"자재": "MT1", "배": 1},
{"전국": True, "배": 1}, # 361-1 — 키 없음 · 이름은 서버가
]
assert got["newest"] == [False, True, True] # 같은 줄 늦은 응답 버림 · 다른 줄은 따로
assert got["dropped"] is False # 만들기 성공 = 캐시 지움
@@ -145,6 +149,16 @@ def test_고르기_창_도우미():
]
assert got["groups"]["text"][:2] == ["되메우기 · A · 산림", "되메우기 · C · 건설"]
assert got["groups"]["material"] == ["1위", "그 밖 후보"]
nat = got["groups"]["national"] # 361-1
assert nat["groups"] == [["1위", False, ["전국"]], ["지역 변형", True, ["M1", "M2"]]]
assert nat["text"] == "레미콘(전국)-관급 25-21-120 [㎥] · 키 없음"
assert nat["region"] == [{"자재": "M2", "입력값": {}, "배": 1, "몫": None}]
assert nat["back"] == [{"전국": True, "입력값": {}, "배": 1, "몫": None}]
assert nat["hit"] == [ # 줄 글 지역 = 1위 · 전국 = 되돌릴 자리 · 남은 변형 접힘
["1위", ["M2"]],
["전국", ["전국"]],
["지역 변형", ["M1"]],
]
assert got["groups"]["pick"] == [True, False, True] # 재료비 있는 단가산출 줄도 자재 검색
assert got["picked"] == [
{"로직": "GC000206", "입력값": {}, "배": 1, "몫": None},