fix(M01): 47-3 로직 상세 오른쪽 = 공용 접는 패널 둘(흐름도 · 원문) — 위아래 접기 카드 걷어냄

- 흐름도 · 원문 = 따로 패널 · 접힌 패널 = 세로 글 띠 · 하나만 열림 · 모두 접힘 됨 · 폭 공유(경계 끌기 · 화살표 키)
- 열린 패널 · 폭 = 취향 키 m01-logic-side · 흐름도 패널 자체 접기 단추 · 끌기 경계는 숨김
- 옛 두 칸 비율 막대(buildSplit) 제거 · 부품 칸 숨김([hidden]) 규칙 덧붙임
- 화면 시나리오 ui_checks/47-3_로직상세_오른쪽패널.js

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NPJGtfXo6GMbcpiboQC4uK
This commit is contained in:
2026-10-05 13:41:48 +09:00
co-authored by Claude Opus 5.5
parent cbe0c17505
commit da510d3cbc
7 changed files with 187 additions and 213 deletions
@@ -0,0 +1,141 @@
/* 47-3 M01 단가산출 로직 상세 오른쪽 패널 — `orca eval --expression "$(cat 이 파일)"` 한 번에 돎 · JSON 돌려줌.
*
* 시작 화면: M01 마스터 데이터(#/m01-master-data · 시스템 관리자 로그인) · 아무 화면이나.
* 로직 GC000277(건설품셈 토목 1-1-2 유도선 설치 및 해체)을 엶 · 고치지 않음 · 끝에 열린 패널 · 폭을 처음 값으로 되돌림.
* 기대 수치:
* panels = ["흐름도","원문"] · 패널 몸 = 공용 패널(.ui-side-panels) · 왼쪽 칸 오른쪽에 붙음(sideRight)
* flow = {open:["flow"], strips:["원문"]} · 흐름도 판 보임 · 흐름도 패널 자체 접기 단추 · 끌기 경계 안 보임(inner 0)
* source = {open:["source"], strips:["흐름도"]} · 폭 = flow 폭(공유) · srcHead 에 「1-1-2 유도선」 · srcTable true
* none = {open:[], strips:["흐름도","원문"]} · 띠 너비 < 40
* back = {open:["flow"]}(흐름도 띠로 다시 폄)
* drag = 왼쪽 경계를 100px 왼쪽으로 끌면 폭 +100 · 원문으로 바꿔도 같은 폭 · key = 화살표 ← 한 번 +20
* noLink = 「원문 연결 없음」 + 「출처 · …」(서버가 빈 목록을 줄 때 · fetch 를 잠시 바꿔 봄)
* restored = 처음 취향 값과 같음
*/
(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 (f, ms = 15000) => {
for (let t = 0; t < ms; t += 200) {
const v = f();
if (v) return v;
await W(200);
}
return null;
};
const PREF = "aislo:pref:m01-logic-side";
const first = localStorage.getItem(PREF);
const out = {};
const openLogic = async (key) => {
sessionStorage.setItem("m01.logic.pending", key);
const tab = $$(".ui-collapsible__title").find((h) => h.innerText.trim() === "단가산출 로직");
if (!tab) return false;
if (!tab.closest(".ui-collapsible").classList.contains("is-collapsed")) tab.click();
tab.click();
return until(() => {
const t = $(".m02-dfp__title")?.innerText ?? "";
return !$(".m01lab-side")?.hidden && $(".m01lab-side .m02-dfp") && t;
});
};
if (!(await openLogic("GC000277"))) return JSON.stringify({ error: "로직 못 엶" });
await W(800);
const shown = (n) => !!n && n.getBoundingClientRect().height > 0;
const panel = (id) => $(`.m01lab-side .ui-side-panels__panel[data-panel="${id}"]`);
const strip = (title) =>
$$(".m01lab-side .ui-side-panels__strip").find((s) => s.innerText.replace(/\s/g, "") === title);
const wOf = (n) => Math.round(n.getBoundingClientRect().width);
const state = () => ({
open: ["flow", "source"].filter((id) => shown(panel(id))),
strips: $$(".m01lab-side .ui-side-panels__strip")
.filter(shown)
.map((s) => s.innerText.replace(/\s/g, "")),
width: ["flow", "source"].filter((id) => shown(panel(id))).map((id) => wOf(panel(id)))[0] ?? 0,
});
const pref = () => JSON.parse(localStorage.getItem(PREF) ?? "{}");
out.panels = $$(".m01lab-side .ui-side-panels__title").map((t) => t.innerText.trim());
const editor = $(".m01lab-side > .m01-logic__editor").getBoundingClientRect();
const side = $(".m01lab-side .ui-side-panels").getBoundingClientRect();
out.sideRight = Math.round(side.left - editor.right);
// 처음 = 흐름도 열림으로 맞춤
if (!shown(panel("flow"))) {
strip("흐름도").click();
await W(400);
}
out.flow = state();
out.flowDiagram = shown($(".m02-dfp__open", panel("flow")));
out.inner = $$(".m01lab-side .m02-dfp__toggle, .m01lab-side .m02-dfp__grip").filter(shown).length;
// 끌기 — 왼쪽 경계를 100px 왼쪽으로
const grip = $(".ui-side-panels__grip", panel("flow"));
const g = grip.getBoundingClientRect();
const x0 = g.left + g.width / 2;
const y0 = g.top + 50;
const w0 = state().width;
grip.dispatchEvent(
new PointerEvent("pointerdown", { bubbles: true, button: 0, clientX: x0, clientY: y0 }),
);
window.dispatchEvent(new PointerEvent("pointermove", { clientX: x0 - 50, clientY: y0 }));
window.dispatchEvent(new PointerEvent("pointerup", { clientX: x0 - 100, clientY: y0 }));
await W(200);
out.drag = { before: w0, after: state().width, pref: pref().width };
// 원문 — 폭 공유
strip("원문").click();
await until(() => $(".m01lab-src__head"));
out.source = state();
out.srcHead = $(".m01lab-src__head")?.innerText;
out.srcTable = $$(".m01lab-src__table td").some((td) => td.innerText === "특별인부");
// 키보드 — ← 한 번 = +20 · → 한 번 = 되돌림
const sg = $(".ui-side-panels__grip", panel("source"));
sg.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowLeft", bubbles: true }));
const k1 = state().width;
sg.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true }));
out.key = { left: k1 - out.source.width, back: state().width - out.source.width };
// 모두 접기 — 원문 머리 ▸
$(".ui-side-panels__fold", panel("source")).click();
await W(300);
out.none = { ...state(), stripW: wOf(strip("흐름도")), pref: pref().open };
strip("흐름도").click();
await W(600);
out.back = { ...state(), pref: pref().open };
// 원문 연결 없음 — 서버가 빈 목록을 줄 때
strip("원문").click();
const real = window.fetch;
window.fetch = (u, o) =>
String(u).includes("/m01/logic/source")
? Promise.resolve(new Response(JSON.stringify({ found: [] }), { status: 200 }))
: real(u, o);
try {
await openLogic("GC000338");
await until(() => $(".m01lab-src__none"));
out.noLink = [
$(".m01lab-src__none")?.innerText,
$(".m01lab-src__body .m01-logic__muted")?.innerText,
];
} finally {
window.fetch = real;
}
// 되돌림 — 폭(끈 만큼 ←→ 화살표로) · 열린 패널
const was = JSON.parse(first ?? "{}");
const want = was.open === undefined ? "flow" : was.open;
const now = () => ["flow", "source"].find((id) => shown(panel(id))) ?? null;
if (now() !== want) {
if (want === null) $(".ui-side-panels__fold", panel(now())).click();
else strip(want === "flow" ? "흐름도" : "원문").click();
await W(300);
}
const target = was.width ?? w0;
const anyGrip = $$(".m01lab-side .ui-side-panels__grip")[0];
for (let i = 0; i < 40 && pref().width !== target; i++) {
const d = target - pref().width;
anyGrip.dispatchEvent(
new KeyboardEvent("keydown", {
key: d > 0 ? "ArrowLeft" : "ArrowRight",
shiftKey: Math.abs(d) >= 80,
bubbles: true,
}),
);
}
out.restored = first === null ? pref() : localStorage.getItem(PREF) === first;
out.first = first;
return JSON.stringify(out);
})();