feat(m02): 상세 이동 단추 · 도면 상세에서 면적산출로 보내기(105-2 · 105-4)
- 상세 머리 제목과 ? 사이 [도면][일위대가][면적산출] — 지금 것 눌림 · 문서 없는 열은 꺼짐 · 뒤로 단추와 같은 보임 - 구조물 도면 상세 머리 [면적산출로 보내기] · TOAREA 듣기 — 같은 열 면적 상세로 옮겨 가 넣을 자리 창 - 미저장 도면이면 물음 · 읽기 · 프로젝트 층 막음 · 저장은 사용자 [저장] Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KYZ2HWWdUwZWYnn3tQz7X
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_DetailNav.ts
|
||||
* 상세 머리 제목과 [?] 사이 [도면][일위대가][면적산출] 묶음(105-4) — 같은 열의 다른 상세로 옮겨 감.
|
||||
* 지금 상세 = 눌림(aria-pressed) · 그 열 문서가 없으면 그 단추 꺼짐(번호 목록에 번호 없음).
|
||||
* 보임 조건 · 옮겨 가기(confirmLeave → open)는 부른 쪽(`UI_Main`) 몫.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, el } from "@ui/ui_template_elements";
|
||||
import {
|
||||
fetchStructureNumbers,
|
||||
getActiveLayer,
|
||||
type Kind,
|
||||
type Layer,
|
||||
type StructureNumbers,
|
||||
} from "./M02_MasterTemplete_Api_Fetch";
|
||||
|
||||
type DetailKind = "structure" | "labor" | "area";
|
||||
|
||||
const ITEMS: [DetailKind, string, keyof StructureNumbers][] = [
|
||||
["structure", "도면", "도번"],
|
||||
["labor", "일위대가", "일위대가번"],
|
||||
["area", "면적산출", "면적번"],
|
||||
];
|
||||
const TIP = "같은 열의 다른 상세로 — 저장 안 한 고침이 있으면 물음";
|
||||
const NONE = "이 열은 문서가 없음";
|
||||
|
||||
export interface DetailNav {
|
||||
root: HTMLElement;
|
||||
/** 연 상세 — null 이면 숨김 · 눌림 · 꺼짐을 새로 */
|
||||
update: (at: { layer: Layer; kind: Kind; name: string } | null) => void;
|
||||
}
|
||||
|
||||
export function createDetailNav(go: (kind: DetailKind) => void): DetailNav {
|
||||
const buttons = ITEMS.map(([kind, label]) => {
|
||||
const button = createButton({ label, size: "sm", onClick: () => go(kind) });
|
||||
button.dataset.detailNav = kind;
|
||||
return button;
|
||||
});
|
||||
const root = el("div", {
|
||||
className: "ui-btn-group m02-main__nav",
|
||||
attrs: { role: "group", "aria-label": TIP },
|
||||
children: buttons,
|
||||
});
|
||||
let ask = 0;
|
||||
|
||||
const update: DetailNav["update"] = (at) => {
|
||||
root.hidden = !at;
|
||||
const mine = ++ask;
|
||||
buttons.forEach((button, i) => {
|
||||
const on = ITEMS[i][0] === at?.kind;
|
||||
button.setAttribute("aria-pressed", String(on)); // 눌림 = 지금 상세(누르면 그대로)
|
||||
button.disabled = false;
|
||||
button.title = on ? "" : TIP;
|
||||
});
|
||||
// 번호 목록은 지금 층만(프로젝트 층 · 남의 층이면 안 끔)
|
||||
if (!at || at.layer === "project" || at.layer !== getActiveLayer()) return;
|
||||
void fetchStructureNumbers().then(
|
||||
(numbers) => {
|
||||
if (mine !== ask) return;
|
||||
buttons.forEach((button, i) => {
|
||||
const [kind, , field] = ITEMS[i];
|
||||
if (kind === at.kind || numbers[at.name]?.[field]) return;
|
||||
button.disabled = true;
|
||||
button.title = NONE;
|
||||
});
|
||||
},
|
||||
() => undefined,
|
||||
);
|
||||
};
|
||||
return { root, update };
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_DrawingToArea.ts
|
||||
* 구조물 도면 상세 → 같은 열 면적산출 상세(105-2) — 머리 [면적산출로 보내기] = CAD 에 고른 범위 요청
|
||||
* (`EXPORT_TABLE_REQUEST`) · 받은 표 자료(`EXPORT_TABLE` · CAD 명령 TOAREA 도 같은 길)를 `onTable` 로.
|
||||
* 옮겨 가기 · 넣을 자리 창은 부른 쪽(`UI_Main` → `importDrawingTable`) · 저장은 사용자 [저장].
|
||||
* 쓸 수 없는 면적(읽기 · 프로젝트 층)이면 단추 없음 · TOAREA 는 알림만.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, showToast } from "@ui/ui_template_elements";
|
||||
import { EXPORT_TABLE, EXPORT_TABLE_REQUEST } from "./M02_MasterTemplete_AreaDrawing";
|
||||
import type { DrawingTable } from "./M02_MasterTemplete_AreaFromDrawing";
|
||||
|
||||
const TEXT = {
|
||||
send: "면적산출로 보내기",
|
||||
sendTip:
|
||||
"도면에서 고른 범위(표 · 선으로 그린 표 · 글)를 같은 열 면적산출로 옮겨 감 — 새 시트 또는 고른 칸부터 · 연동 아님",
|
||||
blocked: "이 층은 면적산출을 고칠 수 없음 — 보내지 않음",
|
||||
};
|
||||
|
||||
/** `host` = 도면 상세 칸(iframe 이 든 곳) · `slot` = 머리 칸 · 떼는 함수를 돌려줌 */
|
||||
export function attachDrawingToArea(
|
||||
host: HTMLElement,
|
||||
slot: HTMLElement,
|
||||
onTable: ((table: DrawingTable) => void) | null,
|
||||
): () => void {
|
||||
const frame = (): HTMLIFrameElement | null => host.querySelector("iframe");
|
||||
const send = createButton({ label: TEXT.send, variant: "ghost" });
|
||||
send.title = TEXT.sendTip;
|
||||
send.dataset.drawingToArea = "";
|
||||
send.addEventListener("click", () =>
|
||||
frame()?.contentWindow?.postMessage({ type: EXPORT_TABLE_REQUEST }, window.location.origin),
|
||||
);
|
||||
if (onTable) slot.append(send);
|
||||
|
||||
const onMessage = (event: MessageEvent): void => {
|
||||
const win = frame()?.contentWindow;
|
||||
if (!win || event.source !== win || event.origin !== window.location.origin) return;
|
||||
const data = event.data as { type?: string; table?: DrawingTable } | null;
|
||||
if (data?.type !== EXPORT_TABLE || !data.table) return;
|
||||
if (onTable) onTable(data.table);
|
||||
else showToast(TEXT.blocked, "info");
|
||||
};
|
||||
window.addEventListener("message", onMessage);
|
||||
return () => {
|
||||
window.removeEventListener("message", onMessage);
|
||||
send.remove();
|
||||
};
|
||||
}
|
||||
@@ -30,6 +30,9 @@ import {
|
||||
} from "./M02_MasterTemplete_Structure";
|
||||
import { buildAreaPanel, type AreaPanel } from "./M02_MasterTemplete_AreaPanel";
|
||||
import { createAreaToDrawing, sentRanges } from "./M02_MasterTemplete_AreaToDrawing";
|
||||
import { importDrawingTable } from "./M02_MasterTemplete_AreaFromDrawing";
|
||||
import { createDetailNav } from "./M02_MasterTemplete_DetailNav";
|
||||
import { attachDrawingToArea } from "./M02_MasterTemplete_DrawingToArea";
|
||||
import { detailHelp, HELP_TITLE } from "./M02_MasterTemplete_CardHelp";
|
||||
import { helpParts } from "./M02_MasterTemplete_CardShell";
|
||||
import { isPrecombo } from "./M02_MasterTemplete_UI_Side_Precombo";
|
||||
@@ -135,6 +138,12 @@ export function createMain(
|
||||
// 구조물 도면 · 일위대가 · 면적산출 상세 화면 → 구조물집계표(그 열 고른 채) · 일반 단추(43-5 · 알약 · ghost 아님)
|
||||
const back = createButton({ label: L("M02_Back") });
|
||||
const helpSlot = el("span", { className: "m02-main__help" }); // 상세 [?](49-7) — 제목 바로 옆
|
||||
// 제목과 [?] 사이 [도면][일위대가][면적산출](105-4) — 같은 열 다른 상세 · 지금 것은 그대로
|
||||
const nav = createDetailNav(async (kind) => {
|
||||
const at = sel;
|
||||
if (!at || at.kind === kind || !(await confirmLeave())) return;
|
||||
void open({ ...at, kind });
|
||||
});
|
||||
const slot = el("div", { className: "m02-main__slot" }); // 도면 양식의 작도 영역 칸이 들어옴
|
||||
const notice = el("div", { className: "m02-main__notice", attrs: { hidden: "" } });
|
||||
const host = el("div", { className: "m02-main__host" });
|
||||
@@ -149,6 +158,7 @@ export function createMain(
|
||||
children: [
|
||||
back,
|
||||
title,
|
||||
nav.root,
|
||||
helpSlot,
|
||||
slot,
|
||||
reload,
|
||||
@@ -236,6 +246,7 @@ export function createMain(
|
||||
reload.hidden = !sel;
|
||||
save.hidden = !sel || !canEdit(sel.layer, isAdmin);
|
||||
back.hidden = !sel || !(sel.kind in NUMBER_FIELD);
|
||||
nav.update(back.hidden ? null : sel); // 보임 조건 = 뒤로 단추와 같음
|
||||
const kind = sel && sel.kind in HELP_TITLE ? (sel.kind as keyof typeof HELP_TITLE) : null;
|
||||
const help = kind && helpParts(HELP_TITLE[kind], detailHelp(kind)); // 열 때마다 새로 — 상자는 닫힘
|
||||
helpSlot.replaceChildren(...(help ? [help.button, help.box] : []));
|
||||
@@ -445,7 +456,25 @@ export function createMain(
|
||||
noArea: at.kind === "structure",
|
||||
frame: at.kind === "structure", // 도각 = 보여주기만(56장)
|
||||
});
|
||||
if (at.kind === "structure") toDrawing.onDrawing(editor, at, doc as StructureDoc);
|
||||
if (at.kind === "structure") {
|
||||
toDrawing.onDrawing(editor, at, doc as StructureDoc);
|
||||
// 도면 → 같은 열 면적산출 상세로 옮겨 가 넣을 자리 창(105-2) · 저장은 사용자 [저장] ·
|
||||
// 읽기 · 프로젝트 층 면적은 막음
|
||||
const off = attachDrawingToArea(
|
||||
host,
|
||||
slot,
|
||||
readOnly || at.layer === "project"
|
||||
? null
|
||||
: async (table) => {
|
||||
if (!(await confirmLeave())) return;
|
||||
await open({ ...at, kind: "area" });
|
||||
if (sel?.kind === "area" && sel.name === at.name)
|
||||
void importDrawingTable(sheetHandle, table);
|
||||
},
|
||||
);
|
||||
const made = editor;
|
||||
editor = { ...made, destroy: () => (off(), made.destroy()) };
|
||||
}
|
||||
return;
|
||||
}
|
||||
empty(L("M02_NoDrawing"));
|
||||
|
||||
@@ -241,3 +241,13 @@
|
||||
.m02-main__host[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 상세 이동 [도면][일위대가][면적산출](105-4) — 지금 상세 = 눌림(강조색 · 밝음 · 어두움 같은 토큰) */
|
||||
.m02-main__nav[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.m02-main__nav .ui-btn[aria-pressed="true"] {
|
||||
border-color: var(--color-accent);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
"""상세 이동 단추 · 도면 상세 → 면적산출(PLAN 105-2 · 105-4) — Node 로 판정 · 화면은 ORCA.
|
||||
|
||||
못박는 것
|
||||
- 머리 제목과 [?] 사이 [도면][일위대가][면적산출](`ui-btn-group`) · 지금 상세 = 눌림(aria-pressed) ·
|
||||
그 열 문서(번호)가 없으면 그 단추 꺼짐 · 프로젝트 층 · 남의 층은 번호를 안 읽고 다 켬 · 상세 아니면 숨김.
|
||||
- 보임 조건 = 뒤로 단추와 같음 · 누름 = confirmLeave → open({...sel, kind}) · 지금 것 누름 = 그대로.
|
||||
- 도면 상세 머리 [면적산출로 보내기] = CAD 요청 · 받은 표(TOAREA 도 같은 길) → 같은 열 면적 상세로 옮겨 가
|
||||
넣을 자리 창(`importDrawingTable`) · 읽기 · 프로젝트 층은 단추 없음 · 떠나면 듣기를 뗌.
|
||||
"""
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
M02 = ROOT / "M02_MasterTemplete"
|
||||
SCRIPT = r"""
|
||||
const fs = require("fs");
|
||||
const ts = require(process.argv[1] + "/config/node_modules/typescript");
|
||||
const src = fs.readFileSync(process.argv[1] + "/M02_MasterTemplete/M02_MasterTemplete_DetailNav.ts", "utf8");
|
||||
const js = ts.transpileModule(src, {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
let fetched = 0;
|
||||
let numbers = {};
|
||||
const button = (o) => {
|
||||
const b = { label: o.label, size: o.size, dataset: {}, attrs: {}, disabled: false, title: "", onClick: o.onClick };
|
||||
b.setAttribute = (k, v) => (b.attrs[k] = v);
|
||||
return b;
|
||||
};
|
||||
const stubs = {
|
||||
"@ui/ui_template_elements": {
|
||||
createButton: button,
|
||||
el: (tag, o) => ({ tag, className: o.className, attrs: o.attrs, children: o.children, hidden: false }),
|
||||
},
|
||||
"./M02_MasterTemplete_Api_Fetch": {
|
||||
fetchStructureNumbers: async () => (fetched++, numbers),
|
||||
getActiveLayer: () => "system",
|
||||
},
|
||||
};
|
||||
const box = { exports: {} };
|
||||
new Function("exports", "module", "require", js)(box.exports, box, (n) => stubs[n] ?? require(n));
|
||||
const gone = [];
|
||||
const nav = box.exports.createDetailNav((kind) => gone.push(kind));
|
||||
const view = () => ({
|
||||
hidden: nav.root.hidden,
|
||||
pressed: nav.root.children.map((b) => b.attrs["aria-pressed"]),
|
||||
disabled: nav.root.children.map((b) => b.disabled),
|
||||
});
|
||||
const tick = () => new Promise((r) => setTimeout(r, 0));
|
||||
(async () => {
|
||||
const out = { className: nav.root.className, labels: nav.root.children.map((b) => b.label) };
|
||||
numbers = { c1: { 도번: "SD-000001", 일위대가번: "", 면적번: "SA-000001" } };
|
||||
nav.update({ layer: "system", kind: "area", name: "c1" });
|
||||
out.before = view();
|
||||
await tick();
|
||||
out.area = view();
|
||||
nav.update({ layer: "project", kind: "structure", name: "c1" });
|
||||
await tick();
|
||||
out.project = view();
|
||||
nav.update({ layer: "company", kind: "labor", name: "c1" }); // 남의 층(지금 층 아님)
|
||||
await tick();
|
||||
out.other = view();
|
||||
out.fetched = fetched;
|
||||
nav.update(null);
|
||||
out.none = view();
|
||||
nav.root.children[1].onClick();
|
||||
out.gone = gone;
|
||||
console.log(JSON.stringify(out));
|
||||
})();
|
||||
"""
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def run():
|
||||
node = shutil.which("node")
|
||||
if not node:
|
||||
pytest.skip("node 없음")
|
||||
done = subprocess.run(
|
||||
[node, "-e", SCRIPT, str(ROOT)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
check=True,
|
||||
)
|
||||
return json.loads(done.stdout)
|
||||
|
||||
|
||||
def _text(name: str) -> str:
|
||||
return (M02 / name).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_단추_셋_묶음(run):
|
||||
assert run["labels"] == ["도면", "일위대가", "면적산출"]
|
||||
assert "ui-btn-group" in run["className"]
|
||||
|
||||
|
||||
def test_지금_것_눌림_없는_문서_꺼짐(run):
|
||||
assert run["before"]["pressed"] == ["false", "false", "true"]
|
||||
assert run["before"]["disabled"] == [False, False, False] # 번호 받기 전엔 켬
|
||||
assert run["area"]["disabled"] == [False, True, False] # 일위대가번 없음 = 꺼짐
|
||||
assert run["area"]["hidden"] is False
|
||||
|
||||
|
||||
def test_프로젝트_남의_층은_안_읽음(run):
|
||||
assert run["project"]["pressed"] == ["true", "false", "false"]
|
||||
assert run["project"]["disabled"] == [False, False, False]
|
||||
assert run["other"]["disabled"] == [False, False, False]
|
||||
assert run["fetched"] == 1
|
||||
|
||||
|
||||
def test_상세_아니면_숨김_누름은_옮겨_감(run):
|
||||
assert run["none"]["hidden"] is True
|
||||
assert run["gone"] == ["labor"]
|
||||
|
||||
|
||||
def test_머리_줄_자리와_보임_조건():
|
||||
main = _text("M02_MasterTemplete_UI_Main.ts")
|
||||
assert " title,\n nav.root,\n helpSlot," in main # 제목과 ? 사이
|
||||
assert "nav.update(back.hidden ? null : sel); // 보임 조건 = 뒤로 단추와 같음" in main
|
||||
assert "if (!at || at.kind === kind || !(await confirmLeave())) return;" in main
|
||||
assert "void open({ ...at, kind });" in main
|
||||
css = (M02 / "M02_MasterTemplete_UI_Style.css").read_text(encoding="utf-8")
|
||||
assert '.m02-main__nav .ui-btn[aria-pressed="true"]' in css
|
||||
|
||||
|
||||
def test_도면_상세에서_면적으로_보내기():
|
||||
main = _text("M02_MasterTemplete_UI_Main.ts")
|
||||
block = main.split("const off = attachDrawingToArea(")[1].split("return;\n }\n empty(")[0]
|
||||
assert 'readOnly || at.layer === "project"' in block # 읽기 · 프로젝트 층 막음
|
||||
assert "if (!(await confirmLeave())) return;" in block # 미저장 도면이면 물음
|
||||
assert 'await open({ ...at, kind: "area" });' in block
|
||||
assert "void importDrawingTable(sheetHandle, table);" in block # 넣을 자리 창 · 저장은 사용자
|
||||
assert "editor = { ...made, destroy: () => (off(), made.destroy()) };" in block # 떠나면 뗌
|
||||
to_area = _text("M02_MasterTemplete_DrawingToArea.ts")
|
||||
assert "postMessage({ type: EXPORT_TABLE_REQUEST }, window.location.origin)" in to_area
|
||||
assert "if (data?.type !== EXPORT_TABLE || !data.table) return;" in to_area
|
||||
assert "if (onTable) slot.append(send);" in to_area
|
||||
assert "event.source !== win" in to_area # 다른 iframe(면적 도면 패널) 메시지는 안 받음
|
||||
Reference in New Issue
Block a user