Files
Aislo/resources/tester/test_804_4_line_stack_take.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

260 lines
12 KiB
Python
Raw 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.
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
"""PLAN 804-4 — 줄 쌓기 다른 단가산출 · 일위대가 복사해 펼침(요구 680-1 · 681) — 임시 storage.
① M01 줄 쌓기 = M01 전체만(찾기 창 탭 · 복사 길 없음) · M01 문서 길은 M02 키를 모름
② M01 단가산출 복사 = 전체 항목 · 줄 · 옵션 굳힘(옵션 0) · 금액 = 원본 · 원본 연결 없음
③ M02 SK 복사 = 그 SK 수식 문서 금액 · 항목 그대로
④ 일위대가(SC) 복사 = 항목 여러 개 · 금액 = 일위대가 계 · 고정(노임 · 손 단가) · 율 · 안 SK(× 수량 ÷ 배)
⑤ 앞 줄 있는 문서에 넣음 = 앞 금액 + 원본(율 밑 = 그 조각만 · 끝수 1 원)
⑥ 권한 — 위 층만(남의 개인 층 403) · 저장 = 서버 정본 셈 같음 · M01 노임 바뀜 → [단가 갱신] 뒤 따라감
"""
from __future__ import annotations
import json
from decimal import Decimal
from pathlib import Path
import pytest
from M02_MasterTemplete import M02_Doc_Copy as dc
from M02_MasterTemplete import M02_Doc_Sk_Path as path
from M02_MasterTemplete import M02_Doc_Take as tk
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from resources.tester.test_795_1_sk_copy import _fake, _live, _sk
from resources.tester.test_795_2_sc_copy import _sc
from resources.tester.test_804_2_line_stack import _ts
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
ROOT = Path(__file__).resolve().parents[2]
FORM = "GC000211" # 합판거푸집(옵션 많음)
DIG = "GF000099" # 측구터파기(굴착기 시간당 경비 부름)
STONE = "GF000219" # 원/㎡
LB = "LB000002" # 보통인부
ME = {"층": "personal", "주인": "7/42"}
def _norm(v):
return json.loads(json.dumps(dc.ml._norm(v), ensure_ascii=False, default=str))
def _take(folder, src):
with store.at(folder):
return _norm(tk.take(src))
def _into(doc, *taken):
return _ts("복사", [{"문서": doc, "받음": list(taken)}])[0]
def _money(doc):
d = json.loads(
json.dumps(dc.ml._norm(doc), default=str), parse_float=Decimal, parse_int=Decimal
)
return dc.mc.calc(d, dc.ml.Links(_live(), dc.folder()))
def _rid(key, at=ME):
return refs.ref_id({**at, "키": key})
def _src(rel):
return (ROOT / rel).read_text(encoding="utf-8")
def test_M01_은_M01전체만():
pick = _src("A00_Common/line_stack_pick.ts")
assert 'kinds: copy ? ["logic", "sk", "pre"] : [want], // M01 = M01 전체만(680-1)' in pick
assert 'const copy = kind === "단가산출" && m02 ? m02 : null;' in pick
lab = _src("M01_MasterData/M01_MasterData_UI_LogicLab.ts")
assert (
'base: "/m01/logic/doc",' in lab and 'cache: "m01_doc_drafts",' in lab
) # 804-7 page = 페이지 [저장]
tabs = _src("M02_MasterTemplete/M02_MasterTemplete_Sangeun_Tabs.ts")
assert "scope: opt.scope, // 804-4" in tabs
screen = _src("A00_Common/line_stack_screen.ts")
assert "cp && opt.scope && key" in screen # 복사 길 = M02 저장한 SK 만
# M01 문서 길 = M02 키 모름(부르면 「없는 단가산출」)
doc = dc.md.blank("X", "x")
doc["변수"] = [
{"기호": "B", "아래": "", "위": "", "연결": {"갈래": "단가산출", "키": _rid("SK-000001")}}
]
doc["금액"] = [{"이름": "남", "비목": "묶음", "수량": "1", "단가": "B"}]
got = dc.mc.calc(doc, dc.ml.Links(_live(), dc.folder()))
assert not got["셈됨"] and "없는 단가산출" in json.dumps(got["표지"], ensure_ascii=False)
def test_M01단가산출_복사_전체_옵션굳힘(world):
me = world["personal"]
t = _take(me, FORM)
assert t["출처"] == f"M01 {FORM}" and [p["갈래"] for p in t["조각"]] == ["문서"]
g = _into(dc.md.blank("SK-000009", "빈"), t)
assert g["옵션"] == [] and g["왕복"] and "번지" not in g["못"]
assert g["항목"][0]["줄"][0] == f"글:M01 {FORM} 「{t['이름']}」에서 복사"
lines = [x for it in g["항목"] for x in it["줄"]]
assert len(lines) > 5 and any(x.startswith("공식:") for x in lines) # 공식 묶음까지 전체
assert _money(g["문서"])["계"] == _money(dc.folder()[FORM])["계"]
# 원본 연결 없음 — 복사본에 원본 키 단가산출 연결 없음 · 기호 모두 이 문서
keys = {(v.get("연결") or {}).get("키") for v in g["문서"]["변수"]}
assert FORM not in keys
def test_SK_복사_그_SK_금액(world):
me = world["personal"]
key = _sk(me, 바탕={"로직": FORM}, 입력값={"유형": "복잡", "사용횟수": "3회"})
with store.at(me):
want = _norm(path.read(key))["셈"]["계"]
t = _take(me, _rid(key))
assert t["출처"] == f"개인 {key}" and t["조각"][0]["갈래"] == "문서"
g = _into(dc.md.blank("SK-000009", "빈"), t)
assert g["옵션"] == [] and _money(g["문서"])["계"] == want
# 옵션 「복잡」 그대로 굳힘
assert any(x.get("식") == "'복잡'" for x in g["문서"]["식"])
def _combo(world):
me = world["personal"]
sk = _sk(me, 바탕={"로직": STONE})
fixed = {"이름": "보통인부", "단위": "인", "단가": {"노무비": 0}, "이음": {"노임": LB}}
hand = {"이름": "sub3임시 잡자재", "단위": "식", "단가": {"재료비": 1234}, "이음": {}}
rate = {"이름": "공구손료", "단위": "%", "단가": {}, "비율": {"기준": "노무비", "비목": "경비"}}
lines = [
{"로직": DIG, "수량": {"손": 2}},
{"로직": "", "고정": fixed, "수량": {"손": 0.3}},
{"로직": "", "고정": hand, "수량": {"손": 2}},
{"참조": {**ME, "키": sk}, "수량": {"손": 3}},
{"로직": "", "고정": rate, "수량": {"손": 3}},
]
return me, sk, _sc(me, lines)
def test_일위대가_복사_여러항목_금액같음(world):
me, sk, sc = _combo(world)
t = _take(me, _rid(sc))
kinds = [p["갈래"] for p in t["조각"]]
assert kinds == ["문서", "인력", "손", "문서", "율"], kinds
assert t["조각"][0]["곱"] == "2" and t["조각"][3]["곱"] == "3"
assert t["조각"][4]["줄"]["단가"] == "노무비 × 3 %"
g = _into(dc.md.blank("SK-000009", "빈"), t)
assert g["옵션"] == [] and g["왕복"] and "번지" not in g["못"]
titles = [it["제목"] for it in g["항목"]]
assert titles[0] == t["이름"] and len(g["항목"]) >= 4 # 머리 + 문서 둘 + 줄 조각 항목
got = float(_money(g["문서"])["계"])
# 일위대가 계 — 끝수 차례만 다름(원본 = 문서 계를 버린 뒤 × 수량 · 복사 = 줄마다 × 수량 뒤 버림)
assert got == pytest.approx(float(t["계"]), abs=10)
# 율 밑 = 그 일위대가 번지 합(앞 줄 섞임 없음)
rate = g["문서"]["금액"][-1]
assert rate["이름"] == "공구손료" and "번지.복사" in rate["단가"]
def test_앞줄있는문서_넣음_율밑안섞임(world):
me = world["personal"]
gc = dc.folder()[FORM]
one = _take(me, DIG)
g = _into(gc, one)
diff = float(_money(g["문서"])["계"]) - float(_money(gc)["계"])
assert diff == pytest.approx(float(_money(dc.folder()[DIG])["계"]), abs=1)
# 일위대가(율 줄 있음) 둘을 잇달아 = 각자 계 합
_, _, sc = _combo(world)
t = _take(me, _rid(sc))
two = _into(dc.md.blank("SK-000009", "빈"), t, t)
one_sc = float(_money(_into(dc.md.blank("SK-000009", "빈"), t)["문서"])["계"])
assert float(_money(two["문서"])["계"]) == pytest.approx(2 * one_sc, abs=2)
def test_권한_저장_단가갱신(world, monkeypatch):
me, sk, sc = _combo(world)
with pytest.raises(store.StoreError) as e: # 회사 층에서 개인 층 = 위 층 아님
_take(world["company"], _rid(sc))
assert e.value.status == 403
with pytest.raises(store.StoreError):
_take(me, "참조|personal|7/43|SC-000001") # 남의 개인 층
# 저장 = 서버 정본 셈 · 다시 읽어 같음
target = _sk(me) # 빈 SK(바탕 · 줄 없음) — 줄 쌓기로 처음부터 복사해 채움
with store.at(me):
read = _norm(path.read(target))
g = _into(read["문서"], _take(me, _rid(sc)))
with store.at(me):
saved = _norm(path.save(target, {"문서": g["문서"], "판": read["판"]}))
again = _norm(path.read(target))
assert saved["셈"]["계"] == again["셈"]["계"] == _money(g["문서"])["계"]
# 페이지 [저장](옛 칸 · 이름) 뒤에도 고친 문서 그대로(빈 SK 씨로 덮지 않음)
with store.at(me):
file = store.read("sangeun", target)
store.write("sangeun", target, file["판"], {**file["문서"], "이름": "sub3임시 이름바꿈"})
again = _norm(path.read(target))
assert again["셈"]["계"] == saved["셈"]["계"] and again["사본"]["따로"]
# M01 노임 바뀜 → [단가 갱신] 미리 셈 = 그 자원 단가 따라감(구조 · 품 그대로) — 값 줄 있는 SK(바탕 = 보통인부 든 로직)
other = _sk(me, 바탕={"로직": STONE})
with store.at(me):
read = _norm(path.read(other))
g = _into(read["문서"], _take(me, _rid(sc)))
with store.at(me):
again = _norm(path.save(other, {"문서": g["문서"], "판": read["판"]}))
row = dict(_live().get(LB))
row["값"] = row["값"] + 1000
_fake(monkeypatch, [row])
with store.at(me):
got = _norm(path.preview(other, {"할일": "단가갱신"}))
assert any(x["키"] == LB for x in got["바뀐자원"])
assert (
got["후"]["계"] > again["셈"]["계"] + 1000 * 0.3
) # 보통인부 줄들(복사한 0.3 인 포함) 따라감
assert got["문서"]["금액"] == again["문서"]["금액"]
def test_끌어옮김_번지이름(world):
"""804-5 — 줄 끌기(항목 안 · 사이) · 항목 끌기 = 되돌려 끌면 같은 문서 · 율 줄 번지 = 읽는 이름."""
gc = dc.folder()[DIG] # 항목 둘(인력 · 굴착기)
one = _ts("옮김", [{"문서": gc, "옮김": []}])[0]
first = one["항목"][0]["줄"][0]
gone = _ts("옮김", [{"문서": gc, "옮김": [["0.0", "1.0"]]}])[0]
assert (
gone["항목"][1]["줄"][0] == first
and len(gone["항목"][0]["줄"]) == len(one["항목"][0]["줄"]) - 1
)
back = _ts("옮김", [{"문서": gc, "옮김": [["0.0", "1.0"], ["1.0", "0.0"]]}])[0]
assert back["문서"] == one["문서"]
# 같은 항목 안 아래로(빼기 전 차례) — 첫 줄을 끝 자리 = 둘째로
inner = _ts("옮김", [{"문서": gc, "옮김": [["0.0", "0.2"]]}])[0]
assert inner["항목"][0]["줄"] == one["항목"][0]["줄"][::-1]
n = len(one["항목"])
rot = _ts("옮김", [{"문서": gc, "옮김": [["0", str(n)]]}])[0]
assert rot["항목"][-1] == one["항목"][0] and rot["항목"][0] == one["항목"][1]
assert (
_money(rot["문서"])["계"] == _money(gc)["계"]
) # 항목만 옮김 = 금액 같음(율 줄은 그 항목 안)
# 율 줄 번지 — 일위대가 복사 = 번지마다 읽는 이름(「복사N」 아님)
me, sk, sc = _combo(world)
g = _into(dc.md.blank("SK-000009", "빈"), _take(me, _rid(sc)))
names = _ts("번지", [{"문서": g["문서"]}])[0]
places = {m["번지"] for m in g["문서"]["금액"] if m.get("번지")}
assert places and set(names) == places
assert all(v and not v.startswith("복사") for v in names.values()), names
def test_표값_화면이름_전수():
"""요구 683 — 표 값 기호(「Q_수량2^표」) = 화면 이름(「Q_작업반장」) · 겹침 없음 · 식 글 왕복 = 같은 식(1,518 전수)."""
docs = dc.folder()
keys = sorted(docs)
got = _ts("별칭", [{"문서": docs[k]} for k in keys])
named = 0
for k, g in zip(keys, got):
assert not g["틀림"], (k, g["틀림"][:3])
alias = g["별칭"]
assert len(set(alias.values())) == len(alias), k # 겹치면 2 · 3
assert all("^표" not in v and k.endswith("^표") for k, v in alias.items()), k
named += len(alias)
assert named > 500
gc = dict(zip(keys, got))[FORM]["별칭"]
assert gc and "Q_보통인부22" not in gc.values() # 번호 꼬리 뗀 뒤 2 · 3
# 문서 기호는 그대로(화면만) — 원본 문서 변수에 별칭 이름 없음
names = {
v["기호"]
+ (f"_{v['아래']}" if v.get("아래") else "")
+ (f"^{v['위']}" if v.get("위") else "")
for v in docs[FORM]["변수"]
}
assert not set(gc.values()) & names