feat(structure): 구조물 목록 줄마다 잇기 램프 — 측점 왼쪽 고정 폭 칸 · 3행 × 열 · 초록 이음 · 빨강 필수 미지정 · 노랑 선택 미지정 · 툴팁 상자 · 항목(54-15)
- 램프 자리 = 폼 잇기 줄 규칙(명세 컨테이너 · 인스턴스 · 배관 흙막이 지형 단 수 · 정의 그룹 · 종류 단위 · B군 없음) - 단 수 바뀜 · 횡단 자료 도착 때 목록 다시 그림 - 지정 줄 왼쪽 선 = 안쪽 그림자(글 시작 자리 같게) - ui_checks/54-15_잇기_램프.js Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013rJwJMVL4EE31y77TB11AP
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
/* 54-15 — 구조물 목록 잇기 램프(측점 왼쪽 · 3행 × 열 · 초록 이음 · 빨강 필수 미지정 · 노랑 선택 미지정)
|
||||
* 시작 화면: B06(#/b06-section) 또는 B05 · 검증 프로젝트(sub4_검증) · 로그인
|
||||
* 부르는 법: orca eval --expression "window.__m5415={mode:'단계'}; $(cat 이 파일)"
|
||||
* look → 줄마다 램프 = 폼 잇기 줄(수 · 색) 대조 · 줄 높이(램프 끈 때와 같음) · 글 시작 x · 칸 폭
|
||||
* live → ARGS.row(기본 「배관 1」) 빨강 하나 잇기 → 초록 → 해제 → 빨강 · 「도수로/돌붙임」 추가 →
|
||||
* 노랑 → 잇기 → 초록 → 해제 → 노랑 → 빼기 → 램프 수 되돌아옴(저장 안 함)
|
||||
* sig → 줄마다 램프 색 글(B05 · B06 대조 · 저장 · 다시 열기 대조용)
|
||||
* (결과는 window.__m5415_out 에도 — orca eval 응답이 끊기면 그것을 읽음)
|
||||
* link / unlink → ARGS.row 첫 빨강 잇기 / 첫 초록 해제 → [저장] → sig
|
||||
*/
|
||||
(async () => {
|
||||
const ARGS = window.__m5415 || { mode: "look" };
|
||||
const ROW = ARGS.row || "배관 1";
|
||||
const w = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const until = async (fn, ms = 20000) => {
|
||||
for (let t = 0; t < ms; t += 200) {
|
||||
if (fn()) return true;
|
||||
await w(200);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const modUrl = (part) =>
|
||||
performance
|
||||
.getEntriesByType("resource")
|
||||
.map((e) => e.name)
|
||||
.filter((n) => n.includes(part))
|
||||
.pop();
|
||||
// 명세 모듈 주소 — 자원 목록에 없으면(먼저 불린 계산 모듈 주소에서) 이름만 바꿔 씀
|
||||
const specUrl =
|
||||
modUrl("/design/design_structure_pipe.ts") ??
|
||||
modUrl("/design/design_structure_pipe_calc.ts").replace("_calc.ts", ".ts");
|
||||
const { STRUCTURE_SPECS } = await import(specUrl);
|
||||
const items = () => [...document.querySelectorAll(".b05-route__irregular-list li")];
|
||||
const nameOf = (li) => li.querySelector("span")?.textContent ?? "";
|
||||
const rowOf = (name) => items().find((li) => nameOf(li) === name);
|
||||
const color = (dot) =>
|
||||
dot.classList.contains("is-linked") ? "G" : dot.classList.contains("is-required") ? "R" : "Y";
|
||||
const lampsOf = (li) => [...li.querySelectorAll(".b05-route__lamps i")].map(color).join("");
|
||||
const visible = (n) => n.offsetParent !== null;
|
||||
/** 폼에 보이는 잇기 줄 — 색 글(같은 차례 · 위 → 아래). */
|
||||
const formLinks = () =>
|
||||
[...document.querySelectorAll(".b05-structure__summary")].filter(visible).map((row) => {
|
||||
if (row.querySelector(".b05-structure__summary-name.is-assigned")) return "G";
|
||||
const box = row.closest("[data-spec-container]");
|
||||
if (!box) return "R"; // 종류 단위 · 정의 그룹 상자 = 필수
|
||||
const c = Object.values(STRUCTURE_SPECS)
|
||||
.flatMap((s) => s.containers)
|
||||
.find(
|
||||
(c) => c.id === box.dataset.specContainer && row.dataset.specLink?.startsWith(c.link),
|
||||
);
|
||||
return c && !c.required ? "Y" : "R";
|
||||
});
|
||||
const sorted = (s) => [...s].sort().join("");
|
||||
const select = async (name) => {
|
||||
await until(() => items().length > 0);
|
||||
const li = rowOf(name);
|
||||
if (!li) return false;
|
||||
if (!li.classList.contains("is-selected")) {
|
||||
li.click();
|
||||
await until(() => rowOf(name)?.classList.contains("is-selected"), 5000);
|
||||
}
|
||||
await w(700);
|
||||
return true;
|
||||
};
|
||||
const release = async () => {
|
||||
const li = document.querySelector(".b05-route__irregular-list li.is-selected");
|
||||
if (li) li.click();
|
||||
await w(500);
|
||||
};
|
||||
const sig = () =>
|
||||
items().map((li) => `${li.querySelector("strong")?.textContent}${nameOf(li)}:${lampsOf(li)}`);
|
||||
const pickLink = async (row) => {
|
||||
row.querySelector('[data-b05-summary="pick"]').click();
|
||||
await until(() => document.querySelector(".ui-item-picker__tree .ui-tree__row"));
|
||||
for (let k = 0; k < 8; k++) {
|
||||
const rows = [...document.querySelectorAll(".ui-item-picker__tree .ui-tree__row")];
|
||||
const leaf = rows.find((r) => !r.classList.contains("is-branch"));
|
||||
if (leaf) {
|
||||
leaf.click();
|
||||
break;
|
||||
}
|
||||
rows.find((r) => r.querySelector(".ui-tree__caret")?.textContent === "▸")?.click();
|
||||
await w(300);
|
||||
}
|
||||
await w(300);
|
||||
[...document.querySelectorAll(".ui-item-picker__actions button")]
|
||||
.find((b) => b.textContent.includes("적용"))
|
||||
.click();
|
||||
await w(1500);
|
||||
};
|
||||
const linkRows = () => [...document.querySelectorAll(".b05-structure__summary")].filter(visible);
|
||||
const save = async () => {
|
||||
document.querySelector('[data-dock-action="save"]').click();
|
||||
await w(500);
|
||||
const ok = await until(() => !document.querySelector(".ui-overlay.is-active"), 90000);
|
||||
await w(1500);
|
||||
return ok;
|
||||
};
|
||||
const out = { mode: ARGS.mode };
|
||||
|
||||
if (ARGS.mode === "look") {
|
||||
await release();
|
||||
const list = document.querySelector(".b05-route__irregular-list");
|
||||
const heights = () =>
|
||||
items().map((li) => Math.round(li.getBoundingClientRect().height * 10) / 10);
|
||||
const withLamps = heights();
|
||||
list.classList.add("m5415-off");
|
||||
const style = document.createElement("style");
|
||||
style.textContent = ".m5415-off .b05-route__lamps{display:none}";
|
||||
document.head.append(style);
|
||||
const without = heights();
|
||||
style.remove();
|
||||
list.classList.remove("m5415-off");
|
||||
const xs = items().map(
|
||||
(li) => Math.round(li.querySelector("strong").getBoundingClientRect().left * 10) / 10,
|
||||
);
|
||||
const widths = items().map(
|
||||
(li) => li.querySelector(".b05-route__lamps").getBoundingClientRect().width,
|
||||
);
|
||||
const dot = items()
|
||||
.map((li) => li.querySelector(".b05-route__lamps i"))
|
||||
.find(Boolean)
|
||||
?.getBoundingClientRect();
|
||||
// 줄마다 폼을 열어 잇기 줄과 대조
|
||||
const rows = [];
|
||||
for (const li of items()) {
|
||||
const name = nameOf(li);
|
||||
const lamps = lampsOf(li);
|
||||
await select(name);
|
||||
const form = formLinks().join("");
|
||||
rows.push({ name, lamps, form, ok: sorted(lamps) === sorted(form) });
|
||||
await release();
|
||||
}
|
||||
const cols = Math.max(...rows.map((r) => Math.ceil(r.lamps.length / 3)));
|
||||
Object.assign(out, {
|
||||
rows: rows.length,
|
||||
mismatch: rows.filter((r) => !r.ok),
|
||||
lampTotal: rows.reduce((n, r) => n + r.lamps.length, 0),
|
||||
colors: Object.fromEntries(
|
||||
["G", "R", "Y"].map((c) => [
|
||||
c,
|
||||
rows
|
||||
.map((r) => r.lamps)
|
||||
.join("")
|
||||
.split(c).length - 1,
|
||||
]),
|
||||
),
|
||||
byKind: [...new Set(rows.map((r) => `${r.name.replace(/ \d+$/, "")}=${r.lamps.length}`))],
|
||||
heightSame: JSON.stringify(withLamps) === JSON.stringify(without),
|
||||
heights: [...new Set(withLamps)],
|
||||
textX: [...new Set(xs)],
|
||||
lampWidth: [...new Set(widths)],
|
||||
cols,
|
||||
dot: dot && [dot.width, dot.height],
|
||||
dotColors: ["is-linked", "is-required", "is-optional"].map((c) => {
|
||||
const d = document.querySelector(`.b05-route__lamps i.${c}`);
|
||||
return d ? getComputedStyle(d).backgroundColor : null;
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
if (ARGS.mode === "live") {
|
||||
await select(ROW);
|
||||
const lamps = () => lampsOf(rowOf(ROW));
|
||||
const steps = [["start", lamps(), formLinks().join("")]];
|
||||
const red = linkRows().find(
|
||||
(r) =>
|
||||
!r.querySelector(".is-assigned") &&
|
||||
!r.closest("[data-spec-container]")?.querySelector("[data-spec-remove]"),
|
||||
);
|
||||
if (red) {
|
||||
await pickLink(red);
|
||||
steps.push(["link", lamps(), formLinks().join("")]);
|
||||
red.querySelector('[data-b05-summary="clear"]').click();
|
||||
await w(1500);
|
||||
steps.push(["clear", lamps(), formLinks().join("")]);
|
||||
}
|
||||
const add = [...document.querySelectorAll("[data-spec-add]")].find(visible);
|
||||
if (add) {
|
||||
const id = add.dataset.specAdd;
|
||||
add.click();
|
||||
await w(1500);
|
||||
steps.push([`add ${id}`, lamps(), formLinks().join("")]);
|
||||
// 램프 4 개 → 2 열 — 칸 폭 · 글 시작 x · 줄 높이가 목록 전체에서 하나
|
||||
const box2 = (sel) => [
|
||||
...new Set(
|
||||
items().map(
|
||||
(li) =>
|
||||
Math.round(
|
||||
li.querySelector(sel).getBoundingClientRect()[sel === "strong" ? "left" : "width"] *
|
||||
10,
|
||||
) / 10,
|
||||
),
|
||||
),
|
||||
];
|
||||
out.twoCols = {
|
||||
lampWidth: box2(".b05-route__lamps"),
|
||||
textX: box2("strong"),
|
||||
heights: [
|
||||
...new Set(items().map((li) => Math.round(li.getBoundingClientRect().height * 10) / 10)),
|
||||
],
|
||||
};
|
||||
const box = [...document.querySelectorAll(`[data-spec-container="${id}"]`)]
|
||||
.filter(visible)
|
||||
.at(-1);
|
||||
await pickLink(box);
|
||||
steps.push(["link", lamps(), formLinks().join("")]);
|
||||
box.querySelector('[data-b05-summary="clear"]').click();
|
||||
await w(1500);
|
||||
steps.push(["clear", lamps(), formLinks().join("")]);
|
||||
[...document.querySelectorAll(`[data-spec-container="${id}"]`)]
|
||||
.filter(visible)
|
||||
.at(-1)
|
||||
.querySelector("[data-spec-remove]")
|
||||
.click();
|
||||
await w(1500);
|
||||
steps.push(["remove", lamps(), formLinks().join("")]);
|
||||
}
|
||||
out.steps = steps.map(([s, l, f]) => `${s}: ${l} (폼 ${f})`);
|
||||
out.back = steps[0][1] === steps.at(-1)[1];
|
||||
await release();
|
||||
}
|
||||
|
||||
if (ARGS.mode === "link" || ARGS.mode === "unlink") {
|
||||
await select(ROW);
|
||||
const target = linkRows().find((r) =>
|
||||
ARGS.mode === "link" ? !r.querySelector(".is-assigned") : r.querySelector(".is-assigned"),
|
||||
);
|
||||
if (ARGS.mode === "link") await pickLink(target);
|
||||
else {
|
||||
target.querySelector('[data-b05-summary="clear"]').click();
|
||||
await w(1500);
|
||||
}
|
||||
out.row = lampsOf(rowOf(ROW));
|
||||
out.saved = await save();
|
||||
await release();
|
||||
}
|
||||
|
||||
out.sig = sig();
|
||||
window.__m5415_out = JSON.stringify(out); // 응답이 끊겨도 뒤에 읽음
|
||||
return window.__m5415_out;
|
||||
})();
|
||||
Reference in New Issue
Block a user