feat(M02): 47-6 면적산출 상세 오른쪽 = 공용 접는 패널 둘(일위대가 · 도면)

- 옛 접기 손잡이 · 폭 손잡이 · 사이드바 카드 틀을 공용 오른쪽 접는 패널(47-3)로 갈음 — 하나만 열림 · 폭 공유 · 모두 접힘
- 「도면」 패널 = 그 열 구조물 도면 보기만(확대 · 이동) · 처음 펼 때 읽고 그림
- DrawingCard 미리보기 한 벌(mountStructure)을 카드 · 도면 패널이 같이 씀
- 시험: test_m02_structure_panel 기대를 공용 패널로 · ui_checks/47-6 시나리오

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W4uKaV4ZBhsJN4GHG7yRj4
This commit is contained in:
2026-10-05 13:47:07 +09:00
co-authored by Claude Opus 5.5
parent 9499ff16cc
commit fff2845117
5 changed files with 178 additions and 115 deletions
+4 -3
View File
@@ -157,11 +157,12 @@ def test_좌측_구조물_집계_양식_아래_하위_넷_새로_본떠_지우
def test_면적_오른쪽_일위대가_패널은_공용_틀_손잡이():
"""좌측과 같은 사이드바 카드 · 접기 · 공용 폭 손잡이 · 접기 손잡이 — 새로 그리지 않음(39-4 · 옛 조합 패널 지움)."""
"""틀 = 공용 오른쪽 접는 패널(47-6 · 일위대가 · 도면) — 새로 그리지 않음(옛 조합 패널 지움)."""
apanel = _text(M02 / "M02_MasterTemplete_AreaPanel.ts")
assert "createSidePanels(" in apanel and "createAreaDrawing(" in apanel
for name in ("createPanelResizer", "createWorkflowPanelHandle", "attachCollapsible"):
assert name in apanel, name
assert "ui-sidebar-card" in apanel and "setColumnCombo" not in apanel
assert name not in apanel, name
assert "setColumnCombo" not in apanel
for gone in ("ComboPanel.ts", "ComboPanel.css", "Combo.ts", "Combo.css", "Combo_Api.ts"):
assert not (M02 / f"M02_MasterTemplete_{gone}").exists(), gone
api = _text(M02 / "M02_MasterTemplete_Api_Fetch.ts")
@@ -0,0 +1,94 @@
/* 47-6 — M02 면적산출 상세 오른쪽 = 공용 접는 패널 둘(일위대가 · 도면) · 하나만 열림 · 폭 공유 · 모두 접힘
* 시작 화면: M02 마스터 템플릿 · 마스터 층(#/m02-master-template?open=구조물조합&root=master) · 시스템 관리자
* · 나무 item1 → 면적산출 카드 [수정] → 상세 · 끝에 [돌아가기] · 저장 안 함
* 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌)
* 기대 수치:
* - titles = ["일위대가", "도면"] · 일위대가 폄 → open = ["labor"] · labor.sections 에 「단가산출 로직」
* - 도면 띠 누름 → open = ["drawing"](일위대가 접힘) · drawing.iframe = true · drawing.h > 200
* · drawing.w = labor.w(폭 공유)
* - 접기 ▸ → open = [] · strips = 2(모두 접힘)
* - 끝에 처음 열림 상태로 되돌림(restored = true)
*/
(async () => {
const W = (ms) => new Promise((r) => setTimeout(r, ms));
const $ = (s, root = document) => root.querySelector(s);
const $$ = (s, root = document) => [...root.querySelectorAll(s)];
const until = async (fn, ms = 8000) => {
for (let t = 0; t < ms; t += 150) {
const v = fn();
if (v) return v;
await W(150);
}
return null;
};
const vis = (b) => b && !b.hidden && b.offsetParent !== null;
const edits = () =>
$$(".m02-struct button").filter((b) => vis(b) && b.textContent.trim() === "수정");
$('[data-node="i|item1"]')?.click();
await until(() => edits().length === 3);
const edit = edits().find((b) => /면적/.test(b.title)) ?? edits()[2];
if (!edit) return JSON.stringify({ error: "면적산출 [수정] 없음" });
edit.click();
const sp = await until(() => $(".m02-main__host .ui-side-panels"));
if (!sp) return JSON.stringify({ error: "오른쪽 패널 없음" });
const panel = (id) => $(`.ui-side-panels__panel[data-panel="${id}"]`, sp);
const strip = (title) =>
$$(".ui-side-panels__strip", sp).find((s) => s.innerText.trim() === title);
const openIds = () =>
$$(".ui-side-panels__panel", sp)
.filter((p) => !p.hidden)
.map((p) => p.dataset.panel);
const strips = () => $$(".ui-side-panels__strip", sp).filter((s) => !s.hidden).length;
const first = openIds()[0] ?? null;
const out = { titles: $$(".ui-side-panels__strip", sp).map((s) => s.innerText.trim()) };
if (panel("labor").hidden) strip("일위대가").click();
await W(1500);
out.labor = {
open: openIds(),
sections: $$(
"h3, h4, .ui-sidebar-section__title, strong",
panel("labor").querySelector(".ui-side-panels__body"),
)
.map((h) => h.innerText.trim())
.filter(Boolean)
.slice(0, 4),
w: Math.round(panel("labor").getBoundingClientRect().width),
};
strip("도면").click();
await until(() => panel("drawing").querySelector("iframe"), 8000);
await W(1500);
const f = panel("drawing").querySelector("iframe");
out.drawing = {
open: openIds(),
iframe: !!f,
h: f ? Math.round(f.getBoundingClientRect().height) : 0,
w: Math.round(panel("drawing").getBoundingClientRect().width),
note: panel("drawing").querySelector(".m02-apanel__note")?.innerText ?? null,
};
panel("drawing").querySelector(".ui-side-panels__fold").click();
await W(300);
out.folded = { open: openIds(), strips: strips() };
// 되돌림 — 처음 열려 있던 패널
if (first) strip(first === "labor" ? "일위대가" : "도면").click();
await W(300);
out.restored = (openIds()[0] ?? null) === first;
out.title = $(".m02-main__title")?.textContent.trim() ?? null;
$$(".m02-main__head button")
.find((x) => vis(x) && x.textContent.trim() === "돌아가기")
?.click();
out.pass =
out.titles.join() === "일위대가,도면" &&
out.labor.open.join() === "labor" &&
out.drawing.open.join() === "drawing" &&
out.drawing.iframe &&
out.drawing.h > 200 &&
out.drawing.w === out.labor.w &&
out.folded.open.length === 0 &&
out.folded.strips === 2 &&
out.restored;
return JSON.stringify(out);
})();