요구 651 · 653 — 계산 공식 참고용 흐름도(① M01 로직 상세 오른쪽 접는 패널)를 걷음. ② M02 일위대가 다이어그램은 그대로.
지움(64 파일 · 18,666 줄 · 되돌릴 길 = git 이력)
- M01 6 파일 1,064 줄 — Store_Flow.py · UI_LogicLab_Flow.ts/.css · FlowRows.ts · FlowView.ts/.css
- M02_MasterTemplete/DataFlow 43 파일 12,799 줄 — 흐름도 화면 · 셈(Compute · Convert · Lookup · Model · Eval …)
- 시험 15 파일 4,803 줄 — test_m01_logic_flow{view,_engine,_project,_save} · test_m02_df_*(값 찾기 values_old 뺌) — 지운 흐름 코드만 지키던 것
남김(바닥) — DataFlow M02_DF_UnitPick(일위대가 셋) · M02_DF_Values(M02_Item_Prices) · M02_DF_Round · _standard 표 ·
M02_DF_Router = GET /api/m02/df/standards 하나 · M02_DF_Api = cfStandards 하나 · CfRound 꼴은 Round 안으로
고침
- M01 Router — /save = Store_Make.save(개정 확인 · 자체 파일 새 줄 GX · added 그대로) · /calc /text /logic/auto = 옛 셈만 · /logic/flow 둘 지움
- master_formula — 흐름 훅 지움 · 줄에 「흐름」 이 남으면 「흐름도 로직은 걷음」 까닭 오류(저장된 흐름 로직 0 — 마스터 · storage 다시 확인)
- Sangeun_Calc._defaults = 옛 자동값 · M02_Logic_Lib_Read /logic/flow 지움 · main.py 주석
- 화면 — LogicLab(startFlow · scheduleFlow · 예시값 잇기 지움 · [신규 로직] 줄 = 입력 · 중간 · 호표 빈 목록 + 끝수 null) ·
Side(원문 패널 하나) · Source(머리 칸 이름 m01lab-src__top/__title · DF_Panel.css 안 씀) · Detail · View(nowInputs 지움) · Sheet(흐름 갈래 지움) · 글
- 시험 — 지운 파일을 가리키던 목록 넷(spreadsheet_size · 208_1 · 588 · input_prefs) · 717_1 흐름 길 두 줄 · values_old 흐름 셈 부분 지움
- test_713_3_list 기대 — 로직 1,461 → 1,515 · 묶음 307 → 335 · 작업량 21 → 65 · 부르는 506 → 512(782-1 · 2 · 3 건설 기초 로직 54 더함 ·
옛 키만 세면 307 · 21 · 506 그대로)
- 새 지킴 test_794_1b_flow_gone — 흐름 길 404 · 표준 표 남음 · 신규 로직 자체 GX · 흐름 남은 줄 까닭 오류
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
185 lines
7.5 KiB
Python
185 lines
7.5 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""PLAN 208-1 — 단가산출 로직 「별칭」 칸 · 로직 찾기는 이름 + 별칭.
|
||
|
||
① 서버 찾기 맞춤(`M01_MasterData_Store_Find.hit`) ↔ 화면 공용 맞춤(`ui_template_logic_find.ts`) 거울
|
||
② 읽기(없으면 빈칸) · 저장 왕복 — 임시 사본 폴더만(정본 마스터 파일은 안 바뀜)
|
||
③ M01 목록 「별칭」 열(이름 옆) · 쓰는 곳마다 공용 맞춤을 부름
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import shutil
|
||
import subprocess
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
from fastapi import FastAPI
|
||
from fastapi.testclient import TestClient
|
||
|
||
from M01_MasterData import M01_MasterData_Router as router_module
|
||
from M01_MasterData import M01_MasterData_Store as store
|
||
from M01_MasterData.M01_MasterData_Store_Find import hit
|
||
|
||
ROOT = Path(__file__).resolve().parents[2]
|
||
REAL = store.FOLDER
|
||
LOGIC_KEY = "GC000091" # 공통 3-3-1 암발파(미진동굴착 TYPE-Ⅰ)
|
||
LOGIC_FILE = "로직_건설품셈_03장_토공사.json"
|
||
|
||
ROW = {
|
||
"키": "GF000001",
|
||
"원문번호": "9-1-1",
|
||
"이름": "흙깎기(보통토사)",
|
||
"별칭": "절토, 터파기 기계",
|
||
}
|
||
# (찾는 글, 맞음, 별칭으로 맞은 것)
|
||
CASES = [
|
||
("흙깎기", True, ""),
|
||
("흙 깎기", True, ""), # 띄어쓰기 무시 · 낱말마다
|
||
("절토", True, "절토"),
|
||
("터파기", True, "터파기 기계"),
|
||
("터파기기계", True, "터파기 기계"), # 별칭 안 띄어쓰기 무시
|
||
("절토 기계", True, "절토, 터파기 기계"), # 낱말이 여러 별칭에 나뉘어 듦
|
||
("흙깎기 절토", True, "절토"), # 이름 + 별칭
|
||
("gf000001", True, ""),
|
||
("9-1", True, ""),
|
||
("성토", False, ""),
|
||
("", True, ""),
|
||
]
|
||
|
||
|
||
def _node(src: str) -> str:
|
||
if shutil.which("node") is None:
|
||
pytest.skip("node 가 없음")
|
||
done = subprocess.run(
|
||
["node", "--experimental-strip-types", "--input-type=module", "-e", src],
|
||
capture_output=True,
|
||
text=True,
|
||
encoding="utf-8",
|
||
timeout=60,
|
||
cwd=ROOT,
|
||
)
|
||
assert done.returncode == 0, done.stderr
|
||
return done.stdout
|
||
|
||
|
||
def test_서버_맞춤_이름_별칭_여러_별칭() -> None:
|
||
for q, ok, _ in CASES:
|
||
assert hit(ROW, q) is ok, q
|
||
assert hit({**ROW, "별칭": ""}, "절토") is False # 별칭 없음 = 빈칸
|
||
assert hit({k: v for k, v in ROW.items() if k != "별칭"}, "흙깎기") is True
|
||
|
||
|
||
def test_화면_맞춤_거울_서버와_같음() -> None:
|
||
url = (ROOT / "ui_template" / "ui_template_logic_find.ts").as_uri()
|
||
src = f"""
|
||
import {{ matchLogic, aliasText, aliasesOf, aliasNote }} from "{url}";
|
||
const row = {json.dumps(ROW, ensure_ascii=False)};
|
||
const cases = {json.dumps([c[0] for c in CASES], ensure_ascii=False)};
|
||
console.log(JSON.stringify({{
|
||
hits: cases.map((q) => matchLogic(q, row)),
|
||
text: aliasText(" 절토 ,, 터파기 기계,"),
|
||
none: aliasesOf(undefined),
|
||
note: [aliasNote("절토"), aliasNote("")],
|
||
chapter: matchLogic("토공", {{ ...row, 상세구분: "09장 토공" }}).ok,
|
||
}}));
|
||
"""
|
||
got = json.loads(_node(src))
|
||
for (q, ok, alias), one in zip(CASES, got["hits"]):
|
||
assert one == {"ok": ok, "alias": alias}, q
|
||
assert one["ok"] is hit(ROW, q), q # 서버와 같은 판정
|
||
assert got["text"] == "절토, 터파기 기계"
|
||
assert got["none"] == []
|
||
assert got["note"] == ["별칭 절토", ""]
|
||
assert got["chapter"] is True # 구분 · 상세구분은 화면만 더 봄
|
||
|
||
|
||
@pytest.fixture
|
||
def client(tmp_path: Path, monkeypatch) -> TestClient:
|
||
for path in REAL.glob("*.json"):
|
||
if not path.name.startswith("_") or path.name == store.mk.BOOK.name:
|
||
shutil.copy(path, tmp_path / path.name)
|
||
monkeypatch.setattr(store, "FOLDER", tmp_path)
|
||
app = FastAPI()
|
||
app.include_router(router_module.router)
|
||
return TestClient(app)
|
||
|
||
|
||
def _logics(client: TestClient, **params) -> list[dict]:
|
||
res = client.get("/api/m01/logics", params=params)
|
||
assert res.status_code == 200, res.text
|
||
return res.json()["logics"]
|
||
|
||
|
||
def _save(client: TestClient, row: dict) -> None:
|
||
one = client.get("/api/m01/logic", params={"key": LOGIC_KEY}).json()
|
||
part = {
|
||
"file": one["file"],
|
||
"version": one["version"],
|
||
"changes": [{"op": "edit", "key": LOGIC_KEY, "row": row}],
|
||
}
|
||
body = {"files": [part], "개정확인": True} # 정본 줄 고침 = 개정 확인(713-1)
|
||
res = client.post("/api/m01/save", json=body)
|
||
assert res.status_code == 200, res.text
|
||
|
||
|
||
def test_읽기_빈칸_저장_왕복_임시_사본만(client: TestClient) -> None:
|
||
real = (REAL / LOGIC_FILE).read_bytes()
|
||
listed = _logics(client)
|
||
assert listed and all(isinstance(x["별칭"], str) for x in listed) # 없으면 빈칸
|
||
one = client.get("/api/m01/logic", params={"key": LOGIC_KEY}).json()
|
||
base = one["logic"]
|
||
assert "별칭" not in base # 마스터 파일에 일괄로 쓰지 않음
|
||
|
||
_save(client, {**base, "별칭": "미진동 발파, 암깨기"})
|
||
got = [x for x in _logics(client, q="암깨기") if x["키"] == LOGIC_KEY]
|
||
assert got and got[0]["별칭"] == "미진동 발파, 암깨기"
|
||
assert LOGIC_KEY in [x["키"] for x in _logics(client, q="미진동 발파")]
|
||
found = client.get("/api/m01/elements", params={"group": "로직", "q": "암깨기"}).json()
|
||
assert [x["별칭"] for x in found["items"] if x["ref"] == LOGIC_KEY] == ["미진동 발파, 암깨기"]
|
||
saved = json.loads((store.FOLDER / LOGIC_FILE).read_text(encoding="utf-8"))
|
||
assert next(r for r in saved["줄"] if r["키"] == LOGIC_KEY)["별칭"] == "미진동 발파, 암깨기"
|
||
|
||
_save(client, base) # 지우면 칸째 빠짐 · 찾기에도 안 걸림
|
||
assert LOGIC_KEY not in [x["키"] for x in _logics(client, q="암깨기")]
|
||
after = json.loads((store.FOLDER / LOGIC_FILE).read_text(encoding="utf-8"))
|
||
assert after == json.loads(real.decode("utf-8")) # 왕복 뒤 내용 그대로
|
||
assert (REAL / LOGIC_FILE).read_bytes() == real # 정본은 안 건드림
|
||
|
||
|
||
def _src(rel: str) -> str:
|
||
return (ROOT / rel).read_text(encoding="utf-8")
|
||
|
||
|
||
def test_M01_목록_별칭_열은_이름_옆_고치면_캐시() -> None:
|
||
ts = _src("M01_MasterData/M01_MasterData_UI_Logic_List.ts")
|
||
cols = ts[ts.index("const COLS") : ts.index("as const")]
|
||
names = [x.strip().strip('"') for x in cols[cols.index("[") + 1 :].split(",")]
|
||
assert names[names.index("이름") + 1] == "별칭" # 이름 바로 옆
|
||
assert "aliasCell(item" in ts and "onAlias(item, next)" in ts
|
||
assert "e.stopPropagation()" in ts # 별칭 칸을 눌러도 로직이 안 열림
|
||
lab = _src("M01_MasterData/M01_MasterData_UI_LogicLab.ts")
|
||
assert "const onAlias = async" in lab and "list.setMarks(marks, extra, aliases)" in lab
|
||
# 자동저장 없음 — 캐시(drafts)에만 · [저장] 이 보냄
|
||
body = lab[lab.index("const onAlias = async") : lab.index("const onDelete = async")]
|
||
assert "saveFiles" not in body and "persist()" in body
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"rel",
|
||
[
|
||
"M01_MasterData/M01_MasterData_UI_Logic_List.ts", # M01 로직 목록
|
||
"M01_MasterData/M01_MasterData_UI_Logic_Pick.ts", # M01 요소 찾기 창 · 로직 그룹
|
||
"M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts", # M02 · B09 [로직 더하기]
|
||
],
|
||
)
|
||
def test_쓰는_곳마다_공용_맞춤을_부름(rel: str) -> None:
|
||
ts = _src(rel)
|
||
assert 'from "@ui/ui_template_logic_find"' in ts
|
||
assert "matchLogic(" in ts
|
||
|
||
|
||
def test_서버_찾기도_한_벌() -> None:
|
||
py = _src("M01_MasterData/M01_MasterData_Store.py")
|
||
assert "import hit as _hit" in py and "def _hit" not in py
|