Merge remote-tracking branch 'origin/sub_laptop_2' into main_laptop_1

This commit is contained in:
2026-10-26 23:39:49 +09:00
20 changed files with 162 additions and 266 deletions
+6
View File
@@ -49,6 +49,12 @@ import {
peekSectionDetail,
} from "../../B06_Section/B06_Section_Section_Store";
import "./design_page.css";
// 클래스로 붙이는 부품(체크 · 빈 상태 · 범례 · 보조 글 · 표)의 CSS — B05 · B06 화면 파일이 시험 때문에 import 없이 클래스만 씀(553-2)
import "@ui/ui_template_checkbox";
import "@ui/ui_template_empty";
import "@ui/ui_template_flow_legend";
import "@ui/ui_template_table";
import "@ui/ui_template_text";
export type { DesignPageShared } from "./design_shared";
@@ -249,7 +249,7 @@ export function createIrregularStationsSection(
const list = document.createElement("ul");
list.className = "b05-route__irregular-list";
const help = document.createElement("p");
help.className = "b05-route__note";
help.className = "ui-text ui-text--muted b05-route__note"; // 부품 보조 글(553-2)
help.textContent =
"구조물을 설치할 위치를 측점번호+잔여거리로 추가합니다. 목록에서 고르면 수정·삭제할 수 있습니다.";
@@ -307,9 +307,13 @@ export function createIrregularStationsSection(
function renderList(): void {
list.replaceChildren();
if (!stations.length) {
// 부품 빈 상태 작은 꼴(553-2)
const empty = document.createElement("li");
empty.className = "b05-route__irregular-empty";
empty.textContent = "추가된 비정규 측점이 없습니다.";
empty.className = "ui-empty ui-empty--small b05-route__irregular-empty";
const text = document.createElement("p");
text.className = "ui-empty__text";
text.textContent = "추가된 비정규 측점이 없습니다.";
empty.append(text);
list.append(empty);
return;
}
+7 -2
View File
@@ -74,12 +74,17 @@ export function numberField(label: string, value = ""): WrappedInput {
}
export function checkbox(label: string, checked: boolean): WrappedInput {
// 부품 「체크」 클래스(553-2) — 시험이 별칭 없이 이 파일을 돌려 클래스로 붙임
const wrapper = document.createElement("label");
wrapper.className = "b05-route__check";
wrapper.className = "ui-checkbox b05-route__check";
const input = document.createElement("input");
input.type = "checkbox";
input.className = "ui-checkbox__input";
input.checked = checked;
wrapper.append(input, document.createTextNode(label));
const caption = document.createElement("span");
caption.className = "ui-checkbox__text";
caption.textContent = label;
wrapper.append(input, caption);
return Object.assign(input, { wrapper });
}
+1 -1
View File
@@ -133,7 +133,7 @@ export function createSettingsPart(callbacks: SettingsPartCallbacks) {
});
});
const criteriaNote = document.createElement("p");
criteriaNote.className = "b05-route__note";
criteriaNote.className = "ui-text ui-text--muted b05-route__note"; // 부품 보조 글(553-2)
const gradeAdvanced = document.createElement("details");
const gradeSummary = document.createElement("summary");
gradeSummary.textContent = "기준값 직접 지정";
@@ -10,7 +10,7 @@
width: min(380px, calc(100% - 24px));
padding: var(--spacing-12, 12px);
border: 1px solid var(--color-border);
border-radius: var(--radius-8, 6px);
border-radius: var(--radius-cards);
background: var(--color-surface-raised);
color: var(--color-text-body);
box-shadow: 0 4px 16px color-mix(in srgb, var(--color-text-body) 18%, transparent);
@@ -39,7 +39,7 @@
height: 200px;
overflow: hidden;
border: 1px solid var(--color-border);
border-radius: var(--radius-8, 6px);
border-radius: var(--radius-inputs);
background: var(--color-surface);
}
@@ -293,9 +293,13 @@ export function createRouteMassHaulPanel(
bar.replaceChildren();
scroll.replaceChildren();
legendLayer.replaceChildren();
const empty = document.createElement("span");
empty.className = "b05-profile__masshaul-empty";
empty.textContent = message;
// 부품 빈 상태 작은 꼴(553-2)
const empty = document.createElement("div");
empty.className = "ui-empty ui-empty--small b05-profile__masshaul-empty";
const text = document.createElement("p");
text.className = "ui-empty__text";
text.textContent = message;
empty.append(text);
bar.append(empty);
}
@@ -135,9 +135,11 @@ export function createRouteEditChrome(
</div>
<div class="b05-routeedit__info">
<span class="b05-routeedit__status">노선을 읽는 중…</span>
<span class="b05-routeedit__legend">
<span><i class="is-expected"></i> 예상노선(원본)</span>
<span><i class="is-planned"></i> 계획노선</span>
<span class="ui-legend ui-legend--vertical b05-routeedit__legend">
<span class="ui-legend__item"><i class="ui-legend__mark ui-legend__mark--dash is-expected"
style="--legend-color: var(--color-text-secondary)"></i>예상노선(원본)</span>
<span class="ui-legend__item"><i class="ui-legend__mark ui-legend__mark--line is-planned"
style="--legend-color: var(--map-route)"></i>계획노선</span>
</span>
<span class="b05-routeedit__summary"></span>
<button type="button" data-size="18" class="ui-icon-btn ui-icon-btn--ghost b05-routeedit__info-toggle" data-act="info-toggle"
@@ -229,7 +229,7 @@ export function openRecommendModal(params: RecommendParams): void {
</div>
<label class="b05-routerec__terrain" data-terrain-row hidden
title="추천 · 초기 종단을 셀 지형 — [적용]하면 이 지형이 설계 지형(평면 · 종단 · 횡단)">지형
<select class="ui-select ui-select--sm" data-act="terrain">
<select class="ui-select ui-select--sm" data-size="32" data-act="terrain">
<option value="las">라이다</option>
<option value="sheet">수치지형도</option>
</select></label>
@@ -240,10 +240,13 @@ export function openRecommendModal(params: RecommendParams): void {
<div class="b05-routerec__row"><button type="button" class="ui-btn ui-btn--glass" data-act="rerun">재계산</button></div>
</div>
</div>
<div class="b05-routerec__legend">
<span><i class="is-expected"></i>예상노선(원본)</span>
<span><i class="is-current"></i>창을 연 노선</span>
<span><i class="is-preview"></i>미리 보기</span>
<div class="ui-legend b05-routerec__legend">
<span class="ui-legend__item"><i class="ui-legend__mark ui-legend__mark--dash is-expected"
style="--legend-color: var(--color-text-secondary)"></i>예상노선(원본)</span>
<span class="ui-legend__item"><i class="ui-legend__mark ui-legend__mark--line is-current"
style="--legend-color: var(--map-route)"></i>창을 연 노선</span>
<span class="ui-legend__item"><i class="ui-legend__mark ui-legend__mark--line is-preview"
style="--legend-color: var(--color-primary)"></i>미리 보기</span>
</div>
<div class="b05-routerec__maphint">휠 = 확대 · 축소 &nbsp; 끌기 = 이동 &nbsp; ↺ ↻ = 회전</div>
</div>
@@ -287,7 +290,7 @@ export function openRecommendModal(params: RecommendParams): void {
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 });
const numField = createInputField({ type: "number", min: 0, max: 100, size: 32 }); // 부품 입력 단 32(553-2)
numField.input.step = "1";
numField.input.inputMode = "numeric";
numField.input.classList.add("b05-routerec__num");
@@ -321,7 +324,7 @@ export function openRecommendModal(params: RecommendParams): void {
row.title = field.title;
const name = document.createElement("span");
name.textContent = field.label;
const input = createInputField({ type: "number", min: 0, compact: true });
const input = createInputField({ type: "number", min: 0, size: 32 }); // 부품 입력 단 32(553-2)
input.input.inputMode = "decimal";
input.input.step = "0.1";
input.input.dataset.criteria = key;
@@ -332,7 +335,7 @@ export function openRecommendModal(params: RecommendParams): void {
criteriaBox.append(row);
}
const targetField = createInputField({ type: "number", min: 0, max: 100, compact: true });
const targetField = createInputField({ type: "number", min: 0, max: 100, size: 32 }); // 부품 입력 단 32(553-2)
targetField.input.inputMode = "decimal";
targetField.input.step = "0.1";
targetField.input.dataset.criteria = TARGET_GRADE_KEY;
@@ -10,8 +10,6 @@
* (`initial_profile` · 438 직선화 · 종단곡선 포함)을 펴서 같은 그림에 선 하나 · 한 줄로 더한다.
* ========================================================================== */
import { renderDataTableHtml, type DataColumn } from "@ui/ui_template_data_table";
/** 후보 하나의 균형 종단 — 서버 `metrics.profile`. */
export interface BalanceProfile {
stationsM: number[];
@@ -183,35 +181,37 @@ export function profileSvg(
<path class="is-plan" d="${planM.length ? path(stationsM, planM) : ""}" vector-effect="non-scaling-stroke" />${initialPath}</svg>`;
}
/** 그림 아래 범례 — 그린 선만. */
/** 그림 아래 범례 — 그린 선만 · 부품 「범례」(553-2) · 색 = 그린 선 값. */
export function profileLegend(hasPlan: boolean, hasInitial: boolean): string {
const item = (cls: string, name: string): string => `<span><i class="${cls}"></i>${name}</span>`;
return `<div class="b05-routerec__plegend">${[
item("is-ground", "지반선"),
hasPlan ? item("is-plan", "균형 계획선") : "",
hasInitial ? item("is-initial", "적용될 초기 종단") : "",
const item = (cls: string, kind: string, color: string, name: string): string =>
`<span class="ui-legend__item"><i class="ui-legend__mark ui-legend__mark--${kind} ${cls}" style="--legend-color: ${color}"></i>${name}</span>`;
return `<div class="ui-legend b05-routerec__plegend">${[
item("is-ground", "line", "var(--draw-text-secondary)", "지반선"),
hasPlan ? item("is-plan", "line", "var(--draw-primary)", "균형 계획선") : "",
hasInitial ? item("is-initial", "dash", "var(--map-route)", "적용될 초기 종단") : "",
].join("")}</div>`;
}
const CANDIDATE_COLUMNS: DataColumn[] = [
{ key: "name", label: "후보" },
{ key: "cut", label: "절토" },
{ key: "fill", label: "성토" },
{ key: "mass", label: "누가토량" },
{ key: "grade", label: "최대 종단경사" },
];
const CANDIDATE_COLUMNS = ["후보", "절토", "성토", "누가토량", "최대 종단경사"];
/** 이 모달에서 셈한 후보들 — 갈래 이름 · 절 · 성 · 누가토량(±) · 최대 종단경사 표. 비면 빈 글. */
const escapeHtml = (text: string): string =>
text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
/** 이 모달에서 셈한 후보들 — 갈래 이름 · 절 · 성 · 누가토량(±) · 최대 종단경사 표. 비면 빈 글.
* 꼴 = 부품 「표」 데이터표(`ui-table--data` · 553-2) — 이름 칸 왼쪽 · 수 칸 오른쪽. */
export function candidatesTable(rows: Array<[string, BalanceProfile]>): string {
if (!rows.length) return "";
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),
})),
const end = (i: number): string => (i ? ' class="is-end"' : "");
const head = CANDIDATE_COLUMNS.map((label, i) => `<th scope="col"${end(i)}>${label}</th>`);
const body = rows.map(([name, b]) => {
const cells = [
escapeHtml(name),
m3(b.cutM3),
m3(b.fillM3),
signedM3(b.massEndM3),
pct(b.maxGradePct),
];
return `<tr>${cells.map((cell, i) => `<td${end(i)}>${cell}</td>`).join("")}</tr>`;
});
return `<div class="ui-table-wrap"><table class="ui-table ui-table--data"><thead><tr>${head.join("")}</tr></thead><tbody>${body.join("")}</tbody></table></div>`;
}
@@ -177,9 +177,13 @@ export function renderStructureList(params: StructureListParams): void {
].sort((left, right) => left.anchor - right.anchor);
if (!rows.length) {
// 부품 빈 상태 작은 꼴(553-2)
const empty = document.createElement("li");
empty.className = "b05-route__irregular-empty";
empty.textContent = "배치된 구조물이 없습니다.";
empty.className = "ui-empty ui-empty--small b05-route__irregular-empty";
const text = document.createElement("p");
text.className = "ui-empty__text";
text.textContent = "배치된 구조물이 없습니다.";
empty.append(text);
list.append(empty);
return;
}
@@ -243,7 +243,7 @@ export function markRequired(
const box = entry.input.closest<HTMLElement>("[data-option-keys]")?.parentElement;
if (!box || box.querySelector(":scope > .b05-structure__required-note")) continue;
const note = document.createElement("p");
note.className = "b05-structure__required-note";
note.className = "ui-text ui-text--error b05-structure__required-note"; // 부품 상태 글(553-2)
note.textContent = "필수 칸 비어 있음 — [저장] 전 채움";
box.append(note);
}
+14 -62
View File
@@ -295,8 +295,7 @@ html.is-bm-route .b05-route__view-controls {
}
.b05-route__panel-body,
.b05-route__field,
.b05-route__metrics {
.b05-route__field {
display: flex;
flex-direction: column;
min-width: 0;
@@ -325,16 +324,14 @@ html.is-bm-route .b05-route__view-controls {
}
.b05-route__field,
.b05-route__check,
.b05-route__metrics {
.b05-route__check {
color: var(--color-text-body);
font-size: var(--text-caption);
}
.b05-route__field input,
.b05-route__field select,
.b05-route__panel-section > select,
.b05-route__textarea {
.b05-route__panel-section > select {
width: 100%;
min-width: 0;
padding: var(--spacing-8);
@@ -392,12 +389,12 @@ html.is-bm-route .b05-route__view-controls {
}
.b05-route__irregular-item:hover {
border-color: var(--color-royal-amethyst, rgb(109 40 217));
border-color: var(--color-accent);
}
.b05-route__irregular-item.is-selected {
border-color: var(--color-royal-amethyst, rgb(109 40 217));
background: color-mix(in srgb, var(--color-royal-amethyst, rgb(109 40 217)) 12%, transparent);
border-color: var(--color-accent);
background: color-mix(in srgb, var(--color-accent) 12%, transparent);
}
.b05-route__irregular-item strong {
@@ -417,10 +414,8 @@ html.is-bm-route .b05-route__view-controls {
white-space: nowrap;
}
/* 빈 목록 = 부품 빈 상태 작은 꼴(553-2) */
.b05-route__irregular-empty {
padding: var(--spacing-4) 0;
color: var(--color-text-muted);
font-size: var(--text-caption);
list-style: none;
}
@@ -440,38 +435,6 @@ html.is-bm-route .b05-route__view-controls {
gap: var(--spacing-8);
}
.b05-route__button-grid,
.b05-route__palette {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
min-width: 0;
gap: var(--spacing-8);
}
.b05-route__chip {
padding: var(--spacing-8);
border: 1px solid var(--color-border);
border-radius: var(--radius-inputs);
background: var(--color-surface);
color: var(--color-text-body);
font-size: var(--text-caption);
cursor: pointer;
}
.b05-route__chip.is-ep,
.b05-route__chip.is-fp {
color: var(--color-danger);
}
.b05-route__chip.is-bp {
color: var(--color-success);
}
.b05-route__chip.is-cp,
.b05-route__stale {
color: var(--color-warning);
}
/* 사이드 최하단 고정 영역 — 공용 ui-sidebar-actions(sticky·배경·상단 구분선)를 그대로
쓰되, 안쪽은 [구조물 목록][중간 구분선][액션 버튼 행] 세로 배치로 되돌린다
(2026-08-18 사용자 지시). */
@@ -494,23 +457,12 @@ html.is-bm-route .b05-route__view-controls {
flex: 1;
}
.b05-route__stale {
font-size: var(--text-caption);
font-weight: var(--font-weight-medium);
}
.b05-route__panel details {
color: var(--color-text-body);
font-size: var(--text-caption);
}
/* 계획선 설계 폼 안내문 */
.b05-route__note {
margin: 0;
color: var(--color-text-muted, var(--color-plum-velvet));
font-size: var(--text-caption);
line-height: 1.4;
}
/* 계획선 설계 폼 안내문 = 부품 보조 글(553-2) */
/* 종단면도 상단 절·성토 균형 지표 바 */
.b05-route-profile__balance {
@@ -536,31 +488,31 @@ html.is-bm-route .b05-route__view-controls {
}
.b05-route-profile__balance-item em {
color: var(--color-text-muted, var(--color-plum-velvet));
color: var(--color-text-muted);
font-style: normal;
}
.b05-route-profile__balance-item.is-cut em {
color: rgb(220 38 38);
color: var(--color-danger);
}
.b05-route-profile__balance-item.is-fill em {
color: rgb(37 99 235);
color: var(--color-info);
}
.b05-route-profile__balance-item.is-over,
.b05-route-profile__balance-item.is-unsaved {
color: rgb(180 83 9);
color: var(--color-warning);
font-weight: var(--font-weight-medium);
}
.b05-route-profile__balance-item.is-edited em {
color: var(--color-royal-amethyst, rgb(109 40 217));
color: var(--color-accent);
}
.b05-route-profile__balance-warning {
overflow: hidden;
color: rgb(180 83 9);
color: var(--color-warning);
text-overflow: ellipsis;
}
@@ -62,7 +62,7 @@
height: var(--b05-masshaul-height, 280px);
border-top: 1px solid var(--color-border);
background: var(--color-surface-raised);
box-shadow: 0 -4px 12px rgb(0 0 0 / 18%);
box-shadow: var(--shadow-lg);
}
/* 공용 리사이저는 절대배치라 오버레이 위 경계에 그대로 앉는다. */
@@ -134,10 +134,9 @@
pointer-events: none;
}
/* 빈 곡선 = 부품 빈 상태 작은 꼴(553-2) · 옆 여백만 */
.b05-profile__masshaul-empty {
padding: var(--spacing-8) var(--spacing-16);
color: var(--color-text-secondary);
font-size: var(--text-caption);
padding-inline: var(--spacing-16);
}
/* 2차 슬라이드 손잡이 — 다른 패널들과 **같은 표준 삼각형 손잡이**를 그대로 쓴다
@@ -174,7 +173,7 @@
height: var(--b05-table-height, 204px);
border-top: 1px solid var(--color-border);
background: var(--color-surface-raised);
box-shadow: 0 -4px 12px rgb(0 0 0 / 18%);
box-shadow: var(--shadow-lg);
}
.b05-profile__table-overlay > .ui-resizer--vertical {
+12 -23
View File
@@ -46,7 +46,7 @@
.b05-routeedit__hint-body,
.b05-routeedit__info {
border: 1px solid color-mix(in srgb, var(--color-border) 65%, transparent);
border-radius: var(--radius-8, 6px);
border-radius: var(--radius-cards);
background: color-mix(in srgb, var(--color-surface-raised) 72%, transparent);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
@@ -153,20 +153,9 @@
white-space: nowrap;
}
.b05-routeedit__legend i {
display: inline-block;
width: 22px;
height: 0;
margin-right: 2px;
vertical-align: middle;
}
.b05-routeedit__legend i.is-expected {
border-top: 2px dashed var(--color-text-secondary, #9ca3af);
}
.b05-routeedit__legend i.is-planned {
border-top: 2px solid var(--map-route, #f97316);
/* 범례 = 부품 「범례」 세로(553-2) · 줄 사이만 붙임 */
.b05-routeedit__legend.ui-legend {
gap: 0;
}
/* 오른쪽 덮는 판 — 패널 양식(382-1 · `ui_template_side_panels` · 손잡이 · 머리 · 폭 끌기) 을 지도 위에 얹음 ·
@@ -193,7 +182,7 @@
}
.b05-routeedit__side-wrap .ui-side-panels__panel {
box-shadow: 0 12px 40px rgb(0 0 0 / 45%);
box-shadow: var(--shadow-lg);
}
.b05-routeedit__side-wrap .ui-side-panels__body {
@@ -261,7 +250,7 @@
overflow: hidden;
width: 100%;
border: 1px solid var(--color-border);
border-radius: var(--radius-8, 6px);
border-radius: var(--radius-cards);
background: var(--color-surface);
}
@@ -363,7 +352,7 @@
height: 32px;
padding: 0 var(--spacing-8);
border: 1px solid color-mix(in srgb, var(--color-border) 65%, transparent);
border-radius: var(--radius-8, 6px);
border-radius: var(--radius-cards);
background: color-mix(in srgb, var(--color-surface-raised) 72%, transparent);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
@@ -395,8 +384,8 @@
gap: var(--spacing-8);
max-width: var(--b05-bottom-half);
padding: var(--spacing-8) var(--spacing-12);
border: 1px solid color-mix(in srgb, #22c55e 60%, transparent);
border-radius: var(--radius-8, 6px);
border: 1px solid color-mix(in srgb, var(--color-success) 60%, transparent);
border-radius: var(--radius-cards);
background: color-mix(in srgb, var(--color-surface-raised) 82%, transparent);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
@@ -430,7 +419,7 @@
align-items: center;
justify-content: center;
padding: var(--spacing-16);
background: rgb(0 0 0 / 55%);
background: var(--color-scrim);
color: #fff;
text-align: center;
}
@@ -448,12 +437,12 @@
min-width: 12rem;
padding: var(--spacing-8);
border: 1px solid var(--color-border);
border-radius: var(--radius-8, 6px);
border-radius: var(--radius-cards);
/* 모달 본체와 **같은 토큰**을 쓴다 — `--color-surface-2` 는 이 테마에 없어 밝은
기본값으로 떨어지면서 다크 화면에서 글자가 안 읽혔다(2026-09-07 실화면). */
background: var(--color-surface-raised);
color: var(--color-text-body);
box-shadow: 0 4px 14px rgb(0 0 0 / 45%);
box-shadow: var(--shadow-lg);
font-size: var(--text-caption);
}
@@ -76,7 +76,7 @@
margin-bottom: var(--spacing-8);
padding: var(--spacing-8);
border: 1px solid transparent;
border-radius: var(--radius-8, 6px);
border-radius: var(--radius-inputs);
}
/* 고른 갈래 — 알약과 같은 강조색 테두리. */
@@ -227,7 +227,7 @@
height: 460px;
overflow: hidden;
border: 1px solid var(--color-border);
border-radius: var(--radius-8, 6px);
border-radius: var(--radius-inputs);
background: var(--color-surface);
}
@@ -244,34 +244,11 @@
font-size: var(--text-body-sm);
}
.b05-routerec__legend {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-12);
color: var(--color-text-secondary);
font-size: var(--text-body-sm);
}
.b05-routerec__legend i {
display: inline-block;
width: 22px;
margin-right: 6px;
vertical-align: middle;
}
.b05-routerec__legend i.is-expected {
border-top: 2px dashed var(--color-text-secondary, #9ca3af);
}
.b05-routerec__legend i.is-current {
border-top: 2px solid var(--map-route, #f97316);
/* 범례 = 부품 「범례」(553-2) · 창을 연 노선은 지도처럼 흐림 */
.b05-routerec__legend .is-current {
opacity: 0.55;
}
.b05-routerec__legend i.is-preview {
border-top: 3px solid var(--color-primary);
}
/* 옵션 겹창 — 지도 왼쪽 위 작은 칸(회전 단추는 오른쪽 위 · PLAN 77-4). 지도 끌기를 막지 않게 제 칸만. */
.b05-routerec__side {
position: absolute;
@@ -286,9 +263,9 @@
overflow: auto;
padding: var(--spacing-8);
border: 1px solid var(--color-border);
border-radius: var(--radius-8, 6px);
border-radius: var(--radius-inputs);
background: color-mix(in srgb, var(--color-surface) 92%, transparent);
box-shadow: var(--shadow-sm, 0 2px 6px rgb(0 0 0 / 0.15));
box-shadow: var(--shadow-sm);
color: var(--color-text-body);
font-size: var(--text-caption);
}
@@ -326,16 +303,11 @@
accent-color: var(--color-primary);
}
/* 값 칸 — 슬라이더 옆에서 숫자를 직접 넣는다. 합 100 을 못 맞추면 빨간 테두리. */
.b05-routerec__num {
/* 값 칸 = 부품 입력 단 32(553-2) — 슬라이더 옆에서 숫자를 직접 넣는다. 합 100 을 못 맞추면 빨간 테두리. */
.b05-routerec__num.ui-input {
width: 100%;
min-width: 3.2em;
padding: 2px 4px;
border: 1px solid var(--color-border);
border-radius: var(--radius-4, 4px);
background: var(--color-surface);
color: var(--color-text-body);
font-size: var(--text-body);
padding-inline: 4px;
font-weight: 600;
text-align: center;
appearance: textfield;
@@ -353,8 +325,8 @@
}
.b05-routerec__num.is-invalid {
border-color: var(--color-danger, #dc2626);
box-shadow: 0 0 0 1px var(--color-danger, #dc2626);
border-color: var(--color-danger);
box-shadow: 0 0 0 1px var(--color-danger);
}
/* 저절로 정해진 비중은 고정 단추가 눌린 채 꺼져(잠김 표시) — 색은 공용 꼴(`ui-btn--glass`)을 쓴다. */
@@ -401,15 +373,10 @@
color: var(--color-text-body);
}
.b05-routerec__criteria input,
.b05-routerec__target input {
/* 제약 칸 = 부품 입력 단 32(553-2) — 자리 · 오른쪽 맞춤만 */
.b05-routerec__criteria input.ui-input,
.b05-routerec__target input.ui-input {
width: 100%;
padding: 4px 8px;
border: 1px solid var(--color-border);
border-radius: var(--radius-4, 4px);
background: var(--color-surface);
color: var(--color-text-body);
font-size: var(--text-body);
text-align: right;
}
@@ -497,7 +464,7 @@
width: 100%;
height: 96px;
border: 1px solid var(--color-border);
border-radius: var(--radius-8, 6px);
border-radius: var(--radius-inputs);
background: var(--color-surface);
}
@@ -521,47 +488,17 @@
stroke-dasharray: 6 3;
}
/* 그림 범례 = 부품 「범례」(553-2) */
.b05-routerec__plegend {
display: flex;
gap: var(--spacing-12);
margin-top: var(--spacing-4, 4px);
color: var(--color-text-secondary);
font-size: var(--text-caption);
}
.b05-routerec__plegend i {
display: inline-block;
width: 16px;
margin-right: var(--spacing-4, 4px);
vertical-align: middle;
border-top: 2px solid var(--color-text-secondary);
}
.b05-routerec__plegend i.is-plan {
border-top-color: var(--color-primary);
}
.b05-routerec__plegend i.is-initial {
border-top: 2px dashed var(--map-route, #f97316);
margin-top: var(--spacing-4);
}
/* 이 모달에서 셈한 후보 표 — 절 · 성 · 반입 · 최대 종단경사. */
.b05-routerec__cands table {
width: 100%;
border-collapse: collapse;
font-size: var(--text-caption);
/* 후보 표 = 부품 표 데이터표(553-2) — 수 칸 자리 맞춤 · 한 줄 */
.b05-routerec__cands .ui-table {
font-variant-numeric: tabular-nums;
}
.b05-routerec__cands th,
.b05-routerec__cands td {
padding: 2px var(--spacing-8);
border-bottom: 1px solid var(--color-border);
text-align: right;
.b05-routerec__cands .ui-table td {
white-space: nowrap;
}
.b05-routerec__cands th[scope="row"],
.b05-routerec__cands thead th:first-child {
text-align: left;
}
+13 -24
View File
@@ -51,7 +51,7 @@
padding: 0 7px;
border-radius: 9px;
border: 1.5px solid currentColor;
background: var(--color-surface, #fff);
background: var(--color-surface);
font-size: 10px;
font-weight: 700;
line-height: 1;
@@ -117,8 +117,8 @@
padding: 4px 8px;
border: 1.5px solid currentColor;
border-radius: 6px;
background: var(--color-surface, #fff);
box-shadow: 0 2px 6px rgb(0 0 0 / 18%);
background: var(--color-surface);
box-shadow: var(--shadow-sm);
font-size: 11px;
line-height: 1.35;
white-space: nowrap;
@@ -161,7 +161,7 @@
.b05-structure__group {
margin: 0;
padding: 6px 8px 8px;
border: 1px solid var(--border-color, #3a3f4a);
border: 1px solid var(--color-border);
border-radius: 6px;
}
@@ -197,7 +197,7 @@
height: 0;
margin: 2px 0;
border: 0;
border-top: 1px solid var(--color-border, #3a3f4a);
border-top: 1px solid var(--color-border);
}
/* 「제원·수량은 다른 화면이 냅니다」 안내 — 경고가 아니라 안내라 색은 흐리게,
@@ -205,9 +205,9 @@
.b05-structure__owner-note {
margin: 0;
padding: 4px 0 4px 8px;
border-left: 2px solid var(--color-border, #3a3f4a);
color: var(--color-text-muted, #9aa1ad);
font-size: var(--text-caption, 12px);
border-left: 2px solid var(--color-border);
color: var(--color-text-muted);
font-size: var(--text-caption);
line-height: 1.5;
}
@@ -236,20 +236,18 @@
/* 측점 입력이 형식에 안 맞을 때(예: "3+18.0"도 숫자도 아님) 칸을 붉힌다. */
.b05-route__field input.is-invalid {
border-color: #e05b5b;
background: rgba(224, 91, 91, 0.08);
border-color: var(--color-danger);
background: color-mix(in srgb, var(--color-danger) 8%, transparent);
}
/* 빈 필수 칸 — 칸 테두리 + 그 칸이 든 상자 안 안내 한 줄(54-12) */
.b05-route__field .is-required-empty {
border-color: #e05b5b;
border-color: var(--color-danger);
}
/* 상태 글 = 부품 보조 글 오류 꼴(553-2) */
.b05-structure__required-note {
grid-column: 1 / -1;
margin: 0;
color: #e05b5b;
font-size: var(--text-caption, 12px);
}
/* 측점 범위(계산 표시) — C군 등 길이 옵션 기반 구간 타입. 입력 칸이 아니라 기준
@@ -365,15 +363,6 @@
font-size: var(--text-caption);
}
/* 물넘이·세월교 개략 단면 결과 — 읽어 보는 안내문이라 입력 칸보다 작게 둔다
(2026-08-30 사용자: 글자가 너무 크다). */
.b05-drainage__facility-note {
margin: 0;
font-size: 10px;
line-height: 1.4;
opacity: 0.75;
}
/* 스텝 묶음과 그 안의 버튼·숫자칸도 같은 높이로 선다. */
.b05-structure__stepper {
height: var(--b05-control-h);
@@ -419,7 +408,7 @@
/* 전면 기울기가 품셈 표준경사 표(0.20~0.50) 밖일 때 — 실무가 S0.7 을 쓰므로 **막지 않고**
테두리로만 알린다(2026-09-09). 말풍선에 까닭이 들어 있다. */
.is-outside-standard {
border-color: var(--color-warning, #c98a00);
border-color: var(--color-warning);
}
/* 구조물 조합 잇기 줄(PLAN 21-4 · 54-17) — 옵션 칸 꼴(`b05-route__field`) 2행 · 아래 줄은
+14 -18
View File
@@ -18,7 +18,7 @@
flex: 0 0 auto;
flex-direction: column;
min-height: 0;
border-top: 2px solid var(--color-text-muted, var(--color-plum-velvet));
border-top: 2px solid var(--color-text-muted);
color: var(--color-text-body);
/* 행 높이와 셀 폭에 맞춰 렌더러가 계산해 넣는다 (createProfileTable). */
font-size: var(--b05-table-font, 11px);
@@ -37,7 +37,7 @@
/* 구배 / 측점값 / 곡선 묶음의 시작 행은 굵은 선으로 그룹을 구분한다. */
.b05-profile-table__row.is-group-start {
border-top: 2px solid var(--color-text-muted, var(--color-plum-velvet));
border-top: 2px solid var(--color-text-muted);
}
.b05-profile-table__row--grade {
@@ -62,7 +62,7 @@
width: var(--b05-table-label-width, 60px);
height: 100%;
padding-inline: var(--spacing-16) var(--spacing-4);
border-right: 2px solid var(--color-text-muted, var(--color-plum-velvet));
border-right: 2px solid var(--color-text-muted);
background: var(--color-surface-raised);
color: var(--color-text);
font-size: var(--b05-table-label-font, inherit);
@@ -121,15 +121,15 @@
}
.b05-profile-table__row.is-cut .b05-profile-table__cell {
color: rgb(220 38 38);
color: var(--color-danger);
}
.b05-profile-table__row.is-fill .b05-profile-table__cell {
color: rgb(37 99 235);
color: var(--color-info);
}
.b05-profile-table__row.is-plan .b05-profile-table__cell {
color: var(--color-royal-amethyst, rgb(109 40 217));
color: var(--color-accent);
font-weight: var(--font-weight-medium);
}
@@ -146,8 +146,8 @@
}
.b05-profile-table__segment.is-violation {
background: color-mix(in srgb, rgb(220 38 38) 12%, transparent);
color: rgb(180 83 9);
background: color-mix(in srgb, var(--color-danger) 12%, transparent);
color: var(--color-warning);
}
/* 구배 블록 값. 가로로 안 들어가면 90도 회전해 좁은 블록에도 값을 표기한다. */
@@ -223,12 +223,8 @@
display: flex;
flex-direction: column;
box-sizing: border-box;
border-inline: 1px solid var(--color-royal-amethyst, rgb(109 40 217));
background: color-mix(
in srgb,
var(--color-royal-amethyst, rgb(109 40 217)) 14%,
var(--color-surface)
);
border-inline: 1px solid var(--color-accent);
background: color-mix(in srgb, var(--color-accent) 14%, var(--color-surface));
transform: translateX(-50%);
/* 열 자체는 클릭을 통과시키고, 입력 셀만 pointer-events를 되살린다(작업6). */
pointer-events: none;
@@ -240,7 +236,7 @@
border-inline-width: 2px;
box-shadow:
0 0 0 1px var(--color-surface-raised),
0 4px 12px rgb(0 0 0 / 28%);
var(--shadow-lg);
}
.b05-profile-table__irregular-col-cell {
@@ -281,11 +277,11 @@
}
.b05-profile-table__irregular-col-cell.is-cut {
color: rgb(220 38 38);
color: var(--color-danger);
}
.b05-profile-table__irregular-col-cell.is-fill {
color: rgb(37 99 235);
color: var(--color-info);
}
/* MC 측점 이름(507-3 덧) — 굵게 · 경고색 */
@@ -315,7 +311,7 @@
.b05-profile-table__irregular-input:focus,
.b05-profile-table__irregular-input:hover {
border-color: var(--color-royal-amethyst, rgb(109 40 217));
border-color: var(--color-accent);
background: var(--color-surface);
}
@@ -34,6 +34,9 @@ function el(tag) {
replaceChildren(...kids) {
this.children = kids;
},
setAttribute(name, value) {
(this.attrs ??= {})[name] = String(value);
},
addEventListener(type, fn) {
(this.listeners[type] ??= []).push(fn);
},
+3
View File
@@ -24,6 +24,9 @@ function el(tag) {
node.children = [];
node.append(...items);
},
setAttribute(name, value) {
(this.attrs ??= {})[name] = String(value);
},
addEventListener(type, fn) {
(listeners[type] ??= []).push(fn);
},
+1 -1
View File
@@ -122,6 +122,6 @@ def test_균형_종단은_캐시에_안_남는다() -> None:
assert result["svg"].count("<path") == 2
assert "<td>토공 최소</td>" in result["table"]
assert (
'<th scope="col" rowspan="1" title="누가토량">누가토량</th>' in result["table"]
'<th scope="col" class="is-end">누가토량</th>' in result["table"] # 부품 표(553-2)
and "반입" not in result["table"]
)