Merge remote-tracking branch 'origin/sub_laptop_2' into main_laptop_1

This commit is contained in:
2026-10-08 12:16:10 +09:00
20 changed files with 239 additions and 48 deletions
+46
View File
@@ -90,3 +90,49 @@ def test_머리_고침은_쓸_수_있는_층만_나무로():
assert page.index("...(canEdit(layer, isAdmin)\n ? {\n onHeadRename") > 0
tree = _text(M02 / "M02_MasterTemplete_TreePanel.ts")
assert "if (opt.canEdit && (await ready())) await edit(body, colId);" in tree
def test_일위대가_합계_줄_번호_칸(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
"""86-3 — `structure_numbers` 의 일위대가 합계: 반영합 `계` · 없으면 기준합 `계` + 합계기준 ·
담은 로직 0 · 계산값 없음 = 칸 없음(목록 `labor` 합계와 같은 규칙)."""
from M02_MasterTemplete import M02_MasterTemplete_Store as store
monkeypatch.setattr(store, "FOLDER", tmp_path)
(tmp_path / "labor").mkdir()
docs = {
"a": {"일위대가번": "SU-000001", "담은로직": ["x"], "계산값": {"반영합": {"계": 1500.4}}},
"b": {
"일위대가번": "SU-000002",
"담은로직": ["x"],
"계산값": {"반영합": {"계": 0}, "기준합": {"계": 900}},
},
"c": {"일위대가번": "SU-000003", "담은로직": [], "계산값": {"반영합": {"계": 5}}},
"d": {"일위대가번": "SU-000004", "담은로직": ["x"], "계산값": {}},
}
for name, doc in docs.items():
(tmp_path / "labor" / f"{name}.json").write_text(json.dumps(doc), encoding="utf-8")
got = store.structure_numbers()
assert (got["a"]["일위대가합계"], got["a"]["합계기준"]) == (1500.4, False)
assert (got["b"]["일위대가합계"], got["b"]["합계기준"]) == (900.0, True)
assert "일위대가합계" not in got["c"] and "일위대가합계" not in got["d"]
assert got["d"]["일위대가번"] == "SU-000004" and got["d"]["도번"] == ""
def test_일위대가_합계_줄_표_부품과_연결():
sheet = _text(SHEET / "ui_template_sheet.ts")
assert "laborSumLabel: opts.laborSumLabel," in sheet
ops = _text(SHEET / "ui_template_sheet_ops.ts")
assert 'export const KEY_SUM = "s:laborsum";' in ops
assert "[KEY_PRICE, KEY_DRAWING, KEY_AREA, KEY_SUM]" in ops # 읽기만 — 고치기 · 쓰기 막힘
assert "KEY_SUM" not in re.search(r"MASTER_ROWS = \[[^\]]*\]", ops).group(
0
) # 방향키 · 고르기 밖
render = _text(SHEET / "ui_template_sheet_render.ts")
assert "state.combo && state.laborSumLabel" in render and "masterRow(KEY_SUM)" in render
main = _text(M02 / "M02_MasterTemplete_UI_Main.ts")
assert "laborSumLabel: (colId: string) => laborSumText(struct.numbers()[colId])" in main
assert "onNumbers: () => sheet?.recalc()" in main # 카드 [저장] 뒤 다시 읽기
struct = _text(M02 / "M02_MasterTemplete_Structure.ts")
assert "fetchStructureNumbers().then(" in struct and "opts.onNumbers?.()" in struct
cards = _text(M02 / "M02_MasterTemplete_ItemCards.ts")
assert "opts.onSaved?.();" in cards
+1 -1
View File
@@ -74,7 +74,7 @@ def test_sub2_가_부르는_이름과_모양():
), name
assert "fetchStructureNumbers = (): Promise<Record<string, StructureNumbers>>" in body
assert re.search(
r"interface StructureNumbers \{\s*도번: string;\s*면적번: string;\s*/\*\* 일위대가[^*]*\*/\s*일위대가번: string;\s*\}",
r"interface StructureNumbers \{\s*도번: string;\s*면적번: string;\s*/\*\* 일위대가[^*]*\*/\s*일위대가번: string;(\s*/\*\*[^*]*\*/)?\s*일위대가합계\?: number;\s*합계기준\?: boolean;\s*\}",
body,
)
assert re.search(r'export type Kind = [^;]*"area" \| "labor"', body) # 재료산출 없앰
+35 -2
View File
@@ -198,6 +198,39 @@ def test_빈_층에서_폴더_항목_깊이_1_2_5_이름_옮기기_지우기(wor
assert json.loads((root / "table/구조물집계표.json").read_bytes())["열"] == []
def test_옮기기_같은_이름은_이름_번호로_저장(world):
"""옮기기(항목 · 폴더) 같은 폴더 같은 이름 = 말없이 「이름(1)」 「이름(2)」 · 키 · 번호 그대로.
사용자가 직접 친 이름(더하기 · 이름 바꾸기) 겹침은 그대로 409."""
root = world["storage"] / "7" / "42" / "templates"
t = Tree(_as(world), "personal")
t.do(op="add-folder", 경로=[], 이름="A")
t.do(op="add-folder", 경로=[], 이름="B")
for f in ("A", "B"):
t.do(op="add-item", 경로=[f], 이름="같음")
t.do(op="add-item", 경로=["B"], 이름="같음(1)")
t.do(op="add-folder", 경로=["A"], 이름="하위")
t.do(op="add-folder", 경로=["B"], 이름="하위")
t.do(op="add-item", 경로=["B", "하위"], 이름="안쪽")
mine = t.item(["A", "같음"])
note = t.do(op="move", 키=mine["key"], 새경로=["B"])["알림"]
assert note["경로"] == ["B", "같음(2)"] and note["바뀐이름"] == ["같음(2)"] # (1) 도 있어서
moved = t.item(["B", "같음(2)"])
assert moved["key"] == mine["key"] and _numbers(root, moved["열"]) == _numbers(root, mine["열"])
# 겹침 없으면 이름 그대로 · 바뀐이름 없음 · 제자리 옮기기도 그대로
free = t.do(op="move", 키=moved["key"], 새경로=["A"])["알림"]
assert free["경로"] == ["A", "같음(2)"] and "바뀐이름" not in free
stay = t.do(op="move", 키=t.item(["B", "같음"])["key"], 새경로=["B"])["알림"]
assert stay["경로"] == ["B", "같음"] and "바뀐이름" not in stay
# 폴더 — 같은 이름 폴더가 이미 있으면 「하위(1)」 · 안의 항목 따라감 · 같은 부모로는 그대로
folder = t.do(op="move", 경로=["A", "하위"], 새경로=["B"])["알림"]
assert folder["경로"] == ["B", "하위(1)"] and folder["바뀐이름"] == ["하위(1)"]
t.do(op="move", 경로=["B", "하위"], 새경로=["B"]) # 제자리 — 409 아님
assert ["B", "하위", "안쪽"] in t.paths()
# 직접 친 이름은 지금처럼 막음
t.do(409, op="add-item", 경로=["B"], 이름="같음")
t.do(409, op="rename", 키=t.item(["B", "같음(1)"])["key"], 이름="같음")
def test_판_409_권한_403(world):
admin = Tree(_as(world, "ADMIN"), "company")
stale = admin.now["판"]
@@ -254,7 +287,7 @@ def test_표_저장과_나무가_한_줄기(world):
def test_프로젝트에서_새_항목도_경로_그대로(world):
"""개인에 저장 · 프로젝트에서 새 항목(`M02_Item_Personal`)도 경로를 넷에 접지 않고 그대로 ·
같은 경로면 「이름 (2)」."""
같은 경로면 「이름(1)」."""
from M02_MasterTemplete import M02_Item_Personal as personal
folder = layers.personal_dir(7, 42)
@@ -262,5 +295,5 @@ def test_프로젝트에서_새_항목도_경로_그대로(world):
again = personal.create_item(folder, ["가", "나", "다", "라"], "마", "m")
t = Tree(_as(world), "personal")
assert t.item(["가", "나", "다", "라", "마"])["key"] == made["key"]
assert t.item(["가", "나", "다", "라", "마 (2)"])["key"] == again["key"]
assert t.item(["가", "나", "다", "라", "마(1)"])["key"] == again["key"]
assert made["이름"] == "가 나 다 라 마"
+17 -4
View File
@@ -1,7 +1,7 @@
"""M02 구조물 라이브러리 여러 개 · 층 사이 붙여넣기(PLAN 30-2) — 임시 storage.
① 회사 항목 · 폴더를 일반 사용자 개인 층에 붙임 — 새 SP 키 · 본뜬 = 원본 · 세 파일 복사 ·
폴더는 안의 것 · 빈 폴더까지 · 다시 붙이면 「이름 (2)」 · 고른 폴더 안의 것은 한 번만.
폴더는 안의 것 · 빈 폴더까지 · 다시 붙이면 「이름(1)」 · 고른 폴더 안의 것은 한 번만.
② 권한 — 쓰기 층만(일반 사용자 회사 층 403 · 남의 층 403) · 복사한 층은 읽기 권한(남의 회사 403).
③ 여러 개 옮기기 · 지우기 한 번에(하나라도 막히면 아무것도 안 바뀜).
"""
@@ -52,10 +52,10 @@ def test_회사_항목_폴더를_개인에_붙임_새_키_파일_이름2(world):
assert made["본뜬"] == {"layer": "company", "owner": "7", "key": wall}
for kind in ("structure", "area"):
assert (root / kind / f"{made['id']}.json").is_file()
# 다시 붙이면 같은 이름 「(2)」 · 붙일 폴더 안으로
# 다시 붙이면 같은 이름 「(1)」 · 붙일 폴더 안으로
again = client.post(PASTE.format("personal"), json={**body, "경로": []}).json()
assert ["배수 (2)", "측구"] in [r["경로"] for r in again["items"]]
assert ["옹벽 (2)"] in [r["경로"] for r in again["items"]]
assert ["배수(1)", "측구"] in [r["경로"] for r in again["items"]]
assert ["옹벽(1)"] in [r["경로"] for r in again["items"]]
inner = client.post(PASTE.format("personal"), json={**body, "대상": [body["대상"][2]],
"경로": ["배수"]}).json() # fmt: skip
assert ["배수", "옹벽"] in [r["경로"] for r in inner["items"]]
@@ -109,6 +109,19 @@ def test_여러_개_옮기기_지우기_한_번에(world):
assert t.paths() == [["나", "둘"]]
def test_여러_개_옮기기_같은_이름은_이름_번호로(world):
t = Tree(_as(world), "personal")
t.do(op="add-folder", 경로=[], 이름="나")
t.do(op="add-item", 경로=["나"], 이름="같음")
t.do(op="add-item", 경로=[], 이름="같음")
t.do(op="add-folder", 경로=[], 이름="묶음")
t.do(op="add-item", 경로=["묶음"], 이름="같음")
moves = [{"경로": [], "키": t.item(["같음"])["key"]}, {"경로": ["묶음"]}]
note = t.do(op="move", 대상=moves, 새경로=["나"])["알림"]
assert sorted(t.paths()) == [["나", "같음"], ["나", "같음(1)"], ["나", "묶음", "같음"]]
assert note["바뀐이름"] == ["같음(1)"]
def test_항목_추가_일위대가_단위_없이(world):
"""PLAN 31-2 · 39-1d — 항목 추가 = 종류(폴더) + 이름 · 단위 안 받아도 됨 ·
일위대가 키는 안 받음 — 열 칸에 안 적고 늘 빈 labor 문서
@@ -76,7 +76,7 @@ def test_text_column_pick_rows_blank():
def test_labor_row_shows_su_number():
"""마스터 일위대가 줄 = 그 열 일위대가번(SU) — 열의 옛 `일위대가` 칸(UA 키)은 안 읽음."""
ops = OPS.read_text(encoding="utf-8")
assert "REF_ROWS: readonly string[] = [KEY_PRICE, KEY_DRAWING, KEY_AREA]" in ops
assert "REF_ROWS: readonly string[] = [KEY_PRICE, KEY_DRAWING, KEY_AREA, KEY_SUM]" in ops
render = (OPS.parent / "ui_template_sheet_render.ts").read_text(encoding="utf-8")
assert re.search(r"key === KEY_PRICE\s*\?\s*state\.laborLabel", render)
assert "col.일위대가" not in render