Merge remote-tracking branch 'origin/main_desktop_1' into main_laptop_1
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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 {
|
||||
<button type="button" class="ui-btn ui-btn--filled" data-act="apply"
|
||||
title="이 노선으로 배수유역부터 다시 계산합니다 — 결과가 새 초기값이 되고 종횡단 · 구조물 · 관 옵션 편집은 새 노선 기본값으로 바뀝니다">확인</button>
|
||||
</span>
|
||||
<button type="button" class="b05-routeedit__close" aria-label="닫기">✕</button>
|
||||
<span data-slot="close"></span>
|
||||
</div>
|
||||
<div class="b05-routeedit__canvas-wrap">
|
||||
<canvas class="b05-routeedit__canvas"></canvas>
|
||||
@@ -79,8 +81,7 @@ export function createRouteEditChrome(): RouteEditChrome {
|
||||
</div>
|
||||
<div class="b05-routeedit__measure" hidden>
|
||||
<span class="b05-routeedit__measure-text"></span>
|
||||
<button type="button" class="b05-routeedit__measure-close" aria-label="닫기"
|
||||
title="닫기">✕</button>
|
||||
<span data-slot="measure-close"></span>
|
||||
</div>
|
||||
<div class="b05-routeedit__info">
|
||||
<span class="b05-routeedit__status">노선을 읽는 중…</span>
|
||||
@@ -101,6 +102,17 @@ export function createRouteEditChrome(): RouteEditChrome {
|
||||
<div class="b05-routeedit__side"></div>`;
|
||||
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<HTMLCanvasElement>(".b05-routeedit__canvas")!;
|
||||
const status = overlay.querySelector<HTMLElement>(".b05-routeedit__status")!;
|
||||
const measureBox = overlay.querySelector<HTMLElement>(".b05-routeedit__measure")!;
|
||||
|
||||
@@ -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 = `
|
||||
<div class="b05-routeedit__label-head">
|
||||
<span class="b05-routeedit__curve-label"></span>
|
||||
<button type="button" class="b05-routeedit__label-toggle" data-act="curve-toggle"></button>
|
||||
<button type="button" class="b05-routeedit__label-close" data-act="curve-close"
|
||||
aria-label="닫기" title="닫기">✕</button>
|
||||
<span data-slot="curve-toggle"></span>
|
||||
<span data-slot="curve-close"></span>
|
||||
</div>
|
||||
<label class="b05-routeedit__curve-field">반지름
|
||||
<input type="number" class="b05-routeedit__curve-radius" min="1" step="0.5" />
|
||||
<span>m</span>
|
||||
<button type="button" class="b05-routeedit__lock" data-act="lock-radius"
|
||||
title="반지름 고정 — 노드를 옮겨도 안 바뀝니다">고정</button>
|
||||
</label>
|
||||
<label class="b05-routeedit__curve-field">곡선 길이
|
||||
<input type="number" class="b05-routeedit__curve-arc" min="1" step="0.5" />
|
||||
<span>m</span>
|
||||
<button type="button" class="b05-routeedit__lock" data-act="lock-arc"
|
||||
title="곡선 길이 고정 — 노드를 옮겨도 안 바뀝니다">고정</button>
|
||||
</label>
|
||||
<span data-slot="radius-field"></span>
|
||||
<span data-slot="arc-field"></span>
|
||||
<div class="b05-routeedit__curve-field"><span class="b05-routeedit__lock-name">교각점</span>
|
||||
<button type="button" class="b05-routeedit__lock" data-act="lock-apex">고정</button>
|
||||
<span data-slot="lock-apex"></span>
|
||||
</div>
|
||||
<span class="b05-routeedit__curve-info"></span>`;
|
||||
document.body.append(root);
|
||||
|
||||
const head = root.querySelector<HTMLElement>(".b05-routeedit__label-head")!;
|
||||
const seatText = root.querySelector<HTMLElement>(".b05-routeedit__curve-label")!;
|
||||
const toggle = root.querySelector<HTMLButtonElement>('[data-act="curve-toggle"]')!;
|
||||
const radius = root.querySelector<HTMLInputElement>(".b05-routeedit__curve-radius")!;
|
||||
const arc = root.querySelector<HTMLInputElement>(".b05-routeedit__curve-arc")!;
|
||||
const lockRadius = root.querySelector<HTMLButtonElement>('[data-act="lock-radius"]')!;
|
||||
const lockArc = root.querySelector<HTMLButtonElement>('[data-act="lock-arc"]')!;
|
||||
const lockApex = root.querySelector<HTMLButtonElement>('[data-act="lock-apex"]')!;
|
||||
const info = root.querySelector<HTMLElement>(".b05-routeedit__curve-info")!;
|
||||
const lockName = root.querySelector<HTMLElement>(".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 =
|
||||
|
||||
@@ -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<WeightKey, string> = {
|
||||
grade: "기울기",
|
||||
};
|
||||
|
||||
/** 고정 단추 그림 — 자물쇠(글자 색을 따라 밝음 · 어두움에서 보인다). */
|
||||
const LOCK_ICON = `<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true" fill="none"
|
||||
stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3.5" y="7" width="9" height="6.5" rx="1.2" /><path d="M5.5 7V5a2.5 2.5 0 0 1 5 0v2" /></svg>`;
|
||||
|
||||
/** 공통 제약 칸 — 표시 이름 · 단위 · 서버가 준 값(`InitialCriteria`)에서 그 칸을 꺼내는 법. */
|
||||
const CRITERIA_FIELD: Record<
|
||||
CriteriaKey,
|
||||
@@ -181,43 +177,20 @@ export function openRecommendModal(params: RecommendParams): void {
|
||||
<button type="button" class="ui-btn ui-btn--filled" data-act="apply" disabled>적용</button>
|
||||
<button type="button" class="ui-btn ui-btn--ghost" data-act="close">닫기</button>
|
||||
</div>
|
||||
<div class="b05-routerec__pills" role="radiogroup" aria-label="추천 갈래">
|
||||
${PILLS.map(
|
||||
(key) => `<button type="button" class="b05-routerec__pill" role="radio"
|
||||
data-logic="${key}" title="${PILL_TITLE[key]}">${LOGIC_LABEL[key]}</button>`,
|
||||
).join("")}
|
||||
</div>
|
||||
<div class="b05-routerec__pills" role="radiogroup" aria-label="추천 갈래" data-slot="pills"></div>
|
||||
<div class="b05-routerec__main">
|
||||
<div class="b05-routerec__mapbox">
|
||||
<div class="b05-routerec__mapwrap" role="img" aria-label="추천 노선 미리 보기">
|
||||
${RECOMMEND_MAP_HTML}
|
||||
<div class="b05-routerec__side" role="group" aria-label="추천 옵션">
|
||||
<div class="b05-routerec__weights" hidden>
|
||||
<div class="b05-routerec__bars">
|
||||
${WEIGHT_KEYS.map(
|
||||
(key) => `<div class="b05-routerec__bar">
|
||||
<input type="number" class="b05-routerec__num" min="0" max="100" step="1"
|
||||
inputmode="numeric" data-weight-num="${key}" aria-label="${WEIGHT_NAME[key]} 비중 값" />
|
||||
<input type="range" min="0" max="100" step="1" data-weight="${key}"
|
||||
aria-label="${WEIGHT_NAME[key]} 비중" />
|
||||
<button type="button" class="b05-routerec__lock" data-weight-lock="${key}"
|
||||
aria-pressed="false" aria-label="${WEIGHT_NAME[key]} 비중 고정">${LOCK_ICON}</button>
|
||||
<span>${WEIGHT_NAME[key]}</span></div>`,
|
||||
).join("")}
|
||||
</div>
|
||||
<div class="b05-routerec__bars" data-slot="bars"></div>
|
||||
<span class="b05-routerec__sum" data-weight-sum></span>
|
||||
</div>
|
||||
<div class="b05-routerec__criteria">
|
||||
${CRITERIA_KEYS.map(
|
||||
(key) => `<label title="${CRITERIA_FIELD[key].title}">${CRITERIA_FIELD[key].label}
|
||||
<input type="number" inputmode="decimal" step="0.1" min="0"
|
||||
data-criteria="${key}" placeholder="기본값" />${CRITERIA_FIELD[key].unit}</label>`,
|
||||
).join("")}
|
||||
</div>
|
||||
<div class="b05-routerec__criteria" data-slot="criteria"></div>
|
||||
<label class="b05-routerec__target" data-target-row hidden
|
||||
title="영선 노선이 따라갈 종단기울기(%) — 비우면 위 종단기울기 상한">목표 기울기
|
||||
<input type="number" inputmode="decimal" step="0.1" min="0" max="100"
|
||||
data-criteria="${TARGET_GRADE_KEY}" placeholder="상한" />%</label>
|
||||
<span data-slot="target-input"></span>%</label>
|
||||
<button type="button" class="ui-btn ui-btn--glass" data-act="rerun" hidden>다시 셈</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -248,6 +221,74 @@ export function openRecommendModal(params: RecommendParams): void {
|
||||
</div>
|
||||
${recommendHelpHtml()}`;
|
||||
|
||||
// 알약 · 비중 막대 · 제약 칸 · 목표 기울기 칸은 공용 부품(createButton · createInputField ·
|
||||
// PF toggleButton)으로 짓는다(PLAN 117) — 글 틀은 위에 자리만 남기고 여기서 채운다.
|
||||
const pillsBox = body.querySelector<HTMLElement>('[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<HTMLElement>('[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<HTMLElement>('[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 = <T extends HTMLElement>(sel: string): T => body.querySelector<T>(sel)!;
|
||||
// [적용] · [닫기]를 제목줄 오른쪽 위로 — 공용 겉틀의 닫기 단추와 갈아 끼운다(PLAN 77-3).
|
||||
const actions = q<HTMLElement>(".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<HTMLButtonElement>("[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;
|
||||
|
||||
@@ -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(
|
||||
<path class="is-plan" d="${planM.length ? path(planM) : ""}" vector-effect="non-scaling-stroke" /></svg>`;
|
||||
}
|
||||
|
||||
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]) =>
|
||||
`<tr><th scope="row">${name}</th><td>${m3(b.cutM3)}</td><td>${m3(b.fillM3)}</td>` +
|
||||
`<td>${signedM3(b.massEndM3)}</td><td>${pct(b.maxGradePct)}</td></tr>`,
|
||||
)
|
||||
.join("");
|
||||
return `<table><thead><tr><th scope="col">후보</th><th scope="col">절토</th>
|
||||
<th scope="col">성토</th><th scope="col">누가토량</th><th scope="col">최대 종단경사</th></tr></thead>
|
||||
<tbody>${body}</tbody></table>`;
|
||||
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),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -120,5 +120,8 @@ def test_균형_종단은_캐시에_안_남는다() -> None:
|
||||
assert "덮개 모자란 관 1/2" in result["line"]
|
||||
assert "누가토량 -40㎥(허용 ±500㎥)" in result["line"]
|
||||
assert result["svg"].count("<path") == 2
|
||||
assert '<th scope="row">토공 최소</th>' in result["table"]
|
||||
assert '<th scope="col">누가토량</th>' in result["table"] and "반입" not in result["table"]
|
||||
assert "<td>토공 최소</td>" in result["table"]
|
||||
assert (
|
||||
'<th scope="col" rowspan="1" title="누가토량">누가토량</th>' in result["table"]
|
||||
and "반입" not in result["table"]
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user