feat(799-2): M01 로직 상세 · M02 단가산출 상세 옛 탭 넷(시트 · 인력/재료/경비 · 시험 계산 · 텍스트 수식) 걷음 — 보기는 수식 · 단가산출 상세 · 셈 구동 따로
- 셈 구동 떼기: 새 M01_MasterData_UI_Logic_Drive.ts(driveRun · previewText · autoFill · createDrive) — M01 상세는 숨은 시험 계산 칸 단추 대신 구동 함수 · 시험 계산 칸(Logic_Calc)은 로직 복사 화면이 쓰므로 남기고 같은 구동을 부름 · 재료 고름은 rerun 고리 - M02: 새 _Sangeun_Tabs.ts(수식 · 단가산출 상세 보기 고르기) · Detail 의 시험 계산 · 인력/재료/경비 걷음 - 걷은 파일(git rm): M01 LogicLab_Sheet · Logic_Sheet_Model · LogicLab_Formula(.ts .css) · FormulaTokens · LogicLab_Est / M02 Sangeun_Sheet(.ts .css) · SheetEdit · SheetFormula · logic Sheet_Model · Const_Model · Formula_Model · 부품 시트 줄(createCalcSheetRow) · Cover_Model.sheetGroups · Calc 의 M02 고리(answer · shown · held) · 죽은 CSS · 글 - 서버 길 · 문서 칸(변수줄 · 글줄 · 덮개)은 남김 — 금액 그대로 - 낡은 시험: test_790_1(시트 화면 전부) · test_714_3 · test_748_2(+ helper)는 화면이 없어져 지움 · test_794_1 · test_790_2 는 화면 · TS 거울만 덜고 서버 시험 남김 · test_714_2 · 717_2b · 721 · 722(원래 틀린 한 줄 고침) · 734 · 737 · 750_2 · 793_3b · 796_2 · 796_2b 는 보기 둘 · 새 파일에 맞춤 · 새 test_799_2_old_tabs_gone Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,20 +0,0 @@
|
||||
/* PLAN 748-2 헬퍼 — 텍스트 수식 조각 나누기(`M01_MasterData_UI_LogicLab_FormulaTokens.ts` 순수 셈).
|
||||
* 짝 = `test_748_2_formula_tokens.py`. 결과 = 표준출력 JSON.
|
||||
*/
|
||||
const path = require("path");
|
||||
const { makeLoader } = require("./helper_ts_stub_loader.cjs");
|
||||
|
||||
const { load } = makeLoader({ root: path.join(__dirname, "..", "..") });
|
||||
const m = load("M01_MasterData", "M01_MasterData_UI_LogicLab_FormulaTokens.ts");
|
||||
const hot = (list) => list.filter((t) => t.hot).map((t) => t.text);
|
||||
const name = "굴착기노무비단가 * 뒤채움및다짐소형장비.굴착기 * 8 / 시공량 = 굴착기노무비";
|
||||
const value = "35,412 * 1 * 8 / 110 = 2,575";
|
||||
const out = {
|
||||
nameHot: hot(m.nameTokens(name)),
|
||||
nameAll: m.nameTokens(name).map((t) => t.text),
|
||||
fnCall: hot(m.nameTokens("올림(단가) + 1.5 - 보정")),
|
||||
valueHot: hot(m.valueTokens(value)),
|
||||
valueAll: m.valueTokens("13,873 = 13,873 · 21 %").map((t) => t.text),
|
||||
empty: [m.nameTokens("").length, m.valueTokens("").length],
|
||||
};
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -1,6 +1,6 @@
|
||||
"""714-2 공용 단가산출 계산서 지킴 시험 — 글 · 순수 함수(화면 · 서버 없음).
|
||||
|
||||
- 704-2 M02 계산서 보기 · M01 「인력·재료·경비」 보기가 한 벌(`A00_Common/calc_sheet.ts`)을 씀
|
||||
- 704-2 M02 계산서 보기 · B09 단가산출근거가 한 벌(`A00_Common/calc_sheet.ts`)을 씀(799-2 — M01 「인력·재료·경비」 보기는 걷음)
|
||||
- 계산서는 부품 상자만(날 단추 · 입력 · 표 없음) · 화면에서 다시 셈 안 함(값 = 서버)
|
||||
"""
|
||||
|
||||
@@ -21,10 +21,9 @@ def _text(p: Path) -> str:
|
||||
|
||||
def test_한_벌을_씀():
|
||||
m02 = _text(ROOT / "M02_MasterTemplete" / "M02_MasterTemplete_Labor_Sheet.ts")
|
||||
est = _text(ROOT / "M01_MasterData" / "M01_MasterData_UI_LogicLab_Est.ts")
|
||||
detail = _text(ROOT / "M01_MasterData" / "M01_MasterData_UI_LogicLab_Detail.ts")
|
||||
b09 = _text(ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Sangeun_Sheet.ts")
|
||||
assert "calcSheetRows(" in m02 and "createCalcFormulaRow" not in m02
|
||||
assert "calcSheetRows(" in est and "buildEst(" in detail
|
||||
assert "calcSheetRows(" in b09
|
||||
|
||||
|
||||
def test_부품_상자만():
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
"""714-3 텍스트 수식 입력 단추 지킴 시험 — 줄이 쓰는 입력 고르기(순수 함수 · node) + 이음 글.
|
||||
|
||||
`M01_MasterData_UI_LogicLab_Formula.ts` 「순수 셈」 구간만 떼어 돌림(화면 · 서버 없음).
|
||||
"""
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
M01 = Path(__file__).resolve().parents[2] / "M01_MasterData"
|
||||
FX = M01 / "M01_MasterData_UI_LogicLab_Formula.ts"
|
||||
|
||||
pytestmark = pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
||||
|
||||
|
||||
def _run(body: str) -> object:
|
||||
text = FX.read_text(encoding="utf-8")
|
||||
pure = text[text.index("/* ── 순수 셈: 줄이 쓰는 입력 ── */") : text.index("/* ── 순수 셈 끝 ── */")]
|
||||
out = subprocess.run(
|
||||
["node", "--experimental-strip-types", "--no-warnings",
|
||||
"--input-type=module-typescript", "-e", pure + "\n" + body],
|
||||
capture_output=True, text=True, encoding="utf-8", timeout=60,
|
||||
) # fmt: skip
|
||||
assert out.returncode == 0, out.stderr
|
||||
return json.loads(out.stdout)
|
||||
|
||||
|
||||
def test_표_행_열쇠와_이름_조각():
|
||||
got = _run(
|
||||
"""
|
||||
const line = {
|
||||
조각: [
|
||||
{ kind: "표", 글: "25 · 깬잡석", 참조: { 키: "QF1", 행: { 뒷길이: 25, 돌: "깬잡석", 줄번호: 3 } } },
|
||||
{ kind: "기호", 글: "*" },
|
||||
{ kind: "값", 글: "두께", 값: "20" },
|
||||
{ kind: "값", 글: "0.009" },
|
||||
{ kind: "값", 글: "뒷길이", 값: "99" },
|
||||
],
|
||||
요소조각: [{ kind: "표", 글: "x", 참조: { 키: "QF2", 행: 4 } }],
|
||||
};
|
||||
console.log(JSON.stringify(usedInputs(line, ["뒷길이", "돌", "두께", "쌓기"])));
|
||||
"""
|
||||
)
|
||||
# 처음 나온 값 · 입력 아닌 열쇠(줄번호) · 이름 없는 수(0.009) · 행 번호(4) 는 뺌
|
||||
assert got == [["뒷길이", "25"], ["돌", "깬잡석"], ["두께", "20"]]
|
||||
|
||||
|
||||
def test_빈_줄():
|
||||
assert _run('console.log(JSON.stringify(usedInputs({}, ["a"])))') == []
|
||||
|
||||
|
||||
def test_이음_시험_입력만_고침():
|
||||
lab = (M01 / "M01_MasterData_UI_LogicLab.ts").read_text(encoding="utf-8")
|
||||
detail = (M01 / "M01_MasterData_UI_LogicLab_Detail.ts").read_text(encoding="utf-8")
|
||||
# 텍스트 수식 고침 = 시험 계산 입력 + 다시 셈(흐름 예시값과 같은 길) · 캐시(drafts)에 안 들어감
|
||||
assert "onInput: (name, v) => syncLeft(current, { [name]: v }, true)" in lab
|
||||
assert "onInput: ctx.onInput" in detail
|
||||
fx = FX.read_text(encoding="utf-8")
|
||||
assert "touch" not in fx and "drafts" not in fx
|
||||
@@ -104,8 +104,6 @@ dropLine(doc, by("add:#0"));
|
||||
out.dropAdded = doc.더한줄.length;
|
||||
const covered = { 덮개: { "labor:A#0": { qty: 0.3, 원값: { qty: 0.09, 단가: 1000 } } } };
|
||||
out.revised = rowsOf(bare, bare, covered).find((r) => r.cid === "labor:A#0").revised;
|
||||
out.sheet = sheetGroups(rowsOf(bare, bare, {}))
|
||||
.map((g) => [g.비목, g.소계, g.줄.map((x) => x.금액)]);
|
||||
// 공용 상세 줄 — 바탕 줄 그대로 · 뺀 줄 없음 · 더한줄 · 별도계상 빠진 줄 = 빈 칸 · 덧줄
|
||||
const base = { 호표: [{ 종류: "인력", 요소: "A", 이름: "석공", 수량: "0.1*x" },
|
||||
{ 종류: "로직", 요소: "로직(G)", 수량: "2" }, { 종류: "재료", 요소: "M", 수량: "0.5" }],
|
||||
@@ -146,12 +144,6 @@ console.log(JSON.stringify(out));
|
||||
assert got["drop"] == [["labor:A#0"], None]
|
||||
assert got["restore"] == [] and got["dropAdded"] == 0
|
||||
assert got["revised"] is True
|
||||
# 계산서 = 표 줄 그대로 · 별도계상 재료 금액 없음(계에 안 듦)
|
||||
assert got["sheet"] == [
|
||||
["노무비", 160, [100, 60]],
|
||||
["재료비", 30, [30, None]],
|
||||
["경비", 10, [10]],
|
||||
]
|
||||
ho, extra, cids, lines, covers = got["view"]
|
||||
assert ho == [
|
||||
["인력", "A", "0.1*x", None], # 바탕 줄 그대로(식)
|
||||
@@ -173,11 +165,9 @@ def test_상세_부품_길_부품상자():
|
||||
cover = _text(M02 / "M02_MasterTemplete_Sangeun_Cover.ts")
|
||||
assert "mountSangeunDetail(host, doc" in view and "createEmpty" not in view
|
||||
assert 'read: "/m02/logic-lib"' in detail and "setLogicApiPaths(LIB)" in detail
|
||||
assert (
|
||||
"buildLabDetail(editor" in detail
|
||||
and "cover: { ho, est, open: lineOpener(hoCtx) }" in detail
|
||||
)
|
||||
assert "buildCalc(calcHost" in detail and "openSangeunLinePicker(scope" in detail
|
||||
assert "buildLabDetail(editor" in detail and "cover: { ho, open: lineOpener(hoCtx) }" in detail
|
||||
assert "buildCalc" not in detail # 799-2 시험 계산 보기 걷음
|
||||
assert "openSangeunLinePicker(scope" in detail
|
||||
assert "if (mine !== seq || !alive) return" in detail # 최신 응답만
|
||||
# 724-1 — 단가산출 상세 = 공용 상세(Ho) · 고침 고리 = SK 덮개 · 옛 고침 표 걷음
|
||||
assert "...buildHo(hoCtx)" in detail and 'scope: "m02",' in detail
|
||||
@@ -188,7 +178,7 @@ def test_상세_부품_길_부품상자():
|
||||
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
|
||||
lab = _text(ROOT / "M01_MasterData" / "M01_MasterData_UI_LogicLab_Detail.ts")
|
||||
assert "ctx.cover?.est ??" in lab and "ctx.cover.ho" in lab
|
||||
assert "ctx.cover.ho" in lab and "cover?.est" not in lab
|
||||
side = _text(ROOT / "M01_MasterData" / "M01_MasterData_UI_Side.ts")
|
||||
modal = _text(ROOT / "M01_MasterData" / "M01_MasterData_UI_LogicLab_Modal.ts")
|
||||
assert "export const hasMasterJump" in side and "go.hidden = !hasMasterJump()" in modal
|
||||
|
||||
@@ -27,8 +27,8 @@ def _text(path: Path) -> str:
|
||||
def test_721_1_단추_이름과_자리():
|
||||
detail = _text(M02 / "M02_MasterTemplete_Sangeun_Detail.ts")
|
||||
assert (
|
||||
"children: [sheet.picker, addButton, rateButton]" in detail
|
||||
) # 750-2 안내 걷음 · 778-4 율 줄 추가 · 790-1 보기 고르기 = 시트 탭 부품
|
||||
"children: [tabs.picker, addButton, rateButton]" in detail
|
||||
) # 750-2 안내 걷음 · 778-4 율 줄 추가 · 799-2 보기 고르기 = 수식 · 단가산출 상세 (`_Sangeun_Tabs`)
|
||||
assert "M02_SangeunPickBase" not in detail # 727-2 단추 걷음
|
||||
locale = _text(ROOT / "ui_template" / "ui_template_locale_m2.ts")
|
||||
assert "M02_SangeunPickBase" not in locale
|
||||
|
||||
@@ -63,9 +63,9 @@ def test_상세가_공용_부품을_씀():
|
||||
def test_시험_고침은_캐시_밖():
|
||||
lab = _text(M01 / "M01_MasterData_UI_LogicLab.ts")
|
||||
# 시험 계산에만 덮은 사본 · 고침이 있으면 줄을 보냄 · drafts(캐시)에는 원본 줄
|
||||
assert ": trial.apply(row);" in lab
|
||||
assert "=> trial.apply(row);" in lab
|
||||
assert "dirty: () => current.id in drafts || trial.has(current.row)," in lab
|
||||
assert "onTrial: drawCalc," in lab
|
||||
assert "onTrial: drawCalc," in lab and "rerun: () => void drive.run()," in lab # 799-2 셈 구동
|
||||
cover = _text(COVER)
|
||||
assert "drafts" not in cover and "touch" not in cover and "/save" not in cover
|
||||
modal = _text(M01 / "M01_MasterData_UI_LogicLab_Modal.ts")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""734-1 계산식 열 단추 지킴 시험 — 낮은 단(20) · 갈래 색(옵션값 · 요소 연결) · 풍선 글(화면 · 서버 없음)."""
|
||||
"""734-1 계산식 열 단추 지킴 시험 — 낮은 단(20) · 갈래 색 · 풍선 글(화면 · 서버 없음)."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
@@ -15,11 +15,6 @@ def test_알약은_낮은_단과_요소연결_갈래():
|
||||
assert 'size: 20, tone: "link"' in s and 'tl("Pill_Tone")' in s
|
||||
|
||||
|
||||
def test_입력칩은_낮은_단과_옵션_갈래():
|
||||
s = _t(M01 / "M01_MasterData_UI_LogicLab_Formula.ts")
|
||||
assert "size: 20," in s and 'tone: "option"' in s and 'tl("Var_Tone")' in s
|
||||
|
||||
|
||||
def test_갈래_색은_공통_값만():
|
||||
css = _t(ROOT / "ui_template" / "ui_template_button.css")
|
||||
head = css[css.index("갈래 색(tone)") :]
|
||||
@@ -30,4 +25,4 @@ def test_갈래_색은_공통_값만():
|
||||
|
||||
def test_풍선_글_두_갈래():
|
||||
s = _t(M01 / "M01_MasterData_UI_LogicLab_Text.ts")
|
||||
assert '"요소 연결"' in s and '["옵션"' in s
|
||||
assert '"요소 연결"' in s # 799-2 — 입력 칩(옵션 풍선)은 텍스트 수식과 함께 걷음
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""737 단가산출 상세 — 옵션 칸 격자 · 합계 한 줄 바닥 · 두 보기 같은 제목 · 비목 상자 지킴 시험(글 대조 · 서버 · 화면 없음)."""
|
||||
"""737 단가산출 상세 — 옵션 칸 격자 · 합계 한 줄 바닥 · 비목 상자 지킴 시험(글 대조 · 서버 · 화면 없음) · 799-2 텍스트 수식 보기는 걷음."""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
@@ -8,8 +8,6 @@ M01 = ROOT / "M01_MasterData"
|
||||
SHEET_CSS = (ROOT / "A00_Common" / "calc_sheet.css").read_text(encoding="utf-8")
|
||||
STYLE = (M01 / "M01_MasterData_UI_LogicLab_Style.css").read_text(encoding="utf-8")
|
||||
HO = (M01 / "M01_MasterData_UI_LogicLab_Ho.ts").read_text(encoding="utf-8")
|
||||
DETAIL = (M01 / "M01_MasterData_UI_LogicLab_Detail.ts").read_text(encoding="utf-8")
|
||||
FX = (M01 / "M01_MasterData_UI_LogicLab_Formula.ts").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_옵션_격자_폭_따라_6_4_3_2_1():
|
||||
@@ -29,21 +27,10 @@ def test_합계_한_줄_바닥_고정():
|
||||
assert "overflow: clip" in STYLE # 메인 칸이 굴림 칸이면 바닥에 안 붙음
|
||||
foot_ts = (M01 / "M01_MasterData_UI_LogicLab_Foot.ts").read_text(encoding="utf-8")
|
||||
assert 'className: "m01lab__foot"' in foot_ts # 748-3 두 보기가 같은 바닥 줄 부품
|
||||
assert "buildFoot(sums, calculated, total)" in HO and "buildFoot(sums, calculated)" in FX
|
||||
assert "buildFoot(sums, calculated, total)" in HO
|
||||
assert 'data-cols="3"' in STYLE and ".m01lab__ho-box + .m01lab__ho-box" in STYLE
|
||||
|
||||
|
||||
def test_공통_옵션은_모든_박스에_같이():
|
||||
assert "optionRows([...usesOf(b), ...common], ctx)" in HO
|
||||
assert "commonCard(" in HO and "boxes.length ||" in HO # 박스 없을 때만 따로 카드
|
||||
|
||||
|
||||
def test_두_보기_같은_제목_지금_입력_꼬리표_없음():
|
||||
assert 'section(tl("Formula_Title")' not in DETAIL
|
||||
assert 'section(tl("Ho_Title"), [formula])' in DETAIL
|
||||
assert "nowInputs(ctx.values), ...blocks" not in DETAIL
|
||||
|
||||
|
||||
def test_텍스트_수식은_비목_상자_옵션은_상자_위():
|
||||
assert "createCard({" in FX and "fxBox" in FX and "optionGrid(" in FX
|
||||
assert "...vars(line)" not in FX # 줄마다 붙던 옵션 꼬리표 걷음
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
"""748-2 지킴 — 텍스트 수식 한 줄을 변수 이름 · 값 하나하나로 나눔(누르면 줄 모달)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def test_변수_이름_값_조각():
|
||||
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HERE / "helper_748_2_formula_tokens.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
out = json.loads(proc.stdout)
|
||||
# 이름식 — 변수 이름(점 든 표 찾기 이름 포함)만 · 수 · 기호는 아님
|
||||
assert out["nameHot"] == [
|
||||
"굴착기노무비단가",
|
||||
"뒤채움및다짐소형장비.굴착기",
|
||||
"시공량",
|
||||
"굴착기노무비",
|
||||
]
|
||||
assert "8" in out["nameAll"] and "=" in out["nameAll"]
|
||||
# 함수 이름(「이름(」)은 변수가 아님 · 수 · 홀로 선 빼기는 아님
|
||||
assert out["fnCall"] == ["단가", "보정"]
|
||||
# 값식 — 수 하나하나(콤마 · 소수점 포함) · 기호는 그대로
|
||||
assert out["valueHot"] == ["35,412", "1", "8", "110", "2,575"]
|
||||
assert out["valueAll"][:3] == ["13,873", "=", "13,873"]
|
||||
assert out["empty"] == [0, 0]
|
||||
@@ -1,4 +1,4 @@
|
||||
"""750-2 화면 — 네 보기가 SK 셈 답 하나로(바탕 M01 로직 따로 셈 길 걷음) · 재료 고름 = 문서 `고름` ·
|
||||
"""750-2 화면 — 수식 · 단가산출 상세가 SK 셈 답 하나로(바탕 M01 로직 따로 셈 길 걷음 · 799-2 시험 계산 보기는 걷음) · 재료 고름 = 문서 `고름` ·
|
||||
읽는 식 줄 = cid 로 이음 · 옵션 바꿈 = SK 셈 한 번(원문 대조는 서버 test_750_1)."""
|
||||
|
||||
from pathlib import Path
|
||||
@@ -12,7 +12,7 @@ def _ts(rel: str) -> str:
|
||||
|
||||
def test_detail_uses_only_sk_answer():
|
||||
ts = _ts("M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts")
|
||||
assert "text = got.글" in ts and "answer: async" in ts and "shown: shownOf" in ts
|
||||
assert "text = got.글" in ts and "answer: async" not in ts and "buildCalc" not in ts
|
||||
assert "onText" not in ts and "onLines" not in ts # 바탕 `/calc` · `/text` 부름 걷음
|
||||
assert "dropText" not in ts and "baseCovers" not in ts and "applyCovers" not in ts
|
||||
assert "calcSangeun(scope, plain, values, false)" in ts # 바탕 원값 셈 = 보기 덧칸 없이
|
||||
@@ -26,10 +26,11 @@ def test_text_lines_joined_by_cid():
|
||||
assert "cid?: string" in api and "나눔?: number" in api
|
||||
|
||||
|
||||
def test_calc_view_answer_hook_skips_logic_lib():
|
||||
def test_calc_drive_is_screenless_and_m02_has_no_calc_hook():
|
||||
ts = _ts("M01_MasterData/M01_MasterData_UI_Logic_Calc.ts")
|
||||
assert "ctx.onText && !ctx.answer" in ts # 미리보기 안 부름
|
||||
assert "if (ctx.answer)" in ts and "runCalc" in ts # M01 옛 길 그대로
|
||||
assert "ctx.answer" not in ts and "CalcShown" not in ts # 799-2 M02 시험 계산 길 걷음
|
||||
drive = _ts("M01_MasterData/M01_MasterData_UI_Logic_Drive.ts")
|
||||
assert "runCalc(body)" in drive and "document." not in drive # 셈 구동 = 화면 없이
|
||||
|
||||
|
||||
def test_752_1_source_panel_only_on_sangeun_screen():
|
||||
@@ -71,9 +72,7 @@ const line = (cid, 이름, 수량, 금액, 꺼짐) => ({ cid, 이름, 단위: "
|
||||
const ans = { 기준: null, 짝잃음: [], 바탕판: "x", 정체: ["material:A#0", "material:B#0"],
|
||||
줄: [line("material:A#0", "폼타이 12.7", 0, 0, true), line("material:B#0", "합판", 1, 100, false)] };
|
||||
const rows = rowsOf(ans, null, {});
|
||||
const groups = sheetGroups(rows);
|
||||
console.log(JSON.stringify({ off: rows.map((r) => r.off),
|
||||
sheet: groups.map((g) => [g.비목, g.소계, g.줄.map((x) => [x.이름, x.옵션꺼짐 ?? false, x.금액])]) }));
|
||||
console.log(JSON.stringify({ off: rows.map((r) => r.off) }));
|
||||
""",
|
||||
]
|
||||
)
|
||||
@@ -85,14 +84,10 @@ console.log(JSON.stringify({ off: rows.map((r) => r.off),
|
||||
assert out.returncode == 0, out.stderr
|
||||
got = json.loads(out.stdout)
|
||||
assert got["off"] == [True, False]
|
||||
assert got["sheet"] == [["재료비", 100, [["폼타이 12.7", True, 0], ["합판", False, 100]]]]
|
||||
sheet = (ROOT / "A00_Common" / "calc_sheet.ts").read_text("utf-8")
|
||||
assert "b.줄.filter(shown)" in sheet and "g.줄.filter(shown)" in sheet
|
||||
ho = (ROOT / "M01_MasterData" / "M01_MasterData_UI_LogicLab_Ho.ts").read_text("utf-8")
|
||||
assert "옵션꺼짐" in ho
|
||||
assert "옵션꺼짐" in (
|
||||
ROOT / "M01_MasterData" / "M01_MasterData_UI_LogicLab_Formula.ts"
|
||||
).read_text("utf-8")
|
||||
assert "옵션꺼짐" in (ROOT / "M01_MasterData" / "M01_MasterData_UI_Logic_Calc.ts").read_text(
|
||||
"utf-8"
|
||||
)
|
||||
|
||||
@@ -1,312 +0,0 @@
|
||||
"""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(로그인 쿠키)
|
||||
@@ -5,14 +5,13 @@
|
||||
인력운반 120 → 100 = 38,696 · 표값 운반량 30 = 35,764) · 옵션 바꿔도 상수 덮개 식 따라감
|
||||
- 식 속 숫자 차례 = 엔진 파서(찾기 머리 · 키 · EQ: · 따옴표 글 안 셈)
|
||||
- 옛 SK(새 칸 없음) = 꼴 · 금액 그대로 · 저장 = M01판 · 셈 답 `원래` · `시트`
|
||||
- [M01 원래로] · 더한줄 열쇠 옮김 · 덮개 갈래 = 파이썬 `M02_Sangeun_Const` ↔ TS `Const_Model` 거울
|
||||
- 799-2 — TS 거울(`Const_Model.ts`)은 시트 탭과 함께 걷음 · 서버 `M02_Sangeun_Const` 만 지킴
|
||||
- 펼침 = 상수 덮개 SK 안 펼침(참조 그대로 · 값 그대로) · [초기화] = 새 덮개도 걷음
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -233,86 +232,3 @@ def test_펼침_막음_초기화_걷음(world):
|
||||
flat.flat_lines("sangeun", {**MINE, "키": a})
|
||||
doc, n = flat.reset("sangeun", {**_read(mine, a), "까닭": {"mid:일운반회수": "가정"}})
|
||||
assert n == 1 and not set(doc) & {"상수덮개", "까닭"}
|
||||
|
||||
|
||||
# ── 파이썬 ↔ TS 거울 ─────────────────────────────────────────────────
|
||||
MIRROR_DOC = {
|
||||
"덮개": {"labor:LB000002#0": {"qty": 1}, "labor:LB000007#0": {"labor": 5}},
|
||||
"뺀줄": ["pct:소모자재#0"],
|
||||
"더한줄": [
|
||||
{
|
||||
"종류": "로직",
|
||||
"로직": "GC000001",
|
||||
"덮개": {"labor:X#0": {"qty": 1}},
|
||||
"뺀줄": ["material:Y#0"],
|
||||
},
|
||||
{"종류": "인력"},
|
||||
],
|
||||
"상수덮개": {"mid:시공량": {"2": 20}, "labor:LB000002#0": {"0": 3}},
|
||||
"표값덮개": {"mid:운반량": {"0": 30}},
|
||||
"글줄": [{"자리": "add:#1", "글": "a"}, {"자리": "add:#0", "글": "b"}],
|
||||
"까닭": {
|
||||
"mid:시공량": "c",
|
||||
"add:#0": "d",
|
||||
"add:#1": "e",
|
||||
"add:#0/labor:X#0": "f",
|
||||
"pct:소모자재#0": "g",
|
||||
},
|
||||
}
|
||||
KEYS = [
|
||||
None,
|
||||
"mid:시공량",
|
||||
"labor:LB000002#0",
|
||||
"pct:소모자재#0",
|
||||
"add:#0/labor:X#0",
|
||||
"add:#0/material:Y#0",
|
||||
"없음",
|
||||
]
|
||||
|
||||
|
||||
def _ts(body: str):
|
||||
src = (ROOT / "M02_MasterTemplete/logic/M02_Sangeun_Const_Model.ts").read_text("utf-8")
|
||||
got = subprocess.run(
|
||||
["node", "--experimental-strip-types", "--no-warnings",
|
||||
"--input-type=module-typescript", "-e", src + body],
|
||||
capture_output=True, text=True, encoding="utf-8", timeout=60,
|
||||
) # fmt: skip
|
||||
assert got.returncode == 0, got.stderr
|
||||
return json.loads(got.stdout)
|
||||
|
||||
|
||||
def test_거울_M01원래로_열쇠옮김_갈래():
|
||||
d = json.dumps(MIRROR_DOC, ensure_ascii=False)
|
||||
keys = json.dumps(KEYS)
|
||||
spots = json.dumps(
|
||||
["labor:LB000002#0", "labor:LB000007#0", "pct:소모자재#0", "mid:운반량", "x"]
|
||||
)
|
||||
ts = _ts(
|
||||
f"\nconst d = {d};"
|
||||
f"\nconst out = {{r: {keys}.map((k) => restoreM01(d, k ?? undefined)),"
|
||||
" a: [0, 1, 5].map((k) => dropAddKeys(d, k)),"
|
||||
f" k: {spots}.map((s) => coverKinds(d, s))}};"
|
||||
"\nconsole.log(JSON.stringify(out));"
|
||||
)
|
||||
assert ts["r"] == [const.restore(MIRROR_DOC, k) for k in KEYS]
|
||||
assert ts["a"] == [const.drop_add_keys(MIRROR_DOC, k) for k in (0, 1, 5)]
|
||||
assert ts["k"] == [const.kinds(MIRROR_DOC, s) for s in json.loads(spots)]
|
||||
assert ts["k"][0] == ["품", "상수"] and ts["k"][2] == ["뺌"]
|
||||
|
||||
|
||||
def test_TS_고치기_꼴_서버가_받음():
|
||||
"""setConst · setTable · addText · editText · dropText · setReason 결과 = 서버 꼴 그대로 통과."""
|
||||
ts = _ts(
|
||||
"\nlet d = {키: 'SK-000001', 이름: '견본', 단위: '식', 바탕: {로직: 'GC000211'}};"
|
||||
"\nd = setConst(d, 'mid:시공량', 2, 20); d = setConst(d, 'mid:시공량', 0, 1);"
|
||||
"\nd = setConst(d, 'mid:시공량', 0, null); d = setTable(d, 'mid:시공량', 0, 45);"
|
||||
"\nd = setTable(d, 'mid:시공량', 0, null);"
|
||||
"\nd = addText(d, '', '근거'); d = addText(d, 'mid:시공량', '※ 조건', 0);"
|
||||
"\nd = editText(d, 1, '근거 고침'); d = dropText(d, 0);"
|
||||
"\nd = setReason(d, 'mid:시공량', ' 까닭 '); d = setReason(d, 'add:#0', '');"
|
||||
"\nconsole.log(JSON.stringify(d));"
|
||||
)
|
||||
assert ts["상수덮개"] == {"mid:시공량": {"2": 20}} and "표값덮개" not in ts
|
||||
assert ts["글줄"] == [{"자리": "", "글": "근거 고침"}] and ts["까닭"] == {"mid:시공량": "까닭"}
|
||||
got = sd.clean({**ts, "입력값": F211}, None, _whole())
|
||||
assert {k: got[k] for k in const.NEW if k in got} == {k: ts[k] for k in const.NEW if k in ts}
|
||||
|
||||
@@ -111,8 +111,9 @@ def test_단산_줄_길_잇기():
|
||||
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
|
||||
sheet = _src("Sangeun_Sheet") # 상태줄 원래 = 가져온 식이면 「엑셀 원래」
|
||||
assert 'Excel: ["엑셀 원래"' in sheet and '"가져온" in d.ans ? "Excel" : "Origin"' in sheet
|
||||
# 수식 탭 위 줄 = 가져온 식이면 「엑셀 원래」(799-2 — 시트 탭 걷음)
|
||||
sheet = _src("Sangeun_Equation")
|
||||
assert 'Excel: ["엑셀 원래"' in sheet and '"가져온" in ans' in sheet
|
||||
locale = (ROOT / "ui_template/ui_template_locale_m2_judge.ts").read_text(encoding="utf-8")
|
||||
assert 'M02_CheckImported: ["가져온 식으로 열기"' in locale
|
||||
|
||||
@@ -148,7 +149,7 @@ def test_부품상자만_700줄_공용_안_고침():
|
||||
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"):
|
||||
for name in ("Labor_Rows", "Labor_SangeunModal", "Sangeun_Detail", "Sangeun_Tabs"):
|
||||
assert "Hopyo_Imported" not in _src(name), name # 공용 쪽은 새 모달을 모름
|
||||
|
||||
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
# ruff: noqa: E501
|
||||
"""794-1 M01 관리자 줄 식 시트 — 로직 상세 첫 보기 「시트」 · 식 칸 = M02 단가산출 시트와 한 벌.
|
||||
"""794-1 M01 관리자 줄 식 시트 — 서버 길(`/logic/sheet` · `/logic/sheet/check`) 지킴.
|
||||
|
||||
- 정적: 시트 화면 = 부품 상자만(날 button · input 0) · 식 칸 몸 = 공용 `sheet_formula_cell`(M02 도 같은 것) ·
|
||||
고침 = 관리자만 · 줄 사본 → 캐시(touch) → 기존 [저장] 길(시트는 `/save` 안 부름) · 보기 첫 칸 = 시트 ·
|
||||
자체 로직(GX) 편집기 식 칸 = 읽기만(날 입력칸 갈음)
|
||||
- 799-2 — 시트 화면 · 모델(`_LogicLab_Sheet.ts` · `logic/M01_Logic_Sheet_Model.ts`)은 걷음 · 이 서버 길은 수식 탭 검사와 같은 엔진이라 남김
|
||||
- 서버(사본 폴더): 이름 꼴 식 글 · 칠 때마다 검사(없는 이름 · 아래 줄 · 괄호 · 빈 식 · 길이 · 없는 자리) ·
|
||||
검사 = 쓰기 없음 · 공구손료 식 1 → 2 % 저장(개정 확인 409 → 200) 뒤 셈 51,802 · 틀린 식 저장 422 · 시스템 관리자가 아니면 403
|
||||
- 모델(node strip-types): 줄 갈래 · 줄 금액 = 호표 → 덧줄 차례 · 고친 줄 · 끝수 · 단계 · 식 칸 고침
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -28,19 +22,12 @@ from M01_MasterData import M01_MasterData_Router as m01_router
|
||||
from M01_MasterData import M01_MasterData_Sheet as line_sheet
|
||||
from M01_MasterData import M01_MasterData_Store as store
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
M01 = ROOT / "M01_MasterData"
|
||||
SHEET = M01 / "M01_MasterData_UI_LogicLab_Sheet.ts"
|
||||
MODEL = M01 / "logic" / "M01_Logic_Sheet_Model.ts"
|
||||
LAB = M01 / "M01_MasterData_UI_LogicLab.ts"
|
||||
CELL = ROOT / "A00_Common" / "sheet_formula_cell.ts"
|
||||
REAL = store.FOLDER
|
||||
KEY = "GC000211" # 합판거푸집 설치 및 해체
|
||||
INPUTS = {"자재지역": "포항", "유형": "보통", "사용횟수": "6회", "높이감률": 0, "지붕슬래브": "아님",
|
||||
"폼타이": "아님", "폼타이규격": "12.7㎜", "측압": "3t/㎡", "구멍땜": 0} # fmt: skip
|
||||
TOOL_1 = "(형틀목공 × [노임 형틀목공] + 보통인부 × [노임 보통인부]) × 1 / 100"
|
||||
TOOL_2 = TOOL_1.replace("× 1 / 100", "× 2 / 100")
|
||||
IMPORTS = re.compile(r"^import[^;]*;\r?\n", re.MULTILINE)
|
||||
|
||||
|
||||
def _text(p: Path) -> str:
|
||||
@@ -51,44 +38,6 @@ def _hashes(folder: Path) -> dict[str, str]:
|
||||
return {p.name: hashlib.md5(p.read_bytes()).hexdigest() for p in sorted(folder.glob("*.json"))}
|
||||
|
||||
|
||||
# ── 정적 ─────────────────────────────────────────────────────────────
|
||||
def test_정적_부품_상자만_관리자만_저장은_기존_길() -> None:
|
||||
for p in (SHEET, MODEL, CELL):
|
||||
s = _text(p)
|
||||
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
|
||||
assert "fetch(" not in _text(MODEL) and "document." not in _text(MODEL)
|
||||
sheet = _text(SHEET)
|
||||
for part in (
|
||||
"createCalcSheetRow",
|
||||
"createFormulaCell",
|
||||
'"/logic/sheet"',
|
||||
'"/logic/sheet/check"',
|
||||
):
|
||||
assert part in sheet, part
|
||||
assert "/save" not in sheet # 저장 = 상세 [저장](기존 길)
|
||||
assert sheet.count("opt.admin &&") >= 2 # [식 고침] · 식 칸 = 관리자만
|
||||
lab = _text(LAB)
|
||||
assert "const sheet = createLabSheet({\n admin," in lab.replace("\r\n", "\n")
|
||||
assert "sheet: sheet.root," in lab and "onAnswer: sheet.answer," in lab
|
||||
assert "edited: () => (touch(), drawEditor(), drawCalc())" in lab # 캐시(touch) → [저장]
|
||||
assert "setLabFormula(d.row, part, at, text)" in sheet and "opt.edited();" in sheet
|
||||
assert len(lab.splitlines()) <= 700 # 796-2 수식 탭 이음 14 줄
|
||||
view = _text(M01 / "M01_MasterData_UI_LogicLab_View.ts")
|
||||
assert ( # 796-2 — 첫 탭 = 수식 · 시트는 뒤로(걷지 않음)
|
||||
'host.dataset.view = "equation";' in view
|
||||
and '{ value: "sheet", label: tl("View_Sheet") }' in view
|
||||
)
|
||||
css = _text(M01 / "M01_MasterData_UI_LogicLab_Style.css")
|
||||
assert '[data-view="sheet"] [data-block]:not([data-block="sheet"])' in css
|
||||
edit = _text(M01 / "M01_MasterData_UI_Logic_Edit.ts")
|
||||
assert "textBox(item.수량" not in edit and "textBox(item.식" not in edit # GX 날 식 칸 갈음
|
||||
m02 = _text(ROOT / "M02_MasterTemplete" / "M02_MasterTemplete_Sangeun_SheetFormula.ts")
|
||||
assert "return createFormulaCell({" in m02 # M02 시트도 같은 식 칸
|
||||
main = _text(ROOT / "main.py")
|
||||
assert "app.include_router(m01_master_data_router, dependencies=system_admin_only)" in main
|
||||
|
||||
|
||||
# ── 서버 ─────────────────────────────────────────────────────────────
|
||||
@pytest.fixture
|
||||
def folder(tmp_path: Path, monkeypatch) -> Path:
|
||||
@@ -175,60 +124,3 @@ def test_시스템_관리자가_아니면_403(folder: Path) -> None:
|
||||
for url, body in (("/api/m01/logic/sheet", {"row": {}}),
|
||||
("/api/m01/logic/sheet/check", {"row": {}, "갈래": "중간", "차례": 0, "식": "1"})): # fmt: skip
|
||||
assert client.post(url, json=body).status_code == 403
|
||||
|
||||
|
||||
# ── 모델 ─────────────────────────────────────────────────────────────
|
||||
ROW = {
|
||||
"키": "GX1", "원문번호": "1-1", "이름": "견본", "결과단위": "원/㎡", "출처": "견본 1-1", "비고": "한 줄\n둘째 줄",
|
||||
"입력": [{"이름": "폭", "단위": "m"}],
|
||||
"중간": [{"이름": "넓이", "식": "폭 * 2"}],
|
||||
"호표": [{"종류": "인력", "요소": "LB000002", "이름": "보통인부", "단위": "인", "수량": "넓이 * 0.1", "비목": "노무비", "번지": "A01"}],
|
||||
"덧줄": [{"이름": "공구손료", "식": "노무비 * 1 / 100", "비목": "경비"}],
|
||||
"단계": [{"이름": "노무 소계", "단": 1, "줄": ["A01"]}],
|
||||
"끝수": {"대상": "비목", "방법": "버림", "자리": 0},
|
||||
} # fmt: skip
|
||||
|
||||
|
||||
def _run() -> dict:
|
||||
src = IMPORTS.sub("", _text(MODEL))
|
||||
body = (
|
||||
f"\nconst R = {json.dumps(ROW, ensure_ascii=False)};"
|
||||
"\nconst now = structuredClone(R);"
|
||||
"\nconst put = setLabFormula(now, '호표', 0, '넓이 * 0.2');"
|
||||
"\nconst miss = setLabFormula(now, '덧줄', 5, '1');"
|
||||
"\nconst answer = { ok: true, middle: { 넓이: 6 }, sums: { 노무비: 1000, 재료비: 0, 경비: 10, 계: 1010 },"
|
||||
" lines: [{ 금액: 1000 }, { 금액: 10.5 }] };"
|
||||
"\nconst items = labItems({ row: now, original: R, values: { 폭: '3' },"
|
||||
" names: { 중간: ['폭 × 2'], 호표: ['넓이 × 0.2'], 덧줄: ['노무비 × 1 / 100'] }, answer });"
|
||||
"\nconst bare = labItems({ row: R, original: null, values: {}, names: null, answer: { ok: false, reason: 'x' } });"
|
||||
"\nconsole.log(JSON.stringify({ put, miss, items, bare, cut: cutText('글자 끝수') }));"
|
||||
)
|
||||
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_모델_줄_갈래_값_고친_줄() -> None:
|
||||
got = _run()
|
||||
assert got["put"] is True and got["miss"] is False
|
||||
kinds = [(x["kind"], x["이름"]) for x in got["items"]]
|
||||
assert kinds == [("근거", "1-1"), ("근거", ""), ("근거", ""), ("옵션", "폭"), ("계산", "넓이"),
|
||||
("자원", "보통인부 (인)"), ("율", "공구손료"), ("단계", "노무 소계"), ("근거", "끝수"),
|
||||
("소계", "노무비"), ("소계", "재료비"), ("소계", "경비"), ("합계", "계")] # fmt: skip
|
||||
by = {(x["kind"], x["이름"]): x for x in got["items"]}
|
||||
assert by[("옵션", "폭")]["값"] == "3"
|
||||
assert by[("계산", "넓이")]["값"] == 6 and not by[("계산", "넓이")]["changed"]
|
||||
res = by[("자원", "보통인부 (인)")]
|
||||
assert res["값"] == 1000 and res["비목"] == "노" and res["번지"] == "A01" and res["changed"]
|
||||
assert res["식"] == "넓이 × 0.2" and res["part"] == "호표" and res["at"] == 0
|
||||
assert by[("율", "공구손료")]["값"] == 10.5 and by[("율", "공구손료")]["part"] == "덧줄"
|
||||
assert by[("단계", "노무 소계")]["식"] == "A01"
|
||||
assert by[("근거", "끝수")]["식"] == "비목 · 버림 · 0 자리"
|
||||
assert by[("합계", "계")]["값"] == 1010
|
||||
bare = {(x["kind"], x["이름"]): x for x in got["bare"]}
|
||||
assert bare[("계산", "넓이")]["식"] == "폭 × 2" # 이름 꼴 전 = 엔진 식 「*」 → 「×」
|
||||
assert bare[("자원", "보통인부 (인)")]["값"] is None and ("합계", "계") not in bare
|
||||
assert got["cut"] == "글자 끝수"
|
||||
|
||||
@@ -69,13 +69,14 @@ def test_계산_자리_화면은_모양만():
|
||||
|
||||
|
||||
def test_수식_탭이_첫_탭_단가산출_상세는_그대로():
|
||||
"""799-2 — 보기 둘(수식 · 단가산출 상세)만 남김(시트 · 인력·재료·경비 · 시험 계산 · 텍스트 수식 걷음)."""
|
||||
view = _text(M01 / "M01_MasterData_UI_LogicLab_View.ts")
|
||||
labels = re.findall(r'\{ value: "(\w+)", label: tl\("(\w+)"\) \}', view)
|
||||
assert [v for v, _ in labels] == ["equation", "ho", "sheet", "est", "calc", "formula"]
|
||||
assert [v for v, _ in labels] == ["equation", "ho"]
|
||||
assert 'host.dataset.view = "equation";' in view and 'value: "equation",\n size: 32' in view
|
||||
detail = _text(M01 / "M01_MasterData_UI_LogicLab_Detail.ts")
|
||||
assert 'block(ctx.equation, "equation")' in detail
|
||||
assert 'block(section(tl("Ho_Title"), [formula]), "formula")' in detail # 옛 탭은 걷지 않음
|
||||
assert '"formula"' not in detail and "buildFormula" not in detail # 799-2 텍스트 수식 걷음
|
||||
css = _text(M01 / "M01_MasterData_UI_LogicLab_Style.css")
|
||||
assert '[data-view="equation"] [data-block]:not([data-block="equation"])' in css
|
||||
lab = _text(M01 / "M01_MasterData_UI_LogicLab.ts")
|
||||
|
||||
@@ -178,7 +178,7 @@ def test_m01_change_kept_regen_reapplies(world, monkeypatch):
|
||||
|
||||
|
||||
def test_screen_same_part_first_tab():
|
||||
"""화면 — M02 수식 = M01 수식 탭 부품 다시 씀(복사 없음) · 첫 탭 · 옛 탭 뒤로 · 저장 = 페이지."""
|
||||
"""화면 — M02 수식 = M01 수식 탭 부품 다시 씀(복사 없음) · 첫 탭 · 옛 탭 걷음(799-2) · 저장 = 페이지."""
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
@@ -186,7 +186,8 @@ def test_screen_same_part_first_tab():
|
||||
mine = (root / "M02_MasterTemplete_Sangeun_Equation.ts").read_text(encoding="utf-8")
|
||||
assert "createLabEquation(" in mine and "fetch(" not in mine and "createEqChain" not in mine
|
||||
assert "/sangeun/equation${suffix}" in mine and "/sangeun/copy/${how}" in mine
|
||||
sheet = (root / "M02_MasterTemplete_Sangeun_Sheet.ts").read_text(encoding="utf-8")
|
||||
views = re.findall(r'\{ value: "(\w+)", label: ', sheet)[:6]
|
||||
assert views == ["equation", "ho", "sheet", "est", "calc", "formula"]
|
||||
assert 'show("equation");' in sheet
|
||||
tabs = (root / "M02_MasterTemplete_Sangeun_Tabs.ts").read_text(encoding="utf-8")
|
||||
views = re.findall(r'\{ value: "(\w+)", label: ', tabs)
|
||||
# 799-2 — 시트 · 인력·재료·경비 · 시험 계산 · 텍스트 수식 걷음
|
||||
assert views == ["equation", "ho"]
|
||||
assert 'show("equation");' in tabs
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
# ruff: noqa: E501
|
||||
"""799-2 옛 탭 넷 걷음 지킴 — 시트 · 인력/재료/경비 · 시험 계산 · 텍스트 수식 (M01 로직 상세 · M02 단가산출 상세).
|
||||
|
||||
- 걷은 파일은 없고 어디서도 안 부름 · 보기 = 수식 · 단가산출 상세 둘 · 기본정보는 맨 위
|
||||
- 셈 구동은 화면 칸 없이 `_Logic_Drive.ts`(서버 셈) — M01 상세는 구동만 · 시험 계산 칸(`Logic_Calc`)은 로직 복사 화면 몫
|
||||
- 서버 길은 남김(`/logic/sheet` · 시트 문서 칸 · 수식 검사가 같은 엔진)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
M01 = ROOT / "M01_MasterData"
|
||||
M02 = ROOT / "M02_MasterTemplete"
|
||||
GONE = [
|
||||
M01 / "M01_MasterData_UI_LogicLab_Sheet.ts",
|
||||
M01 / "logic" / "M01_Logic_Sheet_Model.ts",
|
||||
M01 / "M01_MasterData_UI_LogicLab_Formula.ts",
|
||||
M01 / "M01_MasterData_UI_LogicLab_FormulaTokens.ts",
|
||||
M01 / "M01_MasterData_UI_LogicLab_Formula.css",
|
||||
M01 / "M01_MasterData_UI_LogicLab_Est.ts",
|
||||
M02 / "M02_MasterTemplete_Sangeun_Sheet.ts",
|
||||
M02 / "M02_MasterTemplete_Sangeun_SheetEdit.ts",
|
||||
M02 / "M02_MasterTemplete_Sangeun_SheetFormula.ts",
|
||||
M02 / "M02_MasterTemplete_Sangeun_Sheet.css",
|
||||
M02 / "logic" / "M02_Sangeun_Sheet_Model.ts",
|
||||
M02 / "logic" / "M02_Sangeun_Const_Model.ts",
|
||||
M02 / "logic" / "M02_Sangeun_Formula_Model.ts",
|
||||
]
|
||||
|
||||
|
||||
def _text(p: Path) -> str:
|
||||
return p.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _tracked() -> list[Path]:
|
||||
out = subprocess.run(
|
||||
["git", "-c", "core.quotepath=off", "ls-files", "*.ts", "*.css"],
|
||||
cwd=ROOT, capture_output=True, text=True, encoding="utf-8", check=True,
|
||||
).stdout # fmt: skip
|
||||
return [ROOT / x for x in out.splitlines() if x and not x.startswith(("docs/", "tmp/"))]
|
||||
|
||||
|
||||
def test_걷은_파일_없고_부르는_곳_없음():
|
||||
assert not [p for p in GONE if p.exists()]
|
||||
names = [p.name.rsplit(".", 1)[0] for p in GONE]
|
||||
for p in _tracked():
|
||||
text = _text(p)
|
||||
for n in names:
|
||||
assert f'/{n}"' not in text and f'./{n}"' not in text and f"{n}.css" not in text, (
|
||||
p.name,
|
||||
n,
|
||||
)
|
||||
|
||||
|
||||
def test_보기는_수식_단가산출상세_둘():
|
||||
m01 = _text(M01 / "M01_MasterData_UI_LogicLab_View.ts")
|
||||
m02 = _text(M02 / "M02_MasterTemplete_Sangeun_Tabs.ts")
|
||||
for src in (m01, m02):
|
||||
assert re.findall(r'\{ value: "(\w+)", label: ', src) == ["equation", "ho"]
|
||||
assert 'export type LabView = "equation" | "ho";' in m01
|
||||
css = _text(M01 / "M01_MasterData_UI_LogicLab_Style.css")
|
||||
assert '[data-view="ho"] [data-block]:not([data-block="ho"])' in css
|
||||
assert 'data-view="sheet"' not in css and 'data-view="calc"' not in css
|
||||
|
||||
|
||||
def test_셈_구동은_화면_없이_상세는_구동만():
|
||||
drive = _text(M01 / "M01_MasterData_UI_Logic_Drive.ts")
|
||||
assert "document." not in drive and "sessionStorage." not in drive
|
||||
assert "export async function driveRun(" in drive and "export function createDrive(" in drive
|
||||
lab = _text(M01 / "M01_MasterData_UI_LogicLab.ts")
|
||||
assert "createDrive()" in lab and "buildCalc" not in lab and "data-calc-run" not in lab
|
||||
assert "rerun: () => void drive.run()," in lab
|
||||
detail = _text(M01 / "M01_MasterData_UI_LogicLab_Detail.ts")
|
||||
assert "calcHost" not in detail and "data-calc-run" not in detail and "ctx.rerun?.()" in detail
|
||||
calc = _text(
|
||||
M01 / "M01_MasterData_UI_Logic_Calc.ts"
|
||||
) # 로직 복사 화면이 쓰는 시험 계산 칸 — 구동은 공용
|
||||
assert "driveRun(ctx)" in calc and "runCalc" not in calc
|
||||
m02 = _text(M02 / "M02_MasterTemplete_Sangeun_Detail.ts")
|
||||
assert "buildCalc" not in m02 and "calcHost" not in m02 and "createSangeunTabs(editor" in m02
|
||||
|
||||
|
||||
def test_파일_700줄_안():
|
||||
for p in (M01 / "M01_MasterData_UI_LogicLab.ts", M01 / "M01_MasterData_UI_LogicLab_Detail.ts",
|
||||
M02 / "M02_MasterTemplete_Sangeun_Detail.ts", M02 / "M02_MasterTemplete_Sangeun_Tabs.ts",
|
||||
M01 / "M01_MasterData_UI_Logic_Drive.ts"): # fmt: skip
|
||||
assert len(_text(p).splitlines()) <= 700, p.name
|
||||
Reference in New Issue
Block a user