Files
Aislo/resources/tester/test_804_7_line_stack_fix.py
T
eomsangdonandClaude Opus 5.5 abe722f378 feat(804-7): 줄 쌓기 「수식」 탭 수정 2차(요구 687 · 688)
- 로직 삭제 = 서버 정본에서 바로(로직 줄 + 수식 문서 · `/logic/remove`) → 로직 목록 · 부르는 곳(다른 수식 문서 · 일위대가 조합 · M02 단가산출 바탕)이 있으면 확인 창 전에 막고 까닭(`/logic/users`)
- 머리 단추 다섯 — [신규 로직] · [로직 복사/생성] · [로직 삭제] · [되돌림] · [저장] · [저장] 한 번 = 기본정보 + 수식 · [되돌림] = 둘 다 저장 뒤 상태 · 줄 쌓기 [고침 버림] · [수식 저장]은 M01 에서 걷음(M02 사본은 그대로)
- 합계 상자 화면 아래 고정 · 원문 손잡이 브라우저 오른쪽 끝
- 왼쪽: 그룹 머리 한 줄(접어도 [+] · X 자리 그대로) · 「비우면 번호 없음」 · 가나다 체크 걷음 · 금액 줄 한 줄 배치 · 이름 단추 = 그 이름 찾은 찾기 창 · 수량 기호 = 값 + 출처 꼬리표 · [+] 메뉴(단가산출 · 율 · 식 · 근거 글 · 틀 찾기 걷음) · 식 글 눌러 그 자리 고침([식 고치기] 걷음) · 단위 = 공용 단위 고르기(친 글도 씀) · 만약 안 탄 갈래 숨김
- 오른쪽 기계 줄: 비목 수 링크 걷음 · 제목 줄 누름 = 시간당 경비 펼침 · 펼친 제목 보통 글
- 요소 키가 기호 이름으로 샌 것(`k_EQ0006`) — 변환기가 자원 이름으로(`k_하베스터`) · 수식 문서 다시 빌드(10 파일 · 78 기호 · 금액 같음)
- 시험 페이지 `tools/line-stack-test` · main.py 마운트 · vite 프록시 · M01 왼쪽 「시험: 줄 쌓기」 걷음
- 단위 표준 표 읽기 = 로그인만(M02 일반 사용자 단위 목록)
- 시험: test_804_7 새로 · 804_2 · 804_4 이음 글 고침(createStackScreen 줄 나뉨) · 552_3 여는 단추 기본 ⋯ 꼴로 고침(부르는 쪽이 [+] 로 바꿀 수 있음)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 13:33:36 +09:00

206 lines
8.9 KiB
Python

