Files
Aislo/resources/tester/test_790_1_sangeun_sheet.py
eomsangdonandClaude Opus 5.5 2e7328d3a5 feat(794-1): M01 관리자 줄 식 시트 — 로직 상세 첫 보기 「시트」 · 식 칸 칠 때마다 검사 · 기존 [저장] 길
- 로직 상세 보기 첫 칸 「시트」(기본) — 이름 | 식 | 값 | 비목 · 근거 · 옵션(시험 값) · 변수 · 계산 · 자원 · 율 · 덧줄 · 단계 · 끝수 · 소계 · 합계 · 상태줄 지금 · 고치기 전
- 식 글 = 이름 꼴(M02 단가산출 시트와 같은 번역 · 새 서버 M01_MasterData_Sheet.py · POST /api/m01/logic/sheet · /logic/sheet/check · 저장 때와 같은 엔진 검사 · 쓰기 없음)
- 식 고침 = 시스템 관리자만 · 되는 식만 줄 사본에 · 캐시 → 기존 [저장](개정 확인 · 서버 검사 422) · [원래로]
- 식 칸 몸 = 공용 A00_Common/sheet_formula_cell.ts — M02 단가산출 시트도 같은 것을 씀(동작 그대로)
- 자체 로직(GX) 편집기 식 날 입력칸 → 읽기만(시트 식 칸으로 갈음)
- 시험: test_794_1_m01_sheet 새로 · test_790_1 식 칸 문구 자리를 공용 파일로 옮김(식 칸 몸이 공용으로 감) · test_717_2b · test_722 LogicLab.ts 줄 상한 627 → 637(시트 이음 10 줄 · 셈 · 고침은 시트 파일로 뺌 · 700 아래)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 18:45:48 +09:00

