Merge remote-tracking branch 'origin/dev' into sub_laptop_2
This commit is contained in:
@@ -112,6 +112,13 @@ export function createDesignPanel(ctx: DesignPanelContext, view: DesignView): De
|
||||
const dockParts: HTMLElement[] = [];
|
||||
const dock = createDesignDock(view, dockParts, ctx.dock);
|
||||
|
||||
// 칸은 공용 스크롤 칸(쪼갠 뒤) 안에 둔다 — 쪼개기 전이면 패널 루트의 단추 묶음 앞.
|
||||
const place = (element: HTMLElement): void => {
|
||||
const scroll = root.querySelector<HTMLElement>(":scope > .ui-sidebar-scroll");
|
||||
if (scroll) scroll.append(element);
|
||||
else root.insertBefore(element, dock.root);
|
||||
};
|
||||
|
||||
const show = (target: DesignView): void => {
|
||||
const wanted = new Set(panelSectionsFor(target).map((def) => def.id));
|
||||
for (const def of panelSectionsFor(target)) {
|
||||
@@ -120,12 +127,12 @@ export function createDesignPanel(ctx: DesignPanelContext, view: DesignView): De
|
||||
const sectionRoot = "root" in made ? made.root : made;
|
||||
if ("root" in made && made.dock) dock.root.prepend(made.dock);
|
||||
built.set(def.id, sectionRoot);
|
||||
root.insertBefore(sectionRoot, dock.root);
|
||||
place(sectionRoot);
|
||||
}
|
||||
// 차례를 정의 순서로 다시 맞춘다 — 늦게 만들어진 칸이 맨 아래에 남지 않게.
|
||||
for (const def of panelSectionsFor(target)) {
|
||||
const element = built.get(def.id);
|
||||
if (element) root.insertBefore(element, dock.root);
|
||||
if (element) place(element);
|
||||
}
|
||||
built.forEach((element, id) => {
|
||||
// 칸이 스스로 숨긴 것(선택 포인트)은 건드리지 않고, 보기 밖 칸만 가리고 되살린다.
|
||||
@@ -144,7 +151,9 @@ export function createDesignPanel(ctx: DesignPanelContext, view: DesignView): De
|
||||
dock,
|
||||
setView: (next) => {
|
||||
dock.setView(next);
|
||||
root.className = next === "profile" ? "b05-route__panel" : "b06-profile__form";
|
||||
// 클래스 통째 덮기 금지 — 공용 쪼개기가 붙인 ui-sidebar-fill 이 지워져 단추 묶음이 바닥에서 뜬다.
|
||||
root.classList.toggle("b05-route__panel", next === "profile");
|
||||
root.classList.toggle("b06-profile__form", next !== "profile");
|
||||
show(next);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
import { ROUTES } from "@config/config_frontend";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { splitSidebarActions } from "@ui/ui_template_overlay";
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
import { goToWorkflowStage, WORKFLOW_STEP_ROUTES } from "../b_workflow_nav";
|
||||
import { navigateTo } from "../router";
|
||||
@@ -229,6 +230,9 @@ export function sharedPanel(shared: DesignPageShared): DesignPanel {
|
||||
shared.view(),
|
||||
);
|
||||
shared.left.append(panel.root);
|
||||
// 틀이 먼저 섰으면 좌측 자리가 빈 채로 지나갔다 — 붙인 지금 [스크롤 칸][아래 고정 단추]로 쪼갠다.
|
||||
const body = shared.left.closest<HTMLElement>(".ui-workflow-overlay__body");
|
||||
if (body) splitSidebarActions(body);
|
||||
shared.panel = panel;
|
||||
return panel;
|
||||
}
|
||||
|
||||
@@ -178,7 +178,6 @@ function buildSystemSettingsPanel(user: DashboardUser, companyId: number | null)
|
||||
settingCard(L("B01_Dashboard_MasterElements"), L("B01_Dashboard_PermSourceRead"), () =>
|
||||
navigateTo(ROUTES.M01_MASTER_DATA),
|
||||
),
|
||||
m02(L("B01_Dashboard_CardUnitCombo"), "일위대가", "master", desc),
|
||||
m02(L("B01_Dashboard_CardStructCombo"), "구조물조합", "master", desc),
|
||||
m02(L("B01_Dashboard_CardStructOption"), "옵션", "master", desc),
|
||||
m02(L("B01_Dashboard_CardDrawing"), "도면", "master", desc),
|
||||
@@ -193,10 +192,7 @@ function buildSystemSettingsPanel(user: DashboardUser, companyId: number | null)
|
||||
L(companyAdmin ? "B01_Dashboard_PermCompanyWrite" : "B01_Dashboard_PermCompanyRead"),
|
||||
L("B01_Dashboard_PermPersonalWrite"),
|
||||
].join(" · ");
|
||||
wrap.append(
|
||||
m02(L("B01_Dashboard_CardUnitCombo"), "일위대가", root, desc),
|
||||
m02(L("B01_Dashboard_CardStructCombo"), "구조물조합", root, desc),
|
||||
);
|
||||
wrap.append(m02(L("B01_Dashboard_CardStructCombo"), "구조물조합", root, desc));
|
||||
return wrap;
|
||||
}
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@
|
||||
|
||||
.b05-routeedit__cross-station {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-caption);
|
||||
font-size: inherit; /* 44-2 — 제목(횡단)과 같은 크기 */
|
||||
}
|
||||
|
||||
/* 횡단 판 — 캔버스 그림이 아니라 **SVG 선**이다(2026-09-12 사용자 지시 ③).
|
||||
@@ -235,7 +235,7 @@
|
||||
|
||||
.b05-routeedit__cross-key {
|
||||
fill: var(--color-text-secondary, #94a3b8);
|
||||
font-size: 11px;
|
||||
font-size: 5.5px; /* 44-2 — 지금(11px)의 절반 */
|
||||
}
|
||||
|
||||
.b05-routeedit__cross-key.is-design {
|
||||
|
||||
@@ -43,6 +43,7 @@ import { logicTable, matTable, qtyShort } from "./M02_MasterTemplete_Labor_Rows"
|
||||
import { tc } from "./M02_MasterTemplete_Labor_Text";
|
||||
import { keepKnown } from "./M02_Labor_Values_Model";
|
||||
import { pickMaterial } from "./M02_Material_Pick";
|
||||
import { createUnitPick, NO_UNIT } from "./DataFlow/M02_DF_UnitPick";
|
||||
import "../M01_MasterData/M01_MasterData_UI_Logic_Style.css";
|
||||
import "../M01_MasterData/M01_MasterData_UI_LogicLab_Style.css";
|
||||
import "@ui/ui_template_general_layout.css";
|
||||
@@ -341,15 +342,27 @@ export function mountLaborDetail(
|
||||
}),
|
||||
});
|
||||
|
||||
const unit = createInputField({ type: "text", label: tc("Labor_Unit") });
|
||||
unit.input.value = cur.단위 ?? "";
|
||||
unit.input.readOnly = !edit;
|
||||
unit.input.maxLength = 20;
|
||||
unit.input.setAttribute("data-labor-field", "단위");
|
||||
unit.input.addEventListener("input", () => {
|
||||
cur.단위 = unit.input.value.trim() || null;
|
||||
opt.onChanged(dirty());
|
||||
});
|
||||
// 단위 = 표준 단위 목록에서 고르기(지금 값이 목록에 없어도 선택지로 둠) · 저장은 [저장]
|
||||
const unit = {
|
||||
root: el("div", { className: "ui-field", attrs: { "data-labor-field": "단위" } }),
|
||||
};
|
||||
const unit0 = cur.단위;
|
||||
const drawUnit = (): void =>
|
||||
unit.root.replaceChildren(
|
||||
el("label", { className: "ui-field__label", text: tc("Labor_Unit") }),
|
||||
createUnitPick({
|
||||
value: cur.단위 || NO_UNIT,
|
||||
extra: [unit0, cur.단위].filter((u): u is string => !!u),
|
||||
readOnly: !edit,
|
||||
ariaLabel: tc("Labor_Unit"),
|
||||
onPick: (u) => {
|
||||
cur.단위 = u === NO_UNIT ? null : u;
|
||||
opt.onChanged(dirty());
|
||||
drawUnit();
|
||||
},
|
||||
}),
|
||||
);
|
||||
drawUnit();
|
||||
const origin = createInputField({ type: "text", label: tc("Labor_Origin") });
|
||||
origin.input.value = cur.본뜬?.키 ? `${cur.본뜬.층} ${cur.본뜬.키}` : tc("Origin_Self");
|
||||
origin.input.readOnly = true;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Layer.ts
|
||||
* M02 한 화면의 뿌리 셋(마스터 · 회사 · 개인 · PLAN 27-1) — 진입 해시
|
||||
* `#/m02-master-template?open=구조물조합|일위대가|옵션|도면&root=master|company|personal&owner=…&company=…`
|
||||
* `#/m02-master-template?open=구조물조합|옵션|도면&root=master|company|personal&owner=…&company=…`
|
||||
* (옛 `?layer=` 도 root 로 읽음) · 고칠 권한(`accessFor` — 서버 `M02_Layer_Access` 와 짝 · 27-1c) ·
|
||||
* 회사 없음 안내 · 회사용 읽기만일 때 [개인용으로 복사] 막대.
|
||||
* ========================================================================== */
|
||||
@@ -16,7 +16,7 @@ import { copyItemToPersonal, fetchStructureCombos } from "./M02_MasterTemplete_A
|
||||
export type ManageLayer = "system" | "company" | "personal";
|
||||
/** 해시 · 화면의 뿌리 이름 — 서버 층 이름은 `layerOfRoot` */
|
||||
export type Root = "master" | "company" | "personal";
|
||||
export type OpenTarget = "구조물조합" | "일위대가" | "옵션" | "도면";
|
||||
export type OpenTarget = "구조물조합" | "옵션" | "도면";
|
||||
|
||||
export interface Entry {
|
||||
root: Root;
|
||||
@@ -27,7 +27,7 @@ export interface Entry {
|
||||
company: string | null;
|
||||
}
|
||||
|
||||
const OPENS: OpenTarget[] = ["구조물조합", "일위대가", "옵션", "도면"];
|
||||
const OPENS: OpenTarget[] = ["구조물조합", "옵션", "도면"];
|
||||
|
||||
export const layerOfRoot = (root: Root): ManageLayer => (root === "master" ? "system" : root);
|
||||
|
||||
|
||||
@@ -226,7 +226,7 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise<Mounte
|
||||
root.innerHTML = "";
|
||||
root.append(layout);
|
||||
void main.open(home);
|
||||
// 진입 해시 `open` — 옵션 · 도면 컨테이너는 마스터에만 · 일위대가는 구조물 조합 하위(세 층) ·
|
||||
// 진입 해시 `open` — 옵션 · 도면 컨테이너는 마스터에만 ·
|
||||
// 옵션 화면이 뒤에 열리면 편집 칸 차례(`seq`)가 늦게 온 집계표를 버림
|
||||
if (entry.open !== "구조물조합") side.reveal(entry.open);
|
||||
void tree.refresh();
|
||||
|
||||
@@ -94,7 +94,7 @@ export interface SideHandle {
|
||||
/** 고름 표시를 지금 열린 것 한 줄로 — 편집 칸이 무엇을 열든(카드 [도면 수정] 등) 부름 */
|
||||
paint: () => void;
|
||||
/** 진입 해시 `open`(PLAN 27-1d) — 그 컨테이너를 펼침(옵션은 열기까지) · 옵션 · 도면은 시스템 층에만 */
|
||||
reveal: (target: "옵션" | "도면" | "일위대가") => void;
|
||||
reveal: (target: "옵션" | "도면") => void;
|
||||
}
|
||||
|
||||
/** 서버 이름 규칙과 같게 — 화면에서 먼저 막음 */
|
||||
@@ -414,14 +414,7 @@ export function buildSide(opt: SideOptions): SideHandle {
|
||||
}
|
||||
}
|
||||
|
||||
const reveal = (target: "옵션" | "도면" | "일위대가"): void => {
|
||||
// 일위대가 = 구조물 조합(처음부터 펼침) 안 일위대가 하위를 펼치고 「전체 항목」
|
||||
if (target === "일위대가") {
|
||||
columnSections[COLUMN_KINDS.findIndex((k) => k.kind === "labor")].classList.remove(
|
||||
"is-collapsed",
|
||||
);
|
||||
return void selectAll("labor");
|
||||
}
|
||||
const reveal = (target: "옵션" | "도면"): void => {
|
||||
extras?.[target].classList.remove("is-collapsed");
|
||||
if (target === "옵션") options.body.querySelector<HTMLElement>("button")?.click();
|
||||
};
|
||||
|
||||
@@ -70,31 +70,28 @@ WRITE_CO = READ_CO.replace("CompanyRead", "CompanyWrite")
|
||||
|
||||
|
||||
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
||||
def test_시스템_관리자_카드_다섯_고른_회사_해시():
|
||||
def test_시스템_관리자_카드_넷_고른_회사_해시():
|
||||
m = "B01_Dashboard_PermMasterWrite"
|
||||
assert _cards({"role": "SYSTEM_ADMIN", "is_master": False}, 3) == [
|
||||
["B01_Dashboard_MasterElements", "B01_Dashboard_PermSourceRead", "m01-master-data"],
|
||||
["B01_Dashboard_CardUnitCombo", m, f"{M02}?open=일위대가&root=master&company=3"],
|
||||
["B01_Dashboard_CardStructCombo", m, f"{M02}?open=구조물조합&root=master&company=3"],
|
||||
["B01_Dashboard_CardStructOption", m, f"{M02}?open=옵션&root=master&company=3"],
|
||||
["B01_Dashboard_CardDrawing", m, f"{M02}?open=도면&root=master&company=3"],
|
||||
]
|
||||
# 회사가 없으면 해시에 안 실음
|
||||
assert _cards({"role": "SYSTEM_ADMIN", "is_master": False})[1][2] == (
|
||||
f"{M02}?open=일위대가&root=master"
|
||||
f"{M02}?open=구조물조합&root=master"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
||||
def test_회사_관리자_개인_카드_둘():
|
||||
def test_회사_관리자_개인_카드_하나():
|
||||
company = [
|
||||
["B01_Dashboard_CardUnitCombo", WRITE_CO, f"{M02}?open=일위대가&root=company"],
|
||||
["B01_Dashboard_CardStructCombo", WRITE_CO, f"{M02}?open=구조물조합&root=company"],
|
||||
]
|
||||
assert _cards({"role": "ADMIN", "is_master": False}, 3) == company # 회사 인자 안 실음
|
||||
assert _cards({"role": "USER", "is_master": True}) == company # 회사 마스터 = 회사 수정
|
||||
assert _cards({"role": "USER", "is_master": False}) == [
|
||||
["B01_Dashboard_CardUnitCombo", READ_CO, f"{M02}?open=일위대가&root=personal"],
|
||||
["B01_Dashboard_CardStructCombo", READ_CO, f"{M02}?open=구조물조합&root=personal"],
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""B05 · B06 좌측 패널 아래 고정(계획서 44-3) — 단추 묶음이 패널 바닥에 붙고 위 칸만 스크롤.
|
||||
|
||||
공용 쪼개기(`splitSidebarActions`)가 [스크롤 칸][아래 고정 단추] 로 나누는데, 한 페이지(design_page)는
|
||||
좌측 자리를 빈 채 틀에 넘겨 쪼개기가 지나가고, 보기 전환이 패널 클래스를 통째 덮어 `ui-sidebar-fill` 을
|
||||
지웠다. 세 자리가 다시 어긋나면 빨강. 화면 수치는 `ui_checks/44-3_좌측패널_하단고정.js`.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
OVERLAY = ROOT / "ui_template" / "ui_template_overlay.ts"
|
||||
PANEL = ROOT / "A00_Common" / "design" / "design_panel.ts"
|
||||
SHARED = ROOT / "A00_Common" / "design" / "design_shared.ts"
|
||||
|
||||
|
||||
def read(path: Path) -> str:
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_split_is_exported_and_idempotent():
|
||||
src = read(OVERLAY)
|
||||
assert "export function splitSidebarActions(" in src
|
||||
assert 'if (body.classList.contains("ui-sidebar-split")) return;' in src
|
||||
|
||||
|
||||
def test_shared_panel_splits_after_attach():
|
||||
src = read(SHARED)
|
||||
attach = src.index("shared.left.append(panel.root);")
|
||||
assert "splitSidebarActions(body)" in src[attach : attach + 400]
|
||||
|
||||
|
||||
def test_panel_keeps_fill_class_and_scroll_host():
|
||||
src = read(PANEL)
|
||||
# 보기 전환이 클래스를 통째 덮으면 ui-sidebar-fill 이 지워져 단추 묶음이 바닥에서 뜬다.
|
||||
assert "root.className = next" not in src
|
||||
# 늦게 서는 칸은 스크롤 칸 안으로 — 패널 루트에 바로 끼우면 스크롤 밖에 남는다.
|
||||
assert ":scope > .ui-sidebar-scroll" in src
|
||||
@@ -80,3 +80,13 @@ def test_일위대가_카드는_상세_카드_갈래():
|
||||
rows = _text("M02_MasterTemplete_Labor_Rows.ts")
|
||||
assert "const tools = !!(move || remove);" in rows and "compactTable" not in rows
|
||||
assert "compact" not in _text("M02_MasterTemplete_Labor_View.ts").split("*/", 1)[1]
|
||||
|
||||
|
||||
def test_일위대가_상세_단위는_표준_단위_고르기():
|
||||
"""44-7 — 상세 머리 「단위」 = 표준 단위 목록(UnitPick) · 지금 값이 목록에 없어도 선택지 ·
|
||||
고르면 캐시만(저장은 상세 [저장]) · 「-」 = 없음."""
|
||||
detail = _text("M02_MasterTemplete_Labor_Detail.ts")
|
||||
assert 'import { createUnitPick, NO_UNIT } from "./DataFlow/M02_DF_UnitPick";' in detail
|
||||
assert "extra: [unit0, cur.단위].filter((u): u is string => !!u)," in detail
|
||||
assert "cur.단위 = u === NO_UNIT ? null : u;" in detail
|
||||
assert "readOnly: !edit," in detail and "maxLength = 20" not in detail
|
||||
|
||||
@@ -41,10 +41,10 @@ def test_하위_차례_전체_항목_이름():
|
||||
assert '| "labor";' in api and "일위대가번: string;" in api
|
||||
assert "onAll?: (kind: AllKind) => void;" in side # 화면은 ItemList(39-5) · 페이지가 이음
|
||||
assert "if (!opt.onAll || allPicked === kind || !(await opt.confirmLeave())) return;" in side
|
||||
assert 'return void selectAll("labor");' in side # 해시 open=일위대가
|
||||
assert 'selectAll("labor")' not in side # 해시 open=일위대가 길 없앰(44-7)
|
||||
locale = (ROOT / "ui_template" / "ui_template_locale_m2.ts").read_text("utf-8")
|
||||
assert 'M02_KindLabor: ["일위대가"' in locale and 'M02_ItemsAll: ["전체 항목"' in locale
|
||||
assert 'B01_Dashboard_CardUnitCombo: ["일위대가",' in locale
|
||||
assert "B01_Dashboard_CardUnitCombo" not in locale # 대시보드 일위대가 카드 없앰(44-7)
|
||||
css = _text("M02_MasterTemplete_UI_Style.css")
|
||||
assert ".m02-side__rule {" in css and "border-top: 1px solid var(--color-border);" in css
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ def test_주소_인자를_라우트에서_뗌():
|
||||
# 진입 해시 `?open=&root=&owner=`(PLAN 27-1d) · 옛 `?layer=` 도 root 로
|
||||
assert 'query.get("root") ?? query.get("layer")' in layer
|
||||
assert 'raw === "company" || raw === "personal" ? raw : "master"' in layer
|
||||
assert '["구조물조합", "일위대가", "옵션", "도면"]' in layer
|
||||
assert '["구조물조합", "옵션", "도면"]' in layer # 일위대가 = 구조물 조합 안(44-7)
|
||||
picker = _text(ROOT / "ui_template" / "ui_template_item_picker.ts")
|
||||
assert 'open: "구조물조합", root: "personal"' in picker and "layer=personal" not in picker
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/* 44-2 — B05 계획노선 편집 · 측점 횡단 글자 크기
|
||||
* 시작 화면: B05 종단설계(#/b05-profile) · 환경 검증 프로젝트(OWNERS.md) · 로그인됨 · 계획노선 편집 닫힘
|
||||
* 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌)
|
||||
* 하는 일: [계획노선 편집] 열기 → 첫 측점 라벨 누름(앱 모듈의 라벨 상자 자리) → 횡단 판 글자 잼 → [취소]로 닫음
|
||||
* 기대 수치:
|
||||
* - keys = 원지반 · 기본 계획 횡단 · 「좌 … 표고 …」 셋 모두 fontSize 5.5px(옛 11px 의 절반)
|
||||
* - station.fontSize = title.fontSize(측점 표기 = 제목 「횡단」 크기)
|
||||
* - 바꾸는 값 없음(취소로 닫음 · 노선 안 고침)
|
||||
*/
|
||||
(async () => {
|
||||
const w = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const until = async (fn, ms = 15000) => {
|
||||
for (let t = 0; t < ms; t += 200) {
|
||||
const v = fn();
|
||||
if (v) return v;
|
||||
await w(200);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const btn = (text) =>
|
||||
[...document.querySelectorAll("button")].find(
|
||||
(b) => b.offsetParent && b.textContent.trim() === text,
|
||||
);
|
||||
btn("계획노선 편집")?.click();
|
||||
const canvas = await until(() =>
|
||||
[...document.querySelectorAll("canvas")].find(
|
||||
(c) => c.offsetParent && c.closest("[class*='routeedit']"),
|
||||
),
|
||||
);
|
||||
if (!canvas) return JSON.stringify({ ok: false, why: "편집 캔버스 없음" });
|
||||
// 앱이 실제로 부른 그 모듈 주소(@fs/<폴더>/…) — 같은 주소라야 라벨 상자 WeakMap 을 같이 씀
|
||||
const url = performance
|
||||
.getEntriesByType("resource")
|
||||
.map((e) => e.name)
|
||||
.find((n) => n.includes("B05_Profile_UI_RouteEdit_Input.ts"));
|
||||
const input = await import(url);
|
||||
const boxes = await until(() => {
|
||||
const b = input.stationLabelBoxes(canvas);
|
||||
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) },
|
||||
};
|
||||
out.ok =
|
||||
keys.length === 3 &&
|
||||
keys.every((k) => k.fontSize === "5.5px") &&
|
||||
out.title.fontSize === out.station.fontSize;
|
||||
btn("취소")?.click();
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
@@ -0,0 +1,69 @@
|
||||
/* 44-3 — B05 · B06 좌측 패널 아래 고정(구조물 목록 · 초기화 · 저장 · 보기 전환 단추 묶음)
|
||||
* 시작 화면: B05 종단설계(#/b05-profile) · 좌측 패널 열림 · 프로젝트 = 환경 배정 storage 프로젝트
|
||||
* 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌)
|
||||
* 기대 수치(B05 → B06 → B05 · 각 보기에서 보통 · 긴 패널 둘):
|
||||
* - split = true · 패널 루트 자식 = [ui-sidebar-scroll, 단추 묶음] · 칸은 모두 스크롤 칸 안(loose = 0)
|
||||
* - dockGap = 패널 몸통 바닥 − 단추 묶음 바닥 = 0(±1px)
|
||||
* - 긴 패널(스크롤 칸에 2000px 임시 칸): overflow = true · dockGap 그대로 0 · 단추 묶음 top 안 바뀜
|
||||
* - 끝나면 임시 칸 지우고 처음 주소로 되돌림
|
||||
*/
|
||||
(async () => {
|
||||
const w = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const start = location.hash;
|
||||
const measure = async (tag) => {
|
||||
const panel = document.querySelector(".b05-route__panel, .b06-profile__form");
|
||||
if (!panel) return { tag, error: "패널 없음" };
|
||||
const body = panel.closest(".ui-workflow-overlay__body");
|
||||
const scroll = panel.querySelector(":scope > .ui-sidebar-scroll");
|
||||
const dock = panel.querySelector(":scope > .ui-sidebar-actions");
|
||||
const kids = [...panel.children].map((c) => c.classList[0]);
|
||||
const gap = () =>
|
||||
Math.round(body.getBoundingClientRect().bottom - dock.getBoundingClientRect().bottom);
|
||||
const res = {
|
||||
tag,
|
||||
view: panel.classList.contains("b06-profile__form") ? "B06" : "B05",
|
||||
split: body.classList.contains("ui-sidebar-split"),
|
||||
kids,
|
||||
loose: kids.filter((k) => k !== "ui-sidebar-scroll" && !dock?.classList.contains(k)).length,
|
||||
dockGap: dock ? gap() : null,
|
||||
};
|
||||
if (!scroll || !dock) return res;
|
||||
const top0 = Math.round(dock.getBoundingClientRect().top);
|
||||
const tall = document.createElement("div");
|
||||
tall.style.cssText = "height: 2000px; flex: none";
|
||||
scroll.append(tall);
|
||||
await w(100);
|
||||
res.long = {
|
||||
overflow: scroll.scrollHeight > scroll.clientHeight,
|
||||
dockGap: gap(),
|
||||
dockMoved: Math.round(dock.getBoundingClientRect().top) - top0,
|
||||
};
|
||||
tall.remove();
|
||||
res.ok =
|
||||
res.split &&
|
||||
res.loose === 0 &&
|
||||
Math.abs(res.dockGap) <= 1 &&
|
||||
res.long.overflow &&
|
||||
Math.abs(res.long.dockGap) <= 1 &&
|
||||
res.long.dockMoved === 0;
|
||||
return res;
|
||||
};
|
||||
const go = async (hash) => {
|
||||
location.hash = hash;
|
||||
for (let i = 0; i < 40; i++) {
|
||||
await w(250);
|
||||
const want = hash.includes("b06") ? ".b06-profile__form" : ".b05-route__panel";
|
||||
if (document.querySelector(want + " > .ui-sidebar-actions")) break;
|
||||
}
|
||||
await w(500);
|
||||
};
|
||||
const out = [];
|
||||
await go("#/b05-profile");
|
||||
out.push(await measure("B05"));
|
||||
await go("#/b06-section");
|
||||
out.push(await measure("B06"));
|
||||
await go("#/b05-profile");
|
||||
out.push(await measure("B05 다시"));
|
||||
if (location.hash !== start) await go(start);
|
||||
return JSON.stringify({ ok: out.every((r) => r.ok), out });
|
||||
})();
|
||||
@@ -0,0 +1,78 @@
|
||||
/* 44-7 — ① M02 일위대가 상세 머리 「단위」 = 표준 단위 드롭다운 ② 대시보드 「시스템 설정」 일위대가 카드 없음
|
||||
* 시작 화면: 아무 화면 · 시스템 관리자 로그인 · M02 마스터 층에 구조물 열 하나 이상
|
||||
* 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌 · 20초 안)
|
||||
* 기대 수치:
|
||||
* - cards = ["마스터 데이터", "구조물 조합", "구조물 옵션", "도면 양식"] · hasLabor = false
|
||||
* - pick = 1(단추) · textInput = 0(손글 칸 없음) · options > 10 · hasNow = true(지금 값이 선택지에 있음)
|
||||
* - 다른 단위 고름 → after = 고른 값 · dirty = true(다른 열로 가려면 버림 물음 · 「취소」)
|
||||
* - 처음 값 다시 고름 → back = 처음 값 · dirtyBack = false(물음 없이 옮겨 감 · 저장 안 함 · 바꾼 값 되돌림)
|
||||
*/
|
||||
(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 out = {};
|
||||
// ② 대시보드 시스템 설정 카드
|
||||
location.hash = "#/b01-account";
|
||||
await until(() => document.querySelector(".b01-dashboard__setting-title"));
|
||||
out.cards = [...document.querySelectorAll(".b01-dashboard__setting-title")].map(
|
||||
(e) => e.textContent,
|
||||
);
|
||||
out.hasLabor = out.cards.includes("일위대가");
|
||||
// ① M02 일위대가 상세(좌측 일위대가 하위 첫 줄)
|
||||
location.hash = "#/m02-master-template?open=구조물조합&root=master";
|
||||
// 하위 줄은 다시 그려짐 — 누를 때마다 새로 찾음
|
||||
const row = () =>
|
||||
document
|
||||
.querySelector('[data-all="labor"]')
|
||||
?.parentElement?.querySelector(".m02-side__item:not([data-all])");
|
||||
if (!(await until(row))) return JSON.stringify({ ...out, error: "일위대가 하위 없음" });
|
||||
await w(500);
|
||||
row().click();
|
||||
const field = await until(() => document.querySelector('[data-labor-field="단위"]'));
|
||||
if (!field) return JSON.stringify({ ...out, error: "단위 칸 없음" });
|
||||
// 고침 표시 = 다른 열(하위 둘째 줄)로 갈 때 버림 물음이 뜨는지 — 뜨면 「취소」
|
||||
const asks = async () => {
|
||||
document
|
||||
.querySelector('[data-all="labor"]')
|
||||
.parentElement.querySelectorAll(".m02-side__item:not([data-all])")[1]
|
||||
.click();
|
||||
const ask = await until(() => document.querySelector(".ui-confirm"), 1500);
|
||||
[...(ask?.querySelectorAll("button") ?? [])].find((b) => b.textContent === "취소")?.click();
|
||||
await w(200);
|
||||
return !!ask;
|
||||
};
|
||||
const btn = () => document.querySelector('[data-labor-field="단위"] .m02-df-unitpick > button');
|
||||
const items = async () => {
|
||||
btn().click();
|
||||
return until(() => {
|
||||
const li = document.querySelectorAll('[data-labor-field="단위"] .m02-df-unitpick__item');
|
||||
return li.length ? [...li] : null;
|
||||
});
|
||||
};
|
||||
out.pick = document.querySelectorAll('[data-labor-field="단위"] .m02-df-unitpick').length;
|
||||
out.textInput = field.querySelectorAll("input[type=text]:not(.m02-df-unitpick__filter)").length;
|
||||
const u0 = btn().textContent;
|
||||
out.before = u0;
|
||||
const list = await items();
|
||||
out.options = list.length;
|
||||
out.hasNow = list.some((b) => b.classList.contains("is-now"));
|
||||
const other = list.find((b) => !b.classList.contains("is-now") && !b.textContent.startsWith("-"));
|
||||
other.click();
|
||||
await w(200);
|
||||
out.after = btn().textContent;
|
||||
out.dirty = await asks();
|
||||
// 되돌림 — 처음 값(없음이면 「-」)
|
||||
const again = await items();
|
||||
again.find((b) => (u0 === "-" ? b.textContent.startsWith("-") : b.textContent === u0)).click();
|
||||
await w(200);
|
||||
out.back = btn().textContent;
|
||||
out.dirtyBack = await asks();
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
@@ -4,7 +4,6 @@
|
||||
* ========================================================================== */
|
||||
|
||||
export const ui_locales_m2 = {
|
||||
B01_Dashboard_CardUnitCombo: ["일위대가", "Unit costs"],
|
||||
B01_Dashboard_CardStructCombo: ["구조물 조합", "Structure combos"],
|
||||
B01_Dashboard_CardStructOption: ["구조물 옵션", "Structure options"],
|
||||
B01_Dashboard_CardDrawing: ["도면 양식", "Drawing templates"],
|
||||
|
||||
@@ -112,7 +112,9 @@ function readOpenState(key: string, collapseWhenNarrow = false): boolean {
|
||||
* 아예 빼고, 나머지 그룹만 마진·패딩 없는 래퍼(`.ui-sidebar-scroll`)에 담아 그쪽만
|
||||
* 스크롤시킨다. 페이지는 지금처럼 본문 마지막에 액션 줄을 붙이기만 하면 된다.
|
||||
*/
|
||||
function splitSidebarActions(body: HTMLElement): void {
|
||||
export function splitSidebarActions(body: HTMLElement): void {
|
||||
// 이미 쪼갠 body 는 그대로 — 패널을 늦게 붙이는 화면(B05·B06 한 페이지)이 붙인 뒤 다시 부른다.
|
||||
if (body.classList.contains("ui-sidebar-split")) return;
|
||||
// 액션 줄은 보통 페이지 패널 루트(b05-route__panel 등) **안**에 있다 — body 직계가
|
||||
// 아니므로 깊이 무관하게 찾고, 액션 줄의 실제 부모 기준으로 쪼갠다.
|
||||
const actions = body.querySelector<HTMLElement>(".ui-sidebar-actions");
|
||||
|
||||
Reference in New Issue
Block a user