"""804-7(요구 687 · 688) — 줄 쌓기 「수식」 탭 수정 2차 지킴.
① 로직 삭제 = 서버 정본(로직 줄 + 수식 문서)에서 바로 · 부르는 곳(다른 문서 · M02 단가산출) 있으면 409 {쓰는곳} ·
정본 로직은 개정 확인 409 — 임시 폴더만
② 머리 단추 다섯 — [저장] 한 번 = 로직 줄 + 수식 · [되돌림] = 둘 다 · 줄 쌓기 자기 단추 없음(M01)
③ 수식 문서 1,518 전수 — 요소 키가 기호 이름으로 샌 것(`k_EQ0006`) 0 · 변환기도 자원 이름(`k_하베스터`)
④ 단위 = 공용 단위 고르기(친 글도 씀) · 표준 표 읽기 = 로그인만
⑤ 시험 페이지(804-1) 걷음 · 줄 쌓기 화면 차림(한 줄 · [+] 메뉴 · 가나다 체크 없음 · 기계 제목 펼침)
"""
from __future__ import annotations
import json
import re
import shutil
from pathlib import Path
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from M01_MasterData import M01_MasterData_Doc as doc_screen
from M01_MasterData import M01_MasterData_Router as router_module
from M01_MasterData import M01_MasterData_Store as store
ROOT = Path(__file__).resolve().parents[2]
REAL = store.FOLDER
CODE = re.compile(r"^(?:EQ|MT|LB)\d")
def _src(rel: str) -> str:
return (ROOT / rel).read_text(encoding="utf-8")
@pytest.fixture
def client(tmp_path: Path, monkeypatch) -> TestClient:
logic, docs = tmp_path / "logic", tmp_path / "doc"
logic.mkdir()
for path in REAL.glob("*.json"):
if not path.name.startswith("_") or path.name == store.mk.BOOK.name:
shutil.copy(path, logic / path.name)
shutil.copytree(doc_screen.folder(), docs)
monkeypatch.setattr(store, "FOLDER", logic)
monkeypatch.setattr(doc_screen.ml, "FOLDER", docs)
from M02_MasterTemplete import M02_Template_Layers as layers
sk = tmp_path / "SK-1.json"
monkeypatch.setattr(
layers, "layer_files", lambda kind: [("개인 시험", sk)] if sk.exists() else []
)
app = FastAPI()
app.include_router(router_module.router)
c = TestClient(app)
c.sk = sk # type: ignore[attr-defined]
return c
def _new_logic(c: TestClient, name: str) -> str:
row = {
"키": "",
"이름": name,
"결과단위": "원/㎥",
"출처": "자체",
"입력": [],
"중간": [],
"호표": [],
"끝수": None,
}
ver = store.read("로직_자체.json")[1] if (store.FOLDER / "로직_자체.json").is_file() else ""
res = c.post(
"/api/m01/save",
json={
"files": [
{"file": "로직_자체.json", "version": ver, "changes": [{"op": "add", "row": row}]}
]
},
)
assert res.status_code == 200, res.text
return res.json()["added"][-1]["key"] # fmt: skip
def _doc(c: TestClient, key: str, sub: str | None = None) -> None:
doc = c.get("/api/m01/logic/doc", params={"key": key}).json()["문서"]
doc["금액"] = [
{"이름": "보통인부", "단위": "인", "비목": "노무비", "수량": "1", "단가": "w_보통"}
]
doc["변수"] = [
{"기호": "w", "아래": "보통", "위": "", "연결": {"갈래": "요소", "키": "LB000002"}}
]
if sub:
doc["변수"].append(
{"기호": "B", "아래": "", "위": "", "연결": {"갈래": "단가산출", "키": sub, "입력": {}}}
)
res = c.post("/api/m01/logic/doc/save", json={"문서": doc})
assert res.status_code == 200, res.text
def _remove(c: TestClient, key: str, ok: bool = False):
one = c.get("/api/m01/logic", params={"key": key}).json()
return c.post(
"/api/m01/logic/remove",
json={"key": key, "file": one["file"], "version": one["version"], "개정확인": ok},
)
def test_로직_삭제_서버_정본_바로(client: TestClient) -> None:
a, b = _new_logic(client, "sub3시험 지울 것"), _new_logic(client, "sub3시험 부르는 것")
_doc(client, a)
_doc(client, b, sub=a)
# 다른 문서가 부름 → 막힘 · 까닭 = 그 문서
res = _remove(client, a)
assert res.status_code == 409 and any(b in x for x in res.json()["detail"]["쓰는곳"])
# M02 단가산출 바탕 = 이 로직 → 막힘
client.sk.write_text(json.dumps({"이름": "sub3임시", "바탕": {"로직": b}}), encoding="utf-8") # type: ignore[attr-defined]
res = _remove(client, b)
assert res.status_code == 409 and res.json()["detail"]["쓰는곳"] == [
"M02 단가산출 개인 시험 sub3임시"
]
client.sk.unlink() # type: ignore[attr-defined]
# 부르는 쪽부터 지움 — 로직 줄 · 수식 문서 둘 다 없어짐
assert _remove(client, b).status_code == 200
assert _remove(client, a).status_code == 200
for k in (a, b):
assert client.get("/api/m01/logic", params={"key": k}).status_code == 404
assert k not in doc_screen.docs()
# 정본 로직 = 개정 확인 409(아무것도 안 지움)
res = _remove(client, "GF000099")
assert res.status_code == 409 and res.json()["detail"].get("개정")
assert "GF000099" in doc_screen.docs()
def test_정본_폴더_안_바뀜() -> None:
assert doc_screen.folder() == ROOT / "resources/master_data/수식문서"
assert store.FOLDER == REAL
def test_머리_단추_다섯_저장_한_번() -> None:
lab = _src("M01_MasterData/M01_MasterData_UI_LogicLab.ts")
assert "page: () => persist()," in lab
assert (
"removeLogic(key, one.file, one.version, ok)" in lab and "show(null); // 로직 목록" in lab
)
assert "if (opened) await stack.save();" in lab # [저장] = 로직 줄 + 수식(여는 중이면 기다림)
assert "await loading;" in _src("A00_Common/line_stack_screen.ts")
assert "await stack.discard();" in lab # [되돌림] = 둘 다
assert "n === 0 && !stack.dirty()" in lab
for t in ('tl("New")', "copyButton", 'tx("Bar_Delete")', "discardButton", "saveButton"):
assert t in lab.split("buttons: [")[1].split("]")[0]
text = _src("M01_MasterData/M01_MasterData_UI_Logic_Text.ts")
assert 'Bar_Discard: ["되돌림", "Revert"]' in text and "캐시 삭제" not in text
screen = _src("A00_Common/line_stack_screen.ts")
assert "edit() && key && !opt.page" in screen and "opt.page?.();" in screen
def test_요소키_기호_이름_0_전수() -> None:
docs = doc_screen.docs()
assert len(docs) >= 1518
bad = [
(k, v.get("기호"), v.get("아래"))
for k, d in docs.items()
for v in d.get("변수") or []
if CODE.match(str(v.get("아래") or ""))
or CODE.match(str(v.get("열쇠") or "").split("_")[-1])
]
assert bad == []
ids = {doc_screen.md.var_id(v) for v in docs["GF000011"]["변수"]}
assert {"k_하베스터", "P_하베스터", "k_굴착기", "P_굴착기"} <= ids
import master_doc_convert as cv
whole = store.loaded()[1]
for key in ("GF000011", "GF000145", "GC001043"):
got, _ = cv.convert(whole, store.logic(key)["logic"])
assert not [v for v in got["변수"] if CODE.match(str(v.get("아래") or ""))], key
def test_단위_고르기_공용_친글() -> None:
edit = _src("A00_Common/line_stack_edit.ts")
assert "createUnitPick({" in edit and "free: true," in edit
pick = _src("M02_MasterTemplete/DataFlow/M02_DF_UnitPick.ts")
assert "if (opts.free && typed && !all.includes(typed)) shown.push(typed);" in pick
main = _src("main.py")
at = main.index("M02_MasterTemplete.DataFlow.M02_DF_Router")
assert "dependencies=protected," in main[at : at + 120]
def test_시험_페이지_걷음_화면_차림() -> None:
assert not (ROOT / "tools/line-stack-test").exists()
for rel in ("main.py", "vite.config.ts", "M01_MasterData/M01_MasterData_UI_Side.ts"):
assert "line-stack-test" not in _src(rel), rel
edit = _src("A00_Common/line_stack_edit.ts")
assert "createCheckbox" not in edit and "Gana" not in edit # 가나다 체크 걷음
assert '{ icon: "plus", title: lt("Add_Line"), size: 24 }' in edit # [+] 메뉴
assert '"틀"' not in edit and "K_틀" not in edit # 틀 찾기 걷음
assert "every.has(id) && !taken.has(id)" in edit # 만약 안 탄 갈래 숨김
assert "String(mat?.검색어 ?? line.이름" in edit # 이름 단추 = 그 이름 찾음
text = _src("A00_Common/line_stack_text.ts")
assert '["단가산출", "Unit price"]' in text and '["근거 글", "Basis note"]' in text
assert "비우면 번호 없음" not in text and "식 고치기" not in text
out = _src("A00_Common/line_stack_out.ts")
assert "s ? opener(s, eqText(bundle[c]))" not in out and "fold(s, title)" in out
css = _src("A00_Common/line_stack.css")
assert ".lstk__foot {\n position: sticky;\n inset-block-end: 0;" in css
assert "html:not" in _src("M01_MasterData/M01_MasterData_UI_LogicLab_Side.css")
for f in (ROOT / "A00_Common").glob("line_stack*.ts"):
assert len(f.read_text(encoding="utf-8").splitlines()) <= 700, f.name