Merge remote-tracking branch 'origin/dev' into sub_laptop_5
This commit is contained in:
@@ -64,17 +64,22 @@ def test_표_부품_combo_선택():
|
||||
assert "combo?: { onHeadEdit?: (colId: string, level: number, name: string) => void };" in sheet
|
||||
assert '...(ro || opts.combo ? [] : [el("p", { className: "ui-sheet__hint"' in sheet
|
||||
assert (
|
||||
"filter((k) => !opts.combo || k !== KEY_UNIT)" in sheet
|
||||
) # 방향키가 숨은 단위 줄을 안 밟음
|
||||
"const keys = opts.combo ? [KEY_UNIT] : navRows(state.doc, state.mode);" in sheet
|
||||
) # 방향키가 숨은 단위 · 번호 줄을 안 밟음
|
||||
assert 'head?.classList.add("is-head-picked")' in sheet
|
||||
assert "if (th && opts.combo?.onHeadEdit) editComboHead(th);" in sheet
|
||||
assert "if (opts.combo && sel.r === KEY_UNIT)" in sheet # F2 · Enter = 머리 이름 고침
|
||||
render = _text(SHEET / "ui_template_sheet_render.ts")
|
||||
assert "children: state.combo ? rows : [...rows, unit]" in render # 단위 줄 없음
|
||||
assert "if (state.combo) fitMeta(state, cols, mins);" in render
|
||||
# 45-3 일위대가 · 도면 · 면적산출 키 줄 안 보임(보기만)
|
||||
assert (
|
||||
'...(mode === "master" && p === 0 && !state.combo ? MASTER_ROWS.map(masterRow) : [])'
|
||||
in render
|
||||
)
|
||||
assert "fitMeta" not in render
|
||||
assert "열너비: {}" in render # 저장 폭 안 씀 — 자동
|
||||
css = _text(SHEET / "ui_template_sheet.css")
|
||||
assert ".ui-sheet--combo .ui-sheet__meta td" in css and "th.is-head-picked" in css
|
||||
assert ".ui-sheet--combo .ui-sheet__meta td" not in css and "th.is-head-picked" in css
|
||||
|
||||
|
||||
def test_머리_고침은_쓸_수_있는_층만_나무로():
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
/* 44-2 — B05 계획노선 편집 · 측점 횡단 글자 크기
|
||||
/* 44-2 — B05 계획노선 편집 · 측점 횡단 글자 크기 · 판 크기 바뀜에도 배율 1
|
||||
* 시작 화면: B05 종단설계(#/b05-profile) · 환경 검증 프로젝트(OWNERS.md) · 로그인됨 · 계획노선 편집 닫힘
|
||||
* · 창 너비 1500px 이상(그 아래면 오른쪽 칸이 접힘)
|
||||
* 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌)
|
||||
* 하는 일: [계획노선 편집] 열기 → 첫 측점 라벨 누름(앱 모듈의 라벨 상자 자리) → 횡단 판 글자 잼 → [취소]로 닫음
|
||||
* 기대 수치:
|
||||
* - keys = 원지반 · 기본 계획 횡단 · 「좌 … 표고 …」 셋 모두 fontSize 5.5px(옛 11px 의 절반)
|
||||
* 하는 일: [계획노선 편집] 열기 → 오른쪽 칸 크기를 세 갈래로 둔 채 측점 라벨 누름 → 칸을 원래대로 → 잼
|
||||
* folded = 칸 접힘(display none) 상태로 셈 → 펼침 · narrow = 260×300 으로 셈 → 원래
|
||||
* wide = 원래 크기로 셈 · shrink = wide 뒤 260×300 으로 줄임 → [취소]로 닫음
|
||||
* · 탭이 화면에 안 보여 프레임이 없으면(frames = false) ResizeObserver 대역(100ms 재기)을 깔고 돎
|
||||
* 기대 수치(갈래마다):
|
||||
* - sx · sy = 화면 크기 ÷ viewBox = 1(±0.02 · 글자가 늘어나 보이지 않음)
|
||||
* - keys = 원지반 · 기본 계획 횡단 · 「좌 … 표고 …」 셋 모두 fontSize 11px · screenPx(11 × sy) 10~12
|
||||
* - station.fontSize = title.fontSize(측점 표기 = 제목 「횡단」 크기)
|
||||
* - 바꾸는 값 없음(취소로 닫음 · 노선 안 고침)
|
||||
* - 바꾸는 값 없음(칸 크기 임시 style 지움 · 취소로 닫음 · 노선 안 고침)
|
||||
*/
|
||||
(async () => {
|
||||
const w = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
@@ -21,13 +26,50 @@
|
||||
[...document.querySelectorAll("button")].find(
|
||||
(b) => b.offsetParent && b.textContent.trim() === text,
|
||||
);
|
||||
// 화면에 안 보이는 ORCA 탭은 프레임을 안 그려 ResizeObserver 가 안 옴 — 그때만 100ms 마다
|
||||
// 크기를 재어 같은 방식(처음 한 번 · 바뀔 때마다)으로 부르는 대역을 편집 창 열기 전에 깖
|
||||
const OrigRO = window.ResizeObserver;
|
||||
const frames = await Promise.race([
|
||||
new Promise((r) => requestAnimationFrame(() => r(true))),
|
||||
w(500).then(() => false),
|
||||
]);
|
||||
if (!frames)
|
||||
window.ResizeObserver = class {
|
||||
constructor(cb) {
|
||||
this.cb = cb;
|
||||
this.t = new Map();
|
||||
this.iv = setInterval(() => {
|
||||
for (const [el, last] of this.t) {
|
||||
const r = el.getBoundingClientRect();
|
||||
const k = `${r.width}x${r.height}`;
|
||||
if (k === last) continue;
|
||||
this.t.set(el, k);
|
||||
this.cb([{ target: el }], this);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
observe(el) {
|
||||
if (!this.t.has(el)) this.t.set(el, null);
|
||||
}
|
||||
unobserve(el) {
|
||||
this.t.delete(el);
|
||||
}
|
||||
disconnect() {
|
||||
this.t.clear();
|
||||
clearInterval(this.iv);
|
||||
}
|
||||
};
|
||||
btn("계획노선 편집")?.click();
|
||||
const canvas = await until(() =>
|
||||
[...document.querySelectorAll("canvas")].find(
|
||||
(c) => c.offsetParent && c.closest("[class*='routeedit']"),
|
||||
),
|
||||
);
|
||||
window.ResizeObserver = OrigRO; // 편집 창(횡단 판)은 다 지어짐
|
||||
if (!canvas) return JSON.stringify({ ok: false, why: "편집 캔버스 없음" });
|
||||
const side = document.querySelector(".b05-routeedit__side");
|
||||
if (!side || !side.offsetParent)
|
||||
return JSON.stringify({ ok: false, why: "오른쪽 칸 접힘", innerWidth });
|
||||
// 앱이 실제로 부른 그 모듈 주소(@fs/<폴더>/…) — 같은 주소라야 라벨 상자 WeakMap 을 같이 씀
|
||||
const url = performance
|
||||
.getEntriesByType("resource")
|
||||
@@ -39,34 +81,75 @@
|
||||
return b.length ? b : null;
|
||||
});
|
||||
if (!boxes) return JSON.stringify({ ok: false, why: "측점 라벨 상자 없음" });
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const box = boxes[Math.floor(boxes.length / 2)];
|
||||
const at = { clientX: rect.left + box.x, clientY: rect.top + box.y, button: 0, bubbles: true };
|
||||
canvas.dispatchEvent(new PointerEvent("pointerdown", at));
|
||||
canvas.dispatchEvent(new PointerEvent("pointerup", at));
|
||||
const pane = await until(() =>
|
||||
[...document.querySelectorAll(".b05-routeedit__cross")].find((p) =>
|
||||
p.querySelector(".b05-routeedit__cross-key"),
|
||||
),
|
||||
);
|
||||
if (!pane) return JSON.stringify({ ok: false, why: "횡단 판 안 뜸", chainage: box.chainage });
|
||||
const fs = (el) => el && getComputedStyle(el).fontSize;
|
||||
const keys = [...pane.querySelectorAll(".b05-routeedit__cross-key")].map((t) => ({
|
||||
text: t.textContent.slice(0, 20),
|
||||
fontSize: fs(t),
|
||||
h: +t.getBoundingClientRect().height.toFixed(1),
|
||||
}));
|
||||
const title = pane.querySelector(".b05-routeedit__cross-title");
|
||||
const station = pane.querySelector(".b05-routeedit__cross-station");
|
||||
const out = {
|
||||
keys,
|
||||
title: { text: title.textContent, fontSize: fs(title) },
|
||||
station: { text: station.textContent, fontSize: fs(station) },
|
||||
const pane = side.querySelector(".b05-routeedit__cross");
|
||||
const draw = async () => {
|
||||
const old = pane.querySelector("svg");
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const box = boxes[Math.floor(boxes.length / 2)];
|
||||
const at = { clientX: rect.left + box.x, clientY: rect.top + box.y, button: 0, bubbles: true };
|
||||
canvas.dispatchEvent(new PointerEvent("pointerdown", at));
|
||||
canvas.dispatchEvent(new PointerEvent("pointerup", at));
|
||||
return until(() => {
|
||||
const svg = pane.querySelector("svg");
|
||||
return svg && svg !== old && svg.querySelector(".b05-routeedit__cross-key");
|
||||
});
|
||||
};
|
||||
out.ok =
|
||||
keys.length === 3 &&
|
||||
keys.every((k) => k.fontSize === "5.5px") &&
|
||||
out.title.fontSize === out.station.fontSize;
|
||||
btn("취소")?.click();
|
||||
return JSON.stringify(out);
|
||||
const fs = (el) => el && getComputedStyle(el).fontSize;
|
||||
const measure = (tag) => {
|
||||
const svg = pane.querySelector("svg");
|
||||
const vb = svg.viewBox.baseVal;
|
||||
const r = svg.getBoundingClientRect();
|
||||
const sx = +(r.width / vb.width).toFixed(3);
|
||||
const sy = +(r.height / vb.height).toFixed(3);
|
||||
const keys = [...pane.querySelectorAll(".b05-routeedit__cross-key")].map((t) => ({
|
||||
text: t.textContent.slice(0, 12),
|
||||
fontSize: fs(t),
|
||||
screenPx: +(parseFloat(fs(t)) * sy).toFixed(1),
|
||||
h: +t.getBoundingClientRect().height.toFixed(1),
|
||||
}));
|
||||
const title = pane.querySelector(".b05-routeedit__cross-title");
|
||||
const station = pane.querySelector(".b05-routeedit__cross-station");
|
||||
const res = {
|
||||
tag,
|
||||
viewBox: [vb.width, vb.height],
|
||||
box: [Math.round(r.width), Math.round(r.height)],
|
||||
sx,
|
||||
sy,
|
||||
keys,
|
||||
title: fs(title),
|
||||
station: fs(station),
|
||||
};
|
||||
res.ok =
|
||||
Math.abs(sx - 1) <= 0.02 &&
|
||||
Math.abs(sy - 1) <= 0.02 &&
|
||||
keys.length === 3 &&
|
||||
keys.every((k) => k.fontSize === "11px" && k.screenPx >= 10 && k.screenPx <= 12) &&
|
||||
res.title === res.station;
|
||||
return res;
|
||||
};
|
||||
const narrow = () => Object.assign(side.style, { width: "260px", height: "300px" });
|
||||
const reset = () => Object.assign(side.style, { display: "", width: "", height: "" });
|
||||
const out = [];
|
||||
try {
|
||||
side.style.display = "none";
|
||||
if (!(await draw())) return JSON.stringify({ ok: false, why: "접힘 · 횡단 안 뜸" });
|
||||
reset();
|
||||
await w(600);
|
||||
out.push(measure("folded"));
|
||||
narrow();
|
||||
if (!(await draw())) return JSON.stringify({ ok: false, why: "좁음 · 횡단 안 뜸" });
|
||||
reset();
|
||||
await w(600);
|
||||
out.push(measure("narrow"));
|
||||
if (!(await draw())) return JSON.stringify({ ok: false, why: "넓음 · 횡단 안 뜸" });
|
||||
await w(300);
|
||||
out.push(measure("wide"));
|
||||
narrow();
|
||||
await w(600);
|
||||
out.push(measure("shrink"));
|
||||
} finally {
|
||||
reset();
|
||||
btn("취소")?.click();
|
||||
}
|
||||
return JSON.stringify({ ok: out.every((o) => o.ok), innerWidth, frames, out });
|
||||
})();
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
* 로직 GC000277(건설품셈 토목 1-1-2 유도선 설치 및 해체)을 엶 · 고치지 않음 · 끝에 패널 취향을 처음 값으로 되돌림.
|
||||
* 기대 수치:
|
||||
* order = ["기본정보","시험 계산","단가산출 상세","텍스트 수식"](흐름 로직이면 셋째 · 넷째는 흐름 보기 안)
|
||||
* flow = {flow:true, source:false, pref:"flow"} · 오른쪽 칸 너비 > 300
|
||||
* source = {flow:false, source:true, pref:"source"} · srcHead 에 「1-1-2 유도선」 · srcTable 에 「특별인부」
|
||||
* none = {flow:false, source:false, pref:"none"} · 오른쪽 칸 너비 = 80(띠 둘)
|
||||
* back = {flow:true, source:false, pref:"flow"}(흐름도 띠 단추로 다시 폄)
|
||||
* 두 패널 = 공용 접기 카드(ui-collapsible · 제목 행 누름) · 흐름도 패널 자체 접기 단추는 안 보임(dfpToggle 0)
|
||||
* flow = {flow:true, source:false, pref:"flow", caret:["▾","▸"]} · 오른쪽 칸 너비 > 300
|
||||
* source = {flow:false, source:true, pref:"source", caret:["▸","▾"]} · srcHead 에 「1-1-2 유도선」 · srcTable 에 「특별인부」
|
||||
* none = {flow:false, source:false, pref:"none", caret:["▸","▸"]} · 오른쪽 칸 너비 = 140(제목 행 둘)
|
||||
* back = {flow:true, source:false, pref:"flow"}(흐름도 제목 행으로 다시 폄)
|
||||
* noLink = 「원문 연결 없음」 + 「출처 · …」(서버가 빈 목록을 줄 때 · fetch 를 잠시 바꿔 봄)
|
||||
* restored = 처음 취향 값
|
||||
*/
|
||||
@@ -43,17 +44,23 @@
|
||||
out.order = $$(".m01lab-flow__left .m01lab__box > .m01-logic__section-head h3").map((h) =>
|
||||
h.innerText.trim(),
|
||||
);
|
||||
const slots = () => $$(".m01lab-side__slot");
|
||||
const cards = () => $$(".m01lab-side__card");
|
||||
const heads = () => cards().map((c) => $(".ui-collapsible__title", c));
|
||||
const shown = (n) => !!n && n.getBoundingClientRect().height > 0;
|
||||
const state = () => ({
|
||||
flow:
|
||||
slots()[0].classList.contains("is-open") &&
|
||||
!$(".m02-dfp", slots()[0]).classList.contains("is-folded"),
|
||||
source: slots()[1].classList.contains("is-open") && !$(".m02-dfp__open", slots()[1]).hidden,
|
||||
!cards()[0].classList.contains("is-collapsed") &&
|
||||
shown($(".m02-dfp__open", cards()[0])) &&
|
||||
!$(".m02-dfp", cards()[0]).classList.contains("is-folded"),
|
||||
source: !cards()[1].classList.contains("is-collapsed") && shown($(".m01lab-src", cards()[1])),
|
||||
pref: localStorage.getItem(PREF),
|
||||
caret: heads().map((h) => getComputedStyle(h, "::after").content.replace(/"/g, "")),
|
||||
width: Math.round($(".m01lab-flow__right").getBoundingClientRect().width),
|
||||
});
|
||||
const flowToggle = () => $(".m02-dfp__toggle", slots()[0]);
|
||||
const srcToggle = () => $(".m02-dfp__toggle", slots()[1]);
|
||||
const flowToggle = () => heads()[0];
|
||||
const srcToggle = () => heads()[1];
|
||||
out.titles = heads().map((h) => h.innerText.trim());
|
||||
out.dfpToggle = $$(".m01lab-side .m02-dfp__toggle").filter(shown).length;
|
||||
// 처음 = 흐름도 열림으로 맞춤
|
||||
if (!state().flow) {
|
||||
if (state().source) srcToggle().click();
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/* 45-3 — 구조물 조합 상세 분류 표: 일위대가 · 도면 · 면적산출 키 줄 안 보임 · [저장] 뒤 장부 번호 그대로
|
||||
* 시작 화면: M02 마스터 템플릿 · 마스터 층(#/m02-master-template?open=구조물조합&root=master) · 시스템 관리자
|
||||
* 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글 · 20초 안)
|
||||
* 쓰는 것: 표 [저장] 한 번(고침 없음) — 끝나면 resources/master_template 사본으로 되씀
|
||||
* 기대 수치:
|
||||
* - metaRows = 0 · refRowNames = 0(줄 머리 「일위대가 · 도면 · 면적산출」) · refCells = 0(data-r s:price|drawing|area)
|
||||
* - headPick = true(머리 칸 누르면 열 고름 · 카드 셋) · keyMove = true(방향키로 옆 열)
|
||||
* - book.same = true(저장 앞뒤 장부 줄의 키 · 열 · 도번 · 면적번 같음) · book.laborAfter = rows(일위대가번 모두 있음)
|
||||
* (git 장부가 옛 꼴(재료번)이면 저장 때 서버 맞춤이 일위대가번으로 바꿈 — 보기와 무관)
|
||||
*/
|
||||
(async () => {
|
||||
const w = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const until = async (fn, ms = 6000) => {
|
||||
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 sheet = await until(() => document.querySelector(".ui-sheet--combo"));
|
||||
if (!sheet) return JSON.stringify({ error: "구조물 조합 표 없음" });
|
||||
const out = {};
|
||||
const gutters = () =>
|
||||
[...sheet.querySelectorAll(".ui-sheet__gutter")].map((g) => g.textContent.trim());
|
||||
out.metaRows = sheet.querySelectorAll(".ui-sheet__meta").length;
|
||||
out.refRowNames = gutters().filter((t) => ["일위대가", "도면", "면적산출"].includes(t)).length;
|
||||
out.refCells = sheet.querySelectorAll(
|
||||
'[data-r="s:price"],[data-r="s:drawing"],[data-r="s:area"]',
|
||||
).length;
|
||||
out.gutters = gutters();
|
||||
|
||||
// 머리 칸 누르면 열 고름(카드) · 방향키 = 옆 열
|
||||
const heads = [...sheet.querySelectorAll("thead th[data-col]")].filter(vis);
|
||||
const deepest = Math.max(...heads.map((h) => Number(h.dataset.level)));
|
||||
const leaves = heads.filter((h) => Number(h.dataset.level) === deepest);
|
||||
leaves[0].click();
|
||||
await w(400);
|
||||
out.headPick = !!sheet.querySelector("th.is-head-picked");
|
||||
const first = sheet.querySelector("th.is-head-picked")?.dataset.col;
|
||||
sheet.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true }));
|
||||
await w(300);
|
||||
const second = sheet.querySelector("th.is-head-picked")?.dataset.col;
|
||||
out.keyMove = !!first && !!second && first !== second;
|
||||
|
||||
// [저장] 앞뒤 장부(구조물집계표조합) 번호
|
||||
const book = () =>
|
||||
fetch("/api/m02/layers/system/structure-combos", { credentials: "include" })
|
||||
.then((r) => r.json())
|
||||
.then((b) =>
|
||||
(b.줄 ?? []).map((r) => ({
|
||||
id: JSON.stringify([r.키, r.열, r.도번, r.면적번]),
|
||||
su: r.일위대가번,
|
||||
})),
|
||||
);
|
||||
const before = await book();
|
||||
const save = [...document.querySelectorAll("button")].find(
|
||||
(b) => vis(b) && b.innerText.trim() === "저장",
|
||||
);
|
||||
save.click();
|
||||
await w(2500);
|
||||
const after = await book();
|
||||
out.book = {
|
||||
rows: before.length,
|
||||
same: JSON.stringify(before.map((r) => r.id)) === JSON.stringify(after.map((r) => r.id)),
|
||||
laborAfter: after.filter((r) => r.su).length,
|
||||
sample: after[0],
|
||||
};
|
||||
out.metaAfterSave = document.querySelectorAll(".ui-sheet--combo .ui-sheet__meta").length;
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
Reference in New Issue
Block a user