feat(M02): 일위대가 수량 안 넣은 줄 경고 · 복사 생성 이름 「… 복사」(PLAN 701-4)

- 일위대가 카드 · 상세 합계 줄 — 수량산출 빈 줄 수 「수량 안 넣은 줄 n — 합계에서 빠짐」(좁은 카드에서 제 줄 · 줄바꿈)
- 복사 생성(나무 copyIn) = 대상 복사이름 → 새 이름 「원래 이름 복사」 · 또 하면 「… 복사(1)」 · 그냥 붙여넣기는 그대로
- 시험 test_m02_tree_paste(임시 storage) 한 건 더함

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-31 13:22:51 +09:00
co-authored by Claude Opus 5.5
parent df6bc10237
commit 9246250629
7 changed files with 70 additions and 4 deletions
+4 -2
View File
@@ -1,7 +1,8 @@
"""여러 항목 · 폴더를 층 사이 붙여넣기(PLAN 30-2) — 구조물 라이브러리 나무의 [복사] → [붙여넣기].
붙일 폴더(`dest`) 아래 새 항목 — 대상 층 새 키(SC · SP) · 열 `본뜬` = 원본(층 · 주인 · 키) ·
같은 이름은 「이름(1)」 · 폴더는 안의 항목 · 빈 폴더까지(폴더 이름이 겹치면 「폴더(1)」).
같은 이름은 「이름(1)」 · 폴더는 안의 항목 · 빈 폴더까지(폴더 이름이 겹치면 「폴더(1)」) ·
복사 생성(대상 `복사이름`) = 새 이름 「원래 이름 복사」(701-4 · 층이 달라도 같은 이름 둘 안 생김).
파일(도면 · 면적 · 입력 연결)은 `Items.copy_item` 이 옮김. 권한은 부르는 쪽 몫.
"""
@@ -73,7 +74,8 @@ def paste(
if found is None:
raise store.StoreError(404, f"없는 항목 「{key}」")
used = [*taken, *(p for _, p in plan)]
path = tree.unique_path([*dest, found[-1]], lambda p: p in used)
name = f"{found[-1]} 복사" if one.get("복사이름") else found[-1]
path = tree.unique_path([*dest, name], lambda p: p in used)
renamed += [path[-1]] if path[-1] != found[-1] else []
plan.append((key, path))
continue
@@ -467,6 +467,8 @@ export const editTree = (
export interface TreeRef {
경로: string[];
키?: string;
/** 복사 생성(701-4) — 새 이름 = 「원래 이름 복사」(같은 이름 둘 안 생김) */
복사이름?: boolean;
}
/** 복사해 둔 것 — 나무 종류(`kind` · 구조물 · 일위대가) · 복사한 층 · 고른 것들 */
@@ -85,6 +85,13 @@
border-top: 2px solid var(--color-border);
}
/* 701-4 수량 안 넣은 줄 경고 — 합계 아래 제 줄 · 좁은 카드에서도 줄바꿈 */
.m02-labrow__name > .m02-labrow__noqty {
display: block;
max-width: 9rem;
white-space: normal;
}
.m02-labrow__move {
white-space: nowrap;
}
@@ -493,6 +493,13 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement
});
const sum = (part: Partial<LaborCost> | undefined, c: (typeof COSTS)[number]): number | null =>
part?.[c] ?? null;
// 701-4 수량산출 빈 줄 = 반영 금액 없음 → 합계에서 빠짐 — 조용히 빠지지 않게 합계 줄에 경고
const noQty = data.calc
? data.rows.filter((row) => {
const line = lineOf(lineId(row) ?? "");
return !!line && (line.수량 === null || line.수량 === undefined);
}).length
: 0;
const foot = data.rows.length
? [
el("tr", {
@@ -500,7 +507,21 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement
children: [
el("td", { className: "m02-labrow__no", text: "" }),
el("td", { className: "m02-labrow__kind", text: "" }),
el("td", { className: "m02-labrow__name", text: tc("Prev_Sum") }),
el("td", {
className: "m02-labrow__name",
children: [
el("span", { text: tc("Prev_Sum") }),
...(noQty
? [
el("small", {
className: "ui-text ui-text--error m02-labrow__noqty",
text: tc("Labor_NoQty").replace("{n}", String(noQty)),
attrs: { "data-labor-noqty": String(noQty) },
}),
]
: []),
],
}),
el("td", { className: "m02-labrow__alias", text: "" }),
el("td", { className: "m02-labrow__qty", text: "" }),
el("td", { className: "m02-labrow__unit", text: "" }),
@@ -96,6 +96,10 @@ const TEXT = {
Lib_Personal: ["개인", "Personal"],
Labor_AlreadyRef: ["이미 담은 일위대가 조합 — {value}", "Already added — {value}"],
Labor_Empty: ["빈 값 {n}", "{n} empty"],
Labor_NoQty: [
"수량 안 넣은 줄 {n} — 합계에서 빠짐",
"{n} rows without quantity — left out of the sum",
],
Labor_Lost: [
"로직 입력이 바뀌어 저장 때 빠지는 값 — {value}",
"Inputs no longer in the logic (dropped on save) — {value}",
@@ -673,7 +673,12 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle {
const run = api.paste;
if (!run || !opt.canEdit || !(await ready())) return;
await apply(() =>
run(opt.layer, owner, { kind: clipKind, from, refs: [{ 경로: [], 키: key }] }, dest),
run(
opt.layer,
owner,
{ kind: clipKind, from, refs: [{ 경로: [], 키: key, 복사이름: true }] }, // 701-4 「… 복사」
dest,
),
);
};
+25
View File
@@ -9,6 +9,7 @@
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from resources.tester import test_m02_tree as base
@@ -158,3 +159,27 @@ def test_항목_추가_창_카드_격자_표_접기():
assert "max-height: calc(100dvh - var(--app-header-h));\n overflow-y: auto;" in css
main = (m02 / "M02_MasterTemplete_UI_Main.ts").read_text("utf-8")
assert "host.hidden = !host.hidden;" in main and 'title.classList.toggle("is-folded"' in main
def test_복사_생성_이름_원래_이름_복사_같은_이름_둘_없음(world):
"""701-4 — 복사 생성(대상 `복사이름`) = 「원래 이름 복사」 · 또 하면 「… 복사(1)」 · 그냥 붙여넣기는 그대로."""
company = _company(world)
wall = company.item(["옹벽"])["key"]
client = _as(world)
body = {
"from_layer": "company",
"대상": [{"경로": [], "키": wall, "복사이름": True}],
"경로": [],
}
first = client.post(PASTE.format("personal"), json=body).json()
second = client.post(PASTE.format("personal"), json=body).json()
plain = client.post(PASTE.format("personal"), json={**body, "대상": [{"경로": [], "키": wall}]})
names = [r["경로"][-1] for r in plain.json()["items"]]
assert sorted(names) == ["옹벽", "옹벽 복사", "옹벽 복사(1)"]
assert first["알림"]["바뀐이름"] == ["옹벽 복사"] and second["알림"]["바뀐이름"] == [
"옹벽 복사(1)"
]
assert len(set(names)) == len(names) # 같은 이름 둘 없음
root = Path(__file__).resolve().parents[2]
ui = (root / "M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts").read_text("utf-8")
assert "refs: [{ 경로: [], 키: key, 복사이름: true }]" in ui # 복사 생성 길만