From 9246250629df2f20008b27f16658b628eaf430c4 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 31 Oct 2026 13:22:51 +0900 Subject: [PATCH] =?UTF-8?q?feat(M02):=20=EC=9D=BC=EC=9C=84=EB=8C=80?= =?UTF-8?q?=EA=B0=80=20=EC=88=98=EB=9F=89=20=EC=95=88=20=EB=84=A3=EC=9D=80?= =?UTF-8?q?=20=EC=A4=84=20=EA=B2=BD=EA=B3=A0=20=C2=B7=20=EB=B3=B5=EC=82=AC?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=20=EC=9D=B4=EB=A6=84=20=E3=80=8C=E2=80=A6?= =?UTF-8?q?=20=EB=B3=B5=EC=82=AC=E3=80=8D(PLAN=20701-4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 일위대가 카드 · 상세 합계 줄 — 수량산출 빈 줄 수 「수량 안 넣은 줄 n — 합계에서 빠짐」(좁은 카드에서 제 줄 · 줄바꿈) - 복사 생성(나무 copyIn) = 대상 복사이름 → 새 이름 「원래 이름 복사」 · 또 하면 「… 복사(1)」 · 그냥 붙여넣기는 그대로 - 시험 test_m02_tree_paste(임시 storage) 한 건 더함 Co-Authored-By: Claude Opus 5.5 --- M02_MasterTemplete/M02_Item_Paste.py | 6 +++-- .../M02_MasterTemplete_Api_Fetch.ts | 2 ++ .../M02_MasterTemplete_Labor.css | 7 ++++++ .../M02_MasterTemplete_Labor_Rows.ts | 23 ++++++++++++++++- .../M02_MasterTemplete_Labor_Text.ts | 4 +++ .../M02_MasterTemplete_TreePanel.ts | 7 +++++- resources/tester/test_m02_tree_paste.py | 25 +++++++++++++++++++ 7 files changed, 70 insertions(+), 4 deletions(-) diff --git a/M02_MasterTemplete/M02_Item_Paste.py b/M02_MasterTemplete/M02_Item_Paste.py index e906dffb6..081eb808f 100644 --- a/M02_MasterTemplete/M02_Item_Paste.py +++ b/M02_MasterTemplete/M02_Item_Paste.py @@ -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 diff --git a/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts b/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts index 7c6f6e109..6da5621f7 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts @@ -467,6 +467,8 @@ export const editTree = ( export interface TreeRef { 경로: string[]; 키?: string; + /** 복사 생성(701-4) — 새 이름 = 「원래 이름 복사」(같은 이름 둘 안 생김) */ + 복사이름?: boolean; } /** 복사해 둔 것 — 나무 종류(`kind` · 구조물 · 일위대가) · 복사한 층 · 고른 것들 */ diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor.css b/M02_MasterTemplete/M02_MasterTemplete_Labor.css index 1c32aadc0..083b9ee65 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor.css +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor.css @@ -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; } diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts index d7638b889..a48088c51 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts @@ -493,6 +493,13 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement }); const sum = (part: Partial | 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: "" }), diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts index e5b3b3ce6..807f67756 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts @@ -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}", diff --git a/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts index 259dc409c..384e588c5 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts @@ -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, + ), ); }; diff --git a/resources/tester/test_m02_tree_paste.py b/resources/tester/test_m02_tree_paste.py index 757936cb8..3c19c8dde 100644 --- a/resources/tester/test_m02_tree_paste.py +++ b/resources/tester/test_m02_tree_paste.py @@ -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 # 복사 생성 길만