313 lines
16 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""790-1 M02 단가산출 줄 식 시트(E1) — 상세 첫 탭 「시트」 · 줄 갈래 · 산출서 보기 · 부품 상자만.
- 정적: 시트 화면 = 부품 상자(calc_row 시트 줄 · 입력 · 고르기 · 단추 · 배지 · 글)만 · 날 button · input 0 ·
상세 탭 등록(시트 첫 칸 · 옛 보기 넷 그대로) · 고침 = SK 덮개 · 뺀줄 · 되살림 길(모델 안 바꿈)
- 순수 셈(node strip-types): 줄 갈래(근거 · 옵션 · 상수 · 표 · 계산 · 자원 · 율 · 소계 · 합계) · 엔진 날식 숨김 ·
읽기 쉬운 식 · 산출서 한 줄 글 · 상태줄 금액 셋(790-2 `원래` 있으면 그 값)
"""
import json
import re
import subprocess
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
M02 = ROOT / "M02_MasterTemplete"
SHEET = M02 / "M02_MasterTemplete_Sangeun_Sheet.ts"
MODEL = M02 / "logic" / "M02_Sangeun_Sheet_Model.ts"
DETAIL = M02 / "M02_MasterTemplete_Sangeun_Detail.ts"
ROW = ROOT / "ui_template" / "ui_template_calc_row.ts"
IMPORTS = re.compile(r"^import[^;]*;\r?\n", re.MULTILINE)
def _text(p: Path) -> str:
return p.read_text("utf-8")
def test_부품_상자만_날_요소_없음():
for p in (SHEET, MODEL, SHEET.with_suffix(".css")):
s = _text(p)
assert "<button" not in s and "<input" not in s and "<table" not in s
assert "document.createElement" not in s and "sessionStorage." not in s
assert len(s.splitlines()) < 700
sheet = _text(SHEET)
for part in ("createCalcSheetRow", "createInputField", "createSelectField", "createButton"):
assert f"{part}" in sheet
assert "fetch(" not in _text(MODEL) and "document." not in _text(MODEL)
css = _text(SHEET.with_suffix(".css"))
assert not re.search(r"#[0-9a-fA-F]{3,6}\b|rgb\(", css) # 색 = 공통 값만
row = _text(ROW)
assert "export function createCalcSheetRow(" in row and "ui-calc-row--sheet" in row
def test_상세_탭_등록_시트_첫_칸():
detail = _text(DETAIL)
assert "createSangeunSheet(editor," in detail and "createViewPicker" not in detail
assert "children: [sheet.picker, addButton, rateButton]" in detail
assert re.search(r"sheet\.host,\s+editor,", detail) # 시트 그릇 = 옛 보기 그릇 앞
assert (
"sheet.draw({ doc, ans, bare, base: row, text, rows, view, values, removed, fail });"
in detail
)
assert "sheet.saved();" in detail
assert len(detail.splitlines()) <= 575
sheet = _text(SHEET)
opts = re.search(r"options: \[\s*\{ value: \"sheet\"", sheet)
assert opts, "시트가 보기 고르기 첫 칸"
for v in ('"ho"', '"est"', '"calc"', '"formula"', '"paper"'):
assert f"value: {v}" in sheet # 옛 보기 넷 그대로 · 산출서 전환
# 고침 길 = 지금 SK 모델(덮개 · 뺀줄 · 되살림) · 다시 셈 = 상세 touch(서버 셈 · 최신 응답만)
for call in (
"setLineCover(d.doc, r,",
"dropLine(d.doc, r)",
"restoreLine(d.doc, cid)",
"opt.touch()",
):
assert call in sheet
assert (
"addLine: () => addButton.click()" in detail
and "addRate: () => rateButton.click()" in detail
)
FIX = {
"base": {
"출처": "건설표준품셈 공통 6-3-1",
"원문번호": "6-3-1",
"비고": "※ 높이 3.5 m 이하",
"입력": [{"이름": "사용횟수", "고르기": ["3회", "6회"]}],
"중간": [
{"이름": "상수", "식": "17"},
{"이름": "시공량", "식": "찾기(QC000431, 유형=유형).시공량"},
{"이름": "형틀목공", "식": "찾기(QC000431, 유형=유형).형틀목공 / 시공량"},
{"이름": "합판율", "식": "만약(사용횟수 == '1회', 1, 찾기(QC000428, 사용횟수=사용횟수).합판 / 100)"},
],
},
"values": {"사용횟수": "6회"},
"ans": {
"기준": {"노무비": 38290, "재료비": 12747, "경비": 382, "계": 51419},
"중간": {"상수": 17, "시공량": 45, "형틀목공": 0.1111, "합판율": 0.327},
"원래": None,
},
"rows": [
{"cid": "labor:LB000007#0", "이름": "형틀목공", "단위": "인", "수량": 0.1111, "단가": 275790,
"금액": 30643.3, "parts": {"노무비": 30643.3}, "pct": False, "added": None, "apart": False,
"covered": {"qty": False, "price": False}, "off": False},
{"cid": "material:폼타이#0", "이름": "폼타이", "단위": "본", "수량": 0, "단가": 0, "금액": 0,
"parts": {"재료비": 0}, "pct": False, "added": None, "apart": False,
"covered": {"qty": False, "price": False}, "off": True},
{"cid": "add:#0", "이름": "보통인부", "단위": "인", "수량": 0.1, "단가": 172068, "금액": 17206.8,
"parts": {"노무비": 17206.8}, "pct": False, "added": 0, "apart": False,
"covered": {"qty": False, "price": False}, "off": False},
{"cid": "pct:공구손료#0", "이름": "공구손료", "단위": "%", "수량": 2, "단가": None, "금액": 765.8,
"parts": {"경비": 765.8}, "pct": True, "added": None, "apart": False,
"covered": {"qty": True, "price": False}, "off": False},
],
"text": {
"ok": True,
"계": 51419,
"별칭": [{"이름": "합판거푸집일당", "원문": "찾기(QC000431, 유형=유형)"}],
"groups": [
{"비목": "노무비", "소계": 38290, "끝수": "", "줄": [
{"이름": "형틀목공", "cid": "labor:LB000007#0", "글": "275,790 * 0.1111 = 30,643.3",
"이름글": "형틀목공노임 * 형틀목공 = 형틀목공비", "금액": 30643.3},
{"이름": "폼타이", "cid": "material:폼타이#0", "글": "0 * 0 = 0", "금액": 0, "옵션꺼짐": True},
{"이름": "보통인부", "cid": "add:#0", "글": "", "금액": 17206.8},
]},
{"비목": "경비", "소계": 765, "끝수": "", "줄": [
{"이름": "공구손료", "cid": "pct:공구손료#0", "글": "(30,643.3) * 2 % = 765.8",
"이름글": "(형틀목공 * 형틀목공노임) * 2 % = 공구손료", "금액": 765.8},
]},
],
},
} # fmt: skip
def _run():
src = IMPORTS.sub("", _text(MODEL))
body = (
f"\nconst F = {json.dumps(FIX, ensure_ascii=False)};"
"\nconst items = sheetItems({ base: F.base, inputs: F.base.입력, values: F.values, ans: F.ans,"
" rows: F.rows, text: F.text });"
"\nconst out = {"
"\n items: items.map((x) => ({ kind: x.kind, 이름: x.이름, 식: x.식, 값: x.값, 비목: x.비목,"
" 표: x.표, changed: x.changed })),"
"\n paper: paperLines(F.text, F.rows, F.ans),"
"\n status: statusOf(F.ans, { 기준: { 계: 51000 } }, 50000),"
"\n origin: statusOf({ ...F.ans, 원래: { 계: 49000 } }, { 기준: { 계: 51000 } }, null),"
"\n diff: [diffText(51802, 51419), diffText(51000, 51419), diffText(1, 1)],"
"\n};"
"\nconsole.log(JSON.stringify(out));"
)
result = subprocess.run(
["node", "--experimental-strip-types", "--no-warnings",
"--input-type=module-typescript", "-"],
input=src + body, capture_output=True, text=True, encoding="utf-8", timeout=60,
) # fmt: skip
assert result.returncode == 0, result.stderr
return json.loads(result.stdout)
def test_줄_갈래_읽기_쉬운_식_산출서_상태줄():
got = _run()
kinds = [x["kind"] for x in got["items"]]
assert kinds == ["근거", "근거", "옵션", "상수", "표", "계산", "계산", "자원", "자원", "율",
"소계", "소계", "소계", "합계"] # fmt: skip
by = {(x["kind"], x["이름"]): x for x in got["items"]}
assert by[("근거", "6-3-1")]["식"] == "건설표준품셈 공통 6-3-1"
assert by[("표", "시공량")]["값"] == 45 and by[("표", "시공량")]["표"] == ["QC000431"]
assert by[("계산", "형틀목공")]["식"] == "합판거푸집일당.형틀목공 / 시공량"
assert by[("계산", "합판율")]["식"] == "" and by[("계산", "합판율")]["표"] == [
"QC000428"
] # 날식 숨김
res = by[("자원", "형틀목공 (인)")]
assert res["식"] == "형틀목공노임 × 형틀목공" and res["비목"] == "노" and not res["changed"]
assert ("자원", "폼타이 (본)") not in by # 옵션으로 빠진 줄 안 그림
assert by[("자원", "보통인부 (인)")]["changed"] # 더한줄 = M01 과 다름
assert by[("자원", "보통인부 (인)")]["식"] == "172,068 × 0.1" # 이름글 없음 = 단가 × 품
rate = by[("율", "공구손료")]
assert (
rate["식"] == "(형틀목공 × 형틀목공노임) × 2 %" and rate["changed"] and rate["비목"] == "경"
)
assert by[("합계", "계")]["값"] == 51419
paper = [p["글"] for p in got["paper"]]
assert "형틀목공 : 275,790 × 0.1111 = 30,643.3" in paper
assert "보통인부 : 172,068 × 0.1 = 17,206.8" in paper # 글 없음 = 표 줄로 지음
assert not any(p.startswith("폼타이") for p in paper)
assert "노무비 소계 = 38,290" in paper and paper[-1] == "계 = 51,419"
assert got["status"] == {"원래": 51000, "지금": 51419, "전": 50000} # 원래 = 바탕 셈 계
assert got["origin"]["원래"] == 49000 # 790-2 `원래` 있으면 그 값
assert got["diff"] == ["+383", "−419", ""]
# ── 790-3 E2 화면 이음(식 속 상수 · 표값 · 글 줄 · 까닭 · M01 원래로 · M01 바뀜) ──────────
EDIT = M02 / "M02_MasterTemplete_Sangeun_SheetEdit.ts"
def test_790_3_숫자_넓힘_칸_정적():
s = _text(EDIT)
assert "<button" not in s and "<input" not in s and "document.createElement" not in s
assert "fetch(" not in s and "sessionStorage." not in s and len(s.splitlines()) < 700
for call in (
"setConst(c.doc, spot, p.n, x)", # 식 속 숫자 = 상수 덮개
"setTable(c.doc, spot, p.n, x)", # 표 꼬리표 값 = 표값 덮개
'createBadge(t("Cut"), "warning")', # 표 연결 끊김
"addText(c.doc, spot,", # 글 줄 더함 · 고침 · 지움
"editText(c.doc, i, v)",
"dropText(c.doc, i)",
"setReason(c.doc, key, x)", # 까닭
"restoreM01(c.doc, spot)", # 줄마다 M01 원래로
"restoreM01(c.doc)", # 전체
'createBadge(t("Changed"), "danger")', # M01 바뀜
"c.spots?.짝잃음",
):
assert call in s, call
sheet = _text(SHEET)
for call in (
"formulaEditor(ctx, it.자리)",
"reasonCell(ctx, it.자리, it.changed)",
"spotTail(ctx, it.자리,",
"restoreAll(ctx)",
"lostBanner(ctx)",
"removedReasons(ctx, d.removed)",
"dropAddKeys(d.doc as ConstDoc, r.added)", # 뺀 더한줄 = 까닭 · 글줄 열쇠 당김
"liftAddKeys(d.doc as ConstDoc, at)", # 되살림 = 거꾸로 밂
"paperLines(d.text, d.rows, d.ans, (d.doc as ConstDoc).글줄)", # 글 줄 산출서에
"spots: spotsOf(d.ans)",
):
assert call in sheet, call
assert "Object.assign(doc, next);" in sheet # 상세가 쥔 같은 문서 객체 · 자동저장 없음
assert len(sheet.splitlines()) < 700
SPOT = {
"식": "찾기(QC000431, 유형=유형).시공량 * (1 - (높이감률 + 만약(지붕슬래브 == '아님', 0, 17)) / 100)",
"상수": [{"n": 0, "값": 1, "원값": 1}, {"n": 1, "값": 0, "원값": 0},
{"n": 2, "값": 17, "원값": 17}, {"n": 3, "값": 100, "원값": 100}],
"표값": [{"n": 0, "글": "찾기(QC000431, 유형=유형).시공량", "값": 45, "원값": 45}],
} # fmt: skip
def _run_e2():
src = IMPORTS.sub("", _text(MODEL))
bad = {**SPOT, "상수": SPOT["상수"][:3]}
body = (
f"\nconst S = {json.dumps(SPOT, ensure_ascii=False)};"
f"\nconst B = {json.dumps(bad, ensure_ascii=False)};"
f"\nconst F = {json.dumps(FIX, ensure_ascii=False)};"
"\nconst show = (p) => p && p.map((x) => x.t === '글' ? x.글 : x.t === '수' ? `[#${x.n}]` : `[표${x.n} ${x.표}]`).join('');"
"\nconst notes = [{ 자리: '', 글: '맨 위 글' }, { 자리: 'labor:LB000007#0', 글: '형틀 뒤 글' },"
" { 자리: 'material:폼타이#0', 글: '꺼진 줄 뒤' }, { 자리: 'labor:LB000007#0', 글: '' }];"
"\nconsole.log(JSON.stringify({"
"\n parts: show(formulaParts(S)),"
"\n bad: formulaParts(B),"
"\n none: formulaParts({ 식: '형틀목공', 상수: [], 표값: [] }),"
"\n spots: numSpots(\"만약(a == '3회', LB000007 * 2.5, 줄.'합판 12㎜' / 10)\"),"
"\n paper: paperLines(F.text, F.rows, F.ans, notes).map((p) => p.kind + ':' + p.글),"
"\n lift: liftAddKeys({ 까닭: { 'add:#0': 'a', 'add:#1/x#0': 'b', 'mid:q': 'c' },"
" 글줄: [{ 자리: 'add:#2', 글: 'g' }] }, 1),"
"\n spotsNone: spotsOf(null),"
"\n}));"
) # fmt: skip
result = subprocess.run(
["node", "--experimental-strip-types", "--no-warnings",
"--input-type=module-typescript", "-"],
input=src + body, capture_output=True, text=True, encoding="utf-8", timeout=60,
) # fmt: skip
assert result.returncode == 0, result.stderr
return json.loads(result.stdout)
def test_790_3_식_조각_글줄_산출서_열쇠_밂():
got = _run_e2()
assert got["parts"] == (
"[표0 QC000431] × ([#0] - (높이감률 + 만약(지붕슬래브 == '아님', [#1], [#2])) / [#3])"
) # 서버 scan 차례 그대로 · 표 찾기 = 한 칸
assert got["bad"] is None and got["none"] is None # 차례가 안 맞으면 숫자 칸 안 그림
assert len(got["spots"]) == 2 # 2.5 · 10 만 — 따옴표 글(3회 · 12㎜) · 이름(LB000007) 안 숫자 뺌
paper = got["paper"]
assert paper[0] == "글:맨 위 글" and paper[1] == "글:꺼진 줄 뒤" # 맨 위 · 안 그린 줄 = 위로
i = paper.index("줄:형틀목공 : 275,790 × 0.1111 = 30,643.3")
assert paper[i + 1] == "글:형틀 뒤 글" and "글:" not in paper[i + 2] # 빈 글 안 찍음
assert got["lift"]["까닭"] == {"add:#0": "a", "add:#2/x#0": "b", "mid:q": "c"}
assert got["lift"]["글줄"] == [{"자리": "add:#3", "글": "g"}]
assert got["spotsNone"] is None
# ── 792 식 칸 화면(줄 식 고침 · 칠 때마다 검사 · 변수 줄) ────────────────────────────
FX = M02 / "M02_MasterTemplete_Sangeun_SheetFormula.ts"
CELL = ROOT / "A00_Common" / "sheet_formula_cell.ts" # 794-1 식 칸 몸 = 공용(M01 시트와 한 벌)
def test_792_식_칸_정적():
s = _text(FX)
assert "<button" not in s and "<input" not in s and "document.createElement" not in s
assert "sessionStorage." not in s and len(s.splitlines()) < 700
both = s + _text(CELL)
for call in (
"/sangeun/formula-check", # 검사 길(셈 · 저장 없음)
"ctl?.abort();", # 칠 때마다 · 옛 요청 끊기
"signal,",
'box.input.addEventListener("input"',
'box.setError(got.되나 ? undefined : got.까닭.join(" · "))', # 빨간 칸 + 까닭
"if (!got?.되나 || !got.식) return", # 틀린 식은 문서에 안 넣음
"setFormula(doc, spot, engine)", # 엔진 식을 문서에
"clearNums(c.doc as FormulaDoc, spot)", # 같은 자리 상수 · 표값 · 품 덮개 걷음
"addVar(c.doc as FormulaDoc,",
"editVar(c.doc as FormulaDoc, v.이름, { 이름: to })",
"dropVar(c.doc as FormulaDoc, v.이름)",
):
assert call in both, call
sheet = _text(SHEET)
for call in (
"rowFormula(ctx, it.자리,",
"formulaToggle(ctx, it.자리,",
"varRows(ctx, (v) => v.자리 === it.자리)",
"addVarButton(ctx)",
"scope: opt.scope,",
'coverKinds(d.doc as ConstDoc, r.cid).includes("식")', # 식 덮개 = 품 칸 안 둠
):
assert call in sheet, call
assert "addRate: () => rateButton.click(), scope }" in _text(DETAIL) # 탭 등록 줄에 층만
assert "fetch(" not in s # 길 = Api_Fetch.call(로그인 쿠키)