feat(793-3b): 불러오기 단산 줄 = 가져온 식 단가산출 모달 — 블록 → 가져온 식 SK 길 · 검산 · 엑셀/우리 단가 · 이음 고르기 · 근거 세 단 · 우리 기초로 바꾸기 · 입력 필요 · 줄 잇기 · 결과 칩

- 서버: 올림 때 산근 시트 둠(실무자료/산근/<HP>.json · HP 지울 때 같이) · POST …/sangeun/from-hopyo(블록 → 793-2a 내보내기 → 793-2 만들기 · 같은 내용 = 그 SK 다시 씀 · 옛 HP 409) · STmate 끝전 = 엑셀 계와 맞는 쪽
- 화면: [수정] 모달 단산 줄 · 오른쪽 블록 판 [가져온 식으로 열기](새) → 시트 모달 + 머리 판 · [이 줄에 잇기] = 줄 고른 것 SK(입력 필요 남으면 확인 막힘) · 단산 줄 결과 칩
- 시험: test_793_3b_import_screen 6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-05 18:09:00 +09:00
co-authored by Claude Opus 5.5
parent 2a2822d650
commit ca72718e26
12 changed files with 981 additions and 3 deletions
@@ -0,0 +1,146 @@
# -*- coding: utf-8 -*-
"""793-3b 지킴 — 불러오기 단산 줄 = 가져온 식 단가산출 모달(요구 646 · 647 · 652 갈래 C).
- 서버 길 `…/sangeun/from-hopyo` — 올림 때 산근 시트 둠(`실무자료/산근/<HP>.json`) →
블록 → 793-2a 내보내기 → 793-2 가져온 식 SK(만들 층 · 엑셀 단가 그대로 · 검산 = 엑셀) ·
같은 내용 = 그 SK 다시 씀 · 산근 없는 옛 HP = 409 · HP 지우면 산근도 지움 ·
STmate 블록 끝전 = 엑셀 계와 맞는 쪽
- 화면 소스 — [수정] 모달 단산 줄 · 오른쪽 블록 판 [가져온 식으로 열기] · 시트 모달
(`Sangeun_Detail`) · 머리 판(검산 · [우리 단가로] / [엑셀 단가로] · [원본 글 보기] ·
[이음 고르기] · 근거 세 단 · [우리 기초로 바꾸기] · 입력 필요 붉은 칸) ·
[이 줄에 잇기] = 줄 고른 것 SK · 결과 칩 · 「새」 배지 · 부품 상자만 · 700 줄
임시 storage(`client` 마당) · M01 원본 그대로.
"""
import re
from pathlib import Path
import helper_m02_hopyo_golden as golden
import pytest
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_Sangeun_FromHopyo as fh
from M02_MasterTemplete import M02_Sangeun_Router as sangeun_router
from M02_MasterTemplete import M02_Template_Layers as layers
# ruff: noqa: F811
from resources.tester.test_m02_hopyo_excel import _post, client, master_hash # noqa: F401
ROOT = Path(__file__).resolve().parents[2]
M02 = ROOT / "M02_MasterTemplete"
API = "/api/m02/layers/personal"
KEY = "HP-000001"
MINE = ("Hopyo_Imported", "Hopyo_ImportedPanel", "Hopyo_Check", "Hopyo_CheckRows", "Hopyo_Block")
def _src(name: str) -> str:
return (M02 / f"M02_MasterTemplete_{name}.ts").read_text(encoding="utf-8")
def _skcount() -> int:
folder = layers.personal_dir(7, 42) / "sangeun"
return len(list(folder.glob("*.json"))) if folder.exists() else 0
def _open(client, no=18):
body = {"층": "personal", "키": KEY, "번": no}
return client.post(f"{API}/sangeun/from-hopyo", json=body)
# ── 서버 길 ───────────────────────────────────────────────────────────
def test_단산_블록_가져온_식_만들기_다시씀_지움(client):
client.app.include_router(sangeun_router.router)
before = master_hash()
assert _post(client).status_code == 200
folder = layers.personal_dir(7, 42)
assert fh.has(folder, KEY)
made = _open(client)
assert made.status_code == 200, made.text
got = made.json()
assert got["이름"].startswith("SK-") and got["다시씀"] is False
assert got["단가기준"] == "엑셀" and got["검산"]["같음"] is True
assert round(got["검산"]["셈"]["계"]) == 30369 # 북부청임도 단산18 문양거푸집 = 엑셀
n = _skcount()
again = _open(client).json()
assert again["이름"] == got["이름"] and again["다시씀"] is True and _skcount() == n
with store.at(folder):
doc = store.read("sangeun", got["이름"])["문서"]
assert doc["바탕"] is None and doc["가져온"]["원본"]["엑셀금액"]["계"] == 30369
assert _open(client, 99999).status_code == 404
fh._path(folder, KEY).unlink() # 산근 없는 옛 HP = 다시 올림
old = _open(client)
assert old.status_code == 409 and old.json()["detail"] == fh.RELOAD
assert _post(client).status_code == 200 and fh.has(
folder, KEY
) # 같은 엑셀 다시 올림 = 산근 채움
assert client.delete(f"{API}/hopyo/{KEY}").status_code == 200
assert not fh.has(folder, KEY) # HP 지우면 산근도
assert master_hash() == before
RULES = [("봉화", "D00102", "비목버림"), ("동부청영월", "D00003", "합버림_큰나머지")]
@pytest.mark.parametrize(("tag", "code", "rule"), RULES)
def test_STmate_끝전_엑셀_계와_맞는_쪽(tag, code, rule):
from M02_MasterTemplete import M02_San_Book as book
from M02_MasterTemplete import M02_San_Stmate as stm
if not golden.FILES[tag].exists():
pytest.skip(f"실무 엑셀 없음 {tag}")
data = book.load(golden.FILES[tag])
blk = next(b for b in data["블록"] if b["코드"] == code)
assert fh._rule(blk, stm.parse_block(blk["몸"]), data["단가"]) == rule
# ── 화면 소스 ─────────────────────────────────────────────────────────
def test_단산_줄_길_잇기():
check, rows, block = _src("Hopyo_Check"), _src("Hopyo_CheckRows"), _src("Hopyo_Block")
assert "openImported({" in check and "onLink: (p) => kit.edit(row, [p])" in check
assert "imported }" in check.split("const ctx = ")[1].split(";")[0]
assert 'L("M02_CheckImported")' in rows and "row.번 !== undefined && ctx.imported" in rows
assert 'import("./M02_MasterTemplete_Hopyo_Imported")' in block # 돌림 import 피함
assert 'L("M02_CheckImported")' in block and "dataset.checkNew" in block
imp = _src("Hopyo_Imported")
assert '"/sangeun/from-hopyo"' in imp and "/sangeun/imported/${how}" in imp
assert "mountSangeunDetail(" in imp and "saveTemplate(" in imp and "openModal({" in imp
assert "skPick(sk" in imp and "o.onLink" in imp # [이 줄에 잇기] = 줄 고른 것 SK
for how in ('run("price"', 'run("link"', 'run("to-base"'):
assert how in imp, how
locale = (ROOT / "ui_template/ui_template_locale_m2_judge.ts").read_text(encoding="utf-8")
assert 'M02_CheckImported: ["가져온 식으로 열기"' in locale
def test_머리_판_결과_칩():
panel = _src("Hopyo_ImportedPanel")
for word in (
"ToOurs",
"ToExcel",
"Source",
"Link",
"ToBase",
"PickBase",
"Need",
"Same",
"Diff",
):
assert f'tx("{word}")' in panel, word
assert "newBadge()" in panel and "data-basis-tiers" in panel and "TIERS.map(" in panel
assert 'f.setError(tx("Need"))' in panel # 입력 필요 = 붉은 칸
model = (M02 / "logic/M02_Hopyo_Imported_Model.ts").read_text(encoding="utf-8")
assert '["확정", "근접", "사람"]' in model and "필요: need" in model
assert "입력 필요 ${need.length}" in model and '"가져온 식 있음"' in model
rows = _src("Hopyo_CheckRows")
assert "rowChips(ctx.kit.picksOf(row))" in rows and "dataset.judgeResult" in rows
assert not re.search(r"from \"@ui|document\.|fetch\(|sessionStorage\.", model) # 순수
def test_부품상자만_700줄_공용_안_고침():
for name in MINE:
text = _src(name)
assert len(text.splitlines()) <= 700, name
assert not re.search(r"""\bel\(\s*["'](button|input|select)["']""", text), name
assert not re.search(r"""createElement\(\s*["'](button|input|select)["']""", text), name
assert "#" not in "".join(re.findall(r"color:\s*[^;]+", text)), name
for name in ("Labor_Rows", "Labor_SangeunModal", "Sangeun_Detail", "Sangeun_Sheet"):
assert "Hopyo_Imported" not in _src(name), name # 공용 쪽은 새 모달을 모름