diff --git a/B05_Profile/B05_Profile_UI_Panel_Route.ts b/B05_Profile/B05_Profile_UI_Panel_Route.ts
index 4adc6d72b..a21185ec8 100644
--- a/B05_Profile/B05_Profile_UI_Panel_Route.ts
+++ b/B05_Profile/B05_Profile_UI_Panel_Route.ts
@@ -19,12 +19,9 @@ export interface RoutePartCallbacks {
export function createRoutePart(callbacks: RoutePartCallbacks) {
// 계획노선 편집 — 노선을 고치는 유일한 입구(2026-09-06 사용자 확정). 자동탐색
// (「경로 계산 설정」)은 사용 중단이라 이 자리가 노선을 바꾸는 길이다.
- const plannedRouteBtn = document.createElement("button");
- plannedRouteBtn.type = "button";
- plannedRouteBtn.className = "b05-route__btn";
- plannedRouteBtn.textContent = "계획노선 편집";
+ const plannedRouteBtn = button("계획노선 편집", () => callbacks.onEditPlannedRoute());
+ plannedRouteBtn.classList.add("b05-route__btn");
plannedRouteBtn.title = "예상노선 위에서 계획노선을 고칩니다. [확인] 때만 다시 계산합니다.";
- plannedRouteBtn.addEventListener("click", () => callbacks.onEditPlannedRoute());
const selectedName = document.createElement("strong");
const radius = numberField("회피/금지 반경 (m)", "25");
diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts
index 2788805ea..eb9f18ab7 100644
--- a/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts
+++ b/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts
@@ -1,3 +1,5 @@
+import { createButton } from "@ui/ui_template_elements";
+
/* =============================================================================
* B05_Profile_UI_RouteEdit_Chrome.ts
* 계획노선 편집 모달의 **뼈대** — 창·단추·오버레이 판을 만들고 자주 쓰는 요소를 집어 준다.
@@ -61,7 +63,7 @@ export function createRouteEditChrome(): RouteEditChrome {
-
+
노선을 읽는 중…
@@ -101,6 +102,17 @@ export function createRouteEditChrome(): RouteEditChrome {
`;
document.body.append(overlay);
+ const closeButton = createButton({ label: "✕", variant: "ghost" });
+ closeButton.classList.add("b05-routeedit__close");
+ closeButton.setAttribute("aria-label", "닫기");
+ overlay.querySelector('[data-slot="close"]')!.replaceWith(closeButton);
+
+ const measureCloseButton = createButton({ label: "✕", variant: "ghost" });
+ measureCloseButton.classList.add("b05-routeedit__measure-close");
+ measureCloseButton.setAttribute("aria-label", "닫기");
+ measureCloseButton.title = "닫기";
+ overlay.querySelector('[data-slot="measure-close"]')!.replaceWith(measureCloseButton);
+
const canvas = overlay.querySelector
(".b05-routeedit__canvas")!;
const status = overlay.querySelector(".b05-routeedit__status")!;
const measureBox = overlay.querySelector(".b05-routeedit__measure")!;
diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Label.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Label.ts
index 60269cd99..c0e8fa11c 100644
--- a/B05_Profile/B05_Profile_UI_RouteEdit_Label.ts
+++ b/B05_Profile/B05_Profile_UI_RouteEdit_Label.ts
@@ -20,6 +20,8 @@
* 다른 꺾임점을 고르면 자동 자리로 돌아간다.
* ========================================================================== */
+import { createButton } from "@ui/ui_template_elements";
+import { numberField, toggleButton } from "./B05_Profile_UI_Panel_Fields";
import { CURVE_OPTIONAL_INNER_ANGLE_DEG, type CurveLock } from "./B05_Profile_UI_RouteEdit_Edits";
// `deflectionRad` 는 화면을 안 만지는 셈이라 `_Edits.ts` 로 옮겼다(2026-09-12) — 순수
@@ -113,6 +115,12 @@ function boxReach(dx: number, dy: number, width: number, height: number): number
return Math.min(byX, byY);
}
+function unitLabel(text: string): HTMLSpanElement {
+ const span = document.createElement("span");
+ span.textContent = text;
+ return span;
+}
+
/** 패널을 만든다. 몸통은 `document.body` 에 붙어 모달 밖으로도 넘어간다. */
export function createCurveLabel(handlers: CurveLabelHandlers): CurveLabel {
const root = document.createElement("div");
@@ -121,40 +129,64 @@ export function createCurveLabel(handlers: CurveLabelHandlers): CurveLabel {
root.innerHTML = `
-
-
+
+
-
-
+
+
교각점
-
+
`;
document.body.append(root);
const head = root.querySelector(".b05-routeedit__label-head")!;
const seatText = root.querySelector(".b05-routeedit__curve-label")!;
- const toggle = root.querySelector('[data-act="curve-toggle"]')!;
- const radius = root.querySelector(".b05-routeedit__curve-radius")!;
- const arc = root.querySelector(".b05-routeedit__curve-arc")!;
- const lockRadius = root.querySelector('[data-act="lock-radius"]')!;
- const lockArc = root.querySelector('[data-act="lock-arc"]')!;
- const lockApex = root.querySelector('[data-act="lock-apex"]')!;
const info = root.querySelector(".b05-routeedit__curve-info")!;
const lockName = root.querySelector(".b05-routeedit__lock-name")!;
+
+ const toggle = createButton({ label: "", variant: "ghost", size: "sm" });
+ toggle.dataset.act = "curve-toggle";
+ root.querySelector('[data-slot="curve-toggle"]')!.replaceWith(toggle);
+
+ const closeBtn = createButton({ label: "✕", variant: "ghost", size: "sm" });
+ closeBtn.dataset.act = "curve-close";
+ closeBtn.setAttribute("aria-label", "닫기");
+ closeBtn.title = "닫기";
+ root.querySelector('[data-slot="curve-close"]')!.replaceWith(closeBtn);
+
+ const radiusField = numberField("반지름");
+ radiusField.wrapper.className = "b05-routeedit__curve-field";
+ radiusField.classList.add("b05-routeedit__curve-radius");
+ radiusField.min = "1";
+ radiusField.step = "0.5";
+ const radius = radiusField as HTMLInputElement;
+ const lockRadius = toggleButton("고정", false, (active) =>
+ handlers.onLock(active ? "radius" : null),
+ );
+ lockRadius.dataset.act = "lock-radius";
+ lockRadius.title = "반지름 고정 — 노드를 옮겨도 안 바뀝니다";
+ radiusField.wrapper.append(unitLabel("m"), lockRadius);
+ root.querySelector('[data-slot="radius-field"]')!.replaceWith(radiusField.wrapper);
+
+ const arcField = numberField("곡선 길이");
+ arcField.wrapper.className = "b05-routeedit__curve-field";
+ arcField.classList.add("b05-routeedit__curve-arc");
+ arcField.min = "1";
+ arcField.step = "0.5";
+ const arc = arcField as HTMLInputElement;
+ const lockArc = toggleButton("고정", false, (active) => handlers.onLock(active ? "arc" : null));
+ lockArc.dataset.act = "lock-arc";
+ lockArc.title = "곡선 길이 고정 — 노드를 옮겨도 안 바뀝니다";
+ arcField.wrapper.append(unitLabel("m"), lockArc);
+ root.querySelector('[data-slot="arc-field"]')!.replaceWith(arcField.wrapper);
+
+ const lockApex = toggleButton("고정", false, (active) => handlers.onApexLock(active));
+ lockApex.dataset.act = "lock-apex";
+ root.querySelector('[data-slot="lock-apex"]')!.replaceWith(lockApex);
+
/** 끝점에서는 숨길 곡선 칸 — 곡선 지우기 단추 · 반지름 줄 · 곡선 길이 줄 · 내각 글. */
- const curveOnly = [toggle, radius.closest("label")!, arc.closest("label")!, info];
+ const curveOnly = [toggle, radiusField.wrapper, arcField.wrapper, info];
// 패널 위에서 누른 것이 캔버스로 새어 나가면 노드가 딸려 움직인다.
for (const type of ["pointerdown", "dblclick", "contextmenu", "wheel"] as const) {
@@ -188,13 +220,10 @@ export function createCurveLabel(handlers: CurveLabelHandlers): CurveLabel {
radius.addEventListener("change", () => handlers.onRadius(numberOf(radius, limitRadius)));
arc.addEventListener("change", () => handlers.onArcLength(numberOf(arc, limitArc)));
toggle.addEventListener("click", () => handlers.onCurveOn(!curveOn));
- root
- .querySelector('[data-act="curve-close"]')!
- .addEventListener("click", () => handlers.onClose());
- lockRadius.addEventListener("click", () => handlers.onLock(lock === "radius" ? null : "radius"));
- lockArc.addEventListener("click", () => handlers.onLock(lock === "arc" ? null : "arc"));
+ closeBtn.addEventListener("click", () => handlers.onClose());
+ // 고정 단추 셋은 만들 때(`toggleButton`) 이미 눌림을 배선했다 — 반지름 · 곡선 길이는 서로
+ // 배타적(한쪽을 누르면 다른 쪽은 풀림)이라 `onLock` 쪽에서 그 규칙을 맞춘다.
// 교각점 고정은 **곡선 유무와 무관**하다 — 곡선을 지운 자리도 꺾임점 자리는 못 박을 수 있다.
- lockApex.addEventListener("click", () => handlers.onApexLock(!apexLocked));
// ── 머리를 잡아 옮기기 — 곡선을 가리면 손으로 치울 수 있어야 한다 ──
let dragFrom: { x: number; y: number; left: number; top: number } | null = null;
@@ -297,9 +326,12 @@ export function createCurveLabel(handlers: CurveLabelHandlers): CurveLabel {
arc.disabled = !state.curveOn;
lockRadius.disabled = !state.curveOn;
lockArc.disabled = !state.curveOn;
- lockRadius.classList.toggle("is-on", lock === "radius");
- lockArc.classList.toggle("is-on", lock === "arc");
- lockApex.classList.toggle("is-on", apexLocked);
+ lockRadius.classList.toggle("is-active", lock === "radius");
+ lockRadius.setAttribute("aria-pressed", String(lock === "radius"));
+ lockArc.classList.toggle("is-active", lock === "arc");
+ lockArc.setAttribute("aria-pressed", String(lock === "arc"));
+ lockApex.classList.toggle("is-active", apexLocked);
+ lockApex.setAttribute("aria-pressed", String(apexLocked));
radius.value =
state.radiusShown === null ? "" : String(Math.round(state.radiusShown * 10) / 10);
arc.value =
diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Recommend.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Recommend.ts
index 1fa8d2e44..6d8a4f854 100644
--- a/B05_Profile/B05_Profile_UI_RouteEdit_Recommend.ts
+++ b/B05_Profile/B05_Profile_UI_RouteEdit_Recommend.ts
@@ -15,7 +15,8 @@
* ========================================================================== */
import { openModal } from "@ui/ui_template_modal";
-import { showToast } from "@ui/ui_template_elements";
+import { createButton, createInputField, showToast } from "@ui/ui_template_elements";
+import { toggleButton } from "./B05_Profile_UI_Panel_Fields";
import type {
RouteInitialCriteria,
RouteInitialLogic,
@@ -78,11 +79,6 @@ const WEIGHT_NAME: Record = {
grade: "기울기",
};
-/** 고정 단추 그림 — 자물쇠(글자 색을 따라 밝음 · 어두움에서 보인다). */
-const LOCK_ICON = ``;
-
/** 공통 제약 칸 — 표시 이름 · 단위 · 서버가 준 값(`InitialCriteria`)에서 그 칸을 꺼내는 법. */
const CRITERIA_FIELD: Record<
CriteriaKey,
@@ -181,43 +177,20 @@ export function openRecommendModal(params: RecommendParams): void {
-
${RECOMMEND_MAP_HTML}
-
- ${CRITERIA_KEYS.map(
- (key) => ``,
- ).join("")}
-
+
+
%
@@ -248,6 +221,74 @@ export function openRecommendModal(params: RecommendParams): void {
${recommendHelpHtml()}`;
+ // 알약 · 비중 막대 · 제약 칸 · 목표 기울기 칸은 공용 부품(createButton · createInputField ·
+ // PF toggleButton)으로 짓는다(PLAN 117) — 글 틀은 위에 자리만 남기고 여기서 채운다.
+ const pillsBox = body.querySelector
('[data-slot="pills"]')!;
+ for (const key of PILLS) {
+ const pill = createButton({ label: LOGIC_LABEL[key], variant: "pill" });
+ pill.classList.add("b05-routerec__pill");
+ pill.setAttribute("role", "radio");
+ pill.dataset.logic = key;
+ pill.title = PILL_TITLE[key];
+ pillsBox.append(pill);
+ }
+
+ const barsBox = body.querySelector('[data-slot="bars"]')!;
+ for (const key of WEIGHT_KEYS) {
+ const bar = document.createElement("div");
+ bar.className = "b05-routerec__bar";
+ const numField = createInputField({ type: "number", min: 0, max: 100, compact: true });
+ numField.input.step = "1";
+ numField.input.inputMode = "numeric";
+ numField.input.classList.add("b05-routerec__num");
+ numField.input.dataset.weightNum = key;
+ numField.input.setAttribute("aria-label", `${WEIGHT_NAME[key]} 비중 값`);
+ const slider = document.createElement("input");
+ slider.type = "range";
+ slider.min = "0";
+ slider.max = "100";
+ slider.step = "1";
+ slider.dataset.weight = key;
+ slider.setAttribute("aria-label", `${WEIGHT_NAME[key]} 비중`);
+ const lock = toggleButton("고정", false, (active) => {
+ locks[key] = active;
+ invalid.clear();
+ render();
+ });
+ lock.classList.add("b05-routerec__lock");
+ lock.dataset.weightLock = key;
+ lock.setAttribute("aria-label", `${WEIGHT_NAME[key]} 비중 고정`);
+ const name = document.createElement("span");
+ name.textContent = WEIGHT_NAME[key];
+ bar.append(numField.input, slider, lock, name);
+ barsBox.append(bar);
+ }
+
+ const criteriaBox = body.querySelector('[data-slot="criteria"]')!;
+ for (const key of CRITERIA_KEYS) {
+ const field = CRITERIA_FIELD[key];
+ const row = document.createElement("label");
+ row.title = field.title;
+ const name = document.createElement("span");
+ name.textContent = field.label;
+ const input = createInputField({ type: "number", min: 0, compact: true });
+ input.input.inputMode = "decimal";
+ input.input.step = "0.1";
+ input.input.dataset.criteria = key;
+ input.input.placeholder = "기본값";
+ const unit = document.createElement("span");
+ unit.textContent = field.unit;
+ row.append(name, input.input, unit);
+ criteriaBox.append(row);
+ }
+
+ const targetField = createInputField({ type: "number", min: 0, max: 100, compact: true });
+ targetField.input.inputMode = "decimal";
+ targetField.input.step = "0.1";
+ targetField.input.dataset.criteria = TARGET_GRADE_KEY;
+ targetField.input.placeholder = "상한";
+ body.querySelector('[data-slot="target-input"]')!.replaceWith(targetField.input);
+
const q = (sel: string): T => body.querySelector(sel)!;
// [적용] · [닫기]를 제목줄 오른쪽 위로 — 공용 겉틀의 닫기 단추와 갈아 끼운다(PLAN 77-3).
const actions = q(".b05-routerec__actions");
@@ -297,7 +338,7 @@ export function openRecommendModal(params: RecommendParams): void {
pills.forEach((pill) => {
const on = pill.dataset.logic === logic;
pill.setAttribute("aria-checked", String(on));
- pill.classList.toggle("is-on", on);
+ pill.classList.toggle("is-active", on);
pill.disabled = busy;
});
weightsBox.hidden = logic !== "weighted";
@@ -316,7 +357,7 @@ export function openRecommendModal(params: RecommendParams): void {
num.classList.toggle("is-invalid", invalid.has(key));
num.setAttribute("aria-invalid", String(invalid.has(key)));
lock.setAttribute("aria-pressed", String(shut));
- lock.classList.toggle("is-on", shut);
+ lock.classList.toggle("is-active", shut);
lock.classList.toggle("is-auto", derived === key);
// 셋 다 고정은 막는다 — 마지막 하나는 고정 단추가 꺼지고 잠김으로만 보인다.
lock.disabled = busy || !canLockWeight(locks, key) || derived === key;
@@ -488,16 +529,8 @@ export function openRecommendModal(params: RecommendParams): void {
if (!invalid.has(input.dataset.weightNum as WeightKey)) render();
});
});
- // 고정 단추 — 지금 값을 붙든다. 셋 다 고정은 막혀 있다(단추가 꺼짐).
- body.querySelectorAll("[data-weight-lock]").forEach((button) => {
- button.addEventListener("click", () => {
- const key = button.dataset.weightLock as WeightKey;
- if (!canLockWeight(locks, key)) return;
- locks[key] = !locks[key];
- invalid.clear();
- render();
- });
- });
+ // 고정 단추 — 눌림은 만들 때(`toggleButton`) 이미 배선했다. 셋 다 고정은 `render()` 가
+ // disabled 로 막아 클릭 자체가 안 먹는다.
criteriaInputs.forEach((input) => {
input.addEventListener("input", () => {
dirty = preview !== null;
diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Recommend_Profile.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Recommend_Profile.ts
index 218ae6ddf..63ac6d57b 100644
--- a/B05_Profile/B05_Profile_UI_RouteEdit_Recommend_Profile.ts
+++ b/B05_Profile/B05_Profile_UI_RouteEdit_Recommend_Profile.ts
@@ -8,6 +8,8 @@
* (계획고 = 지반고)으로 다시 세운다. 계산 자리 = 서버 단독 — 여기는 받은 값을 그리기만 한다.
* ========================================================================== */
+import { renderDataTableHtml, type DataColumn } from "@ui/ui_template_data_table";
+
/** 후보 하나의 균형 종단 — 서버 `metrics.profile`. */
export interface BalanceProfile {
stationsM: number[];
@@ -118,17 +120,25 @@ export function profileSvg(
`;
}
+const CANDIDATE_COLUMNS: DataColumn[] = [
+ { key: "name", label: "후보" },
+ { key: "cut", label: "절토" },
+ { key: "fill", label: "성토" },
+ { key: "mass", label: "누가토량" },
+ { key: "grade", label: "최대 종단경사" },
+];
+
/** 이 모달에서 셈한 후보들 — 갈래 이름 · 절 · 성 · 누가토량(±) · 최대 종단경사 표. 비면 빈 글. */
export function candidatesTable(rows: Array<[string, BalanceProfile]>): string {
if (!rows.length) return "";
- const body = rows
- .map(
- ([name, b]) =>
- `| ${name} | ${m3(b.cutM3)} | ${m3(b.fillM3)} | ` +
- `${signedM3(b.massEndM3)} | ${pct(b.maxGradePct)} |
`,
- )
- .join("");
- return `| 후보 | 절토 |
- 성토 | 누가토량 | 최대 종단경사 |
- ${body}
`;
+ return renderDataTableHtml({
+ columns: CANDIDATE_COLUMNS,
+ rows: rows.map(([name, b]) => ({
+ name,
+ cut: m3(b.cutM3),
+ fill: m3(b.fillM3),
+ mass: signedM3(b.massEndM3),
+ grade: pct(b.maxGradePct),
+ })),
+ });
}
diff --git a/B05_Profile/B05_Profile_UI_Style_RouteEdit.css b/B05_Profile/B05_Profile_UI_Style_RouteEdit.css
index e142c56a4..45cb046a5 100644
--- a/B05_Profile/B05_Profile_UI_Style_RouteEdit.css
+++ b/B05_Profile/B05_Profile_UI_Style_RouteEdit.css
@@ -49,15 +49,6 @@
gap: var(--spacing-8);
}
-.b05-routeedit__close {
- flex: none;
- border: none;
- background: none;
- color: var(--color-text-secondary);
- font-size: 16px;
- cursor: pointer;
-}
-
.b05-routeedit__canvas-wrap {
position: relative;
flex: 1 1 auto;
@@ -284,14 +275,6 @@
line-height: 1.5;
}
-.b05-routeedit__measure-close {
- flex: none;
- border: none;
- background: none;
- color: var(--color-text-secondary);
- cursor: pointer;
-}
-
/* 재계산 중에는 화면 전체를 덮는다 — 결과를 기다릴 수밖에 없는 조작(CLAUDE.md 5장). */
.b05-routeedit__busy {
position: absolute;
@@ -344,57 +327,11 @@
touch-action: none;
}
-.b05-routeedit__label-close {
- border: none;
- background: none;
- color: var(--color-text-secondary);
- font-size: 13px;
- line-height: 1;
- cursor: pointer;
-}
-
-/* 고정 단추 — 켜지면 색이 찬다. 켠 값은 노드를 옮겨도 안 바뀐다. */
-.b05-routeedit__lock {
- padding: 1px 6px;
- border: 1px solid var(--color-border);
- border-radius: var(--radius-4, 4px);
- background: var(--color-surface);
- color: var(--color-text-secondary);
- font: inherit;
- cursor: pointer;
-}
-
-.b05-routeedit__lock.is-on {
- border-color: transparent;
- background: var(--color-primary, #7c3aed);
- color: #fff;
-}
-
-.b05-routeedit__lock:disabled {
- color: var(--color-text-secondary);
- cursor: default;
-}
-
/* 「칸을 비우면 자동」 — 상태 설명과 줄을 나눈다(2026-09-07 사용자 지시 4). */
.b05-routeedit__curve-note {
color: var(--color-text-secondary);
}
-.b05-routeedit__label-toggle {
- padding: 1px 6px;
- border: 1px solid var(--color-border);
- border-radius: var(--radius-4, 4px);
- background: var(--color-surface);
- color: var(--color-text-body);
- font: inherit;
- cursor: pointer;
-}
-
-.b05-routeedit__label-toggle:disabled {
- color: var(--color-text-secondary);
- cursor: default;
-}
-
.b05-routeedit__curve-label {
font-weight: 600;
white-space: nowrap;
diff --git a/B05_Profile/B05_Profile_UI_Style_RouteRecommend.css b/B05_Profile/B05_Profile_UI_Style_RouteRecommend.css
index 506d81ce4..5eef6bf43 100644
--- a/B05_Profile/B05_Profile_UI_Style_RouteRecommend.css
+++ b/B05_Profile/B05_Profile_UI_Style_RouteRecommend.css
@@ -188,27 +188,13 @@
}
.b05-routerec__pill {
- padding: 8px var(--spacing-8);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
- border: 1px solid var(--color-border);
- border-radius: 999px;
- background: var(--color-surface);
- color: var(--color-text-body);
- font-size: var(--text-body);
- cursor: pointer;
}
-.b05-routerec__pill:hover:not(:disabled) {
- border-color: var(--color-primary);
-}
-
-.b05-routerec__pill.is-on {
- border-color: var(--color-primary);
- background: var(--color-primary);
- color: var(--color-on-primary, #fff);
- font-weight: 600;
+.b05-routerec__pill:disabled {
+ cursor: progress;
}
/* 제목줄 오른쪽 위 [적용] · [닫기](공용 겉틀 닫기 단추 자리 · PLAN 77-3). */
@@ -217,11 +203,6 @@
gap: var(--spacing-8);
}
-.b05-routerec__pill:disabled {
- cursor: progress;
- opacity: 0.7;
-}
-
.b05-routerec__main {
display: grid;
grid-template-columns: minmax(0, 1fr);
@@ -371,36 +352,7 @@
box-shadow: 0 0 0 1px var(--color-danger, #dc2626);
}
-/* 고정 단추 — 자물쇠. 눌린 것 = 고정 · 저절로 정해진 비중은 눌린 채 꺼져(잠김 표시). */
-.b05-routerec__lock {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 28px;
- height: 26px;
- padding: 0;
- border: 1px solid var(--color-border);
- border-radius: var(--radius-4, 4px);
- background: var(--color-surface);
- color: var(--color-text-secondary);
- cursor: pointer;
-}
-
-.b05-routerec__lock:hover:not(:disabled) {
- border-color: var(--color-primary);
-}
-
-.b05-routerec__lock.is-on {
- border-color: var(--color-primary);
- background: var(--color-primary);
- color: var(--color-on-primary, #fff);
-}
-
-.b05-routerec__lock:disabled {
- cursor: not-allowed;
- opacity: 0.55;
-}
-
+/* 저절로 정해진 비중은 고정 단추가 눌린 채 꺼져(잠김 표시) — 색은 공용 꼴(`ui-btn--glass`)을 쓴다. */
.b05-routerec__lock.is-auto {
opacity: 0.8;
border-style: dashed;
diff --git a/resources/tester/helper_b05_routeedit_initial.cjs b/resources/tester/helper_b05_routeedit_initial.cjs
index a9762095c..1e673fdc1 100644
--- a/resources/tester/helper_b05_routeedit_initial.cjs
+++ b/resources/tester/helper_b05_routeedit_initial.cjs
@@ -40,6 +40,8 @@ function loadTs(file) {
cache.set(full, box.exports);
const localRequire = (request) => {
if (STUBS[request]) return STUBS[request];
+ if (request.endsWith(".css")) return {};
+ if (request.startsWith("@ui/")) return loadTs(path.join(ROOT, "ui_template", `${request.slice(4)}.ts`));
if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`));
return require(request);
};
diff --git a/resources/tester/test_63_4_route_profile.py b/resources/tester/test_63_4_route_profile.py
index d9bf18b4c..3f77662cf 100644
--- a/resources/tester/test_63_4_route_profile.py
+++ b/resources/tester/test_63_4_route_profile.py
@@ -120,5 +120,8 @@ def test_균형_종단은_캐시에_안_남는다() -> None:
assert "덮개 모자란 관 1/2" in result["line"]
assert "누가토량 -40㎥(허용 ±500㎥)" in result["line"]
assert result["svg"].count("토공 최소' in result["table"]
- assert '누가토량 | ' in result["table"] and "반입" not in result["table"]
+ assert "토공 최소 | " in result["table"]
+ assert (
+ '누가토량 | ' in result["table"]
+ and "반입" not in result["table"]
+ )