Merge remote-tracking branch 'origin/dev' into sub_laptop_7

This commit is contained in:
2026-10-10 14:01:04 +09:00
10 changed files with 399 additions and 92 deletions
@@ -244,7 +244,7 @@ export function visiblePieces(pieces: DesignPiece[], occluders: CrossPoint[][]):
return out;
}
/** 치수 — 사면길이 · 수평 · 높이(m) · 구배(사면 1:n · 노면 %). */
/** 치수 — 길이 · 수평 · 높이(m) · 경사비율(사면 1:n · 노면 %). */
export function pieceMeasure(
piece: DesignPiece,
zone: CrossZone,
@@ -262,13 +262,13 @@ export function pieceMeasure(
return { lengthM: Math.hypot(horizontalM, heightM), horizontalM, heightM, grade };
}
/** 두 줄(151-1) — 1줄 = 길이 · 수평 · 2줄 = 높이 · 구배(줄바꿈 글자로 가름). */
/** 두 줄(151-1 · 195-1) — 1줄 = 길이 · 경사비율 · 2줄 = 수평 · 높이(줄바꿈 글자로 가름). */
export function pieceLabel(design: ZoneDesign, piece: DesignPiece): string {
const { zone } = pieceZone(design, piece);
const m = pieceMeasure(piece, zone);
return (
`${zone === "road" ? "길이" : "사면길이"} ${m.lengthM.toFixed(2)}m · ` +
`수평 ${m.horizontalM.toFixed(2)}m\n높이 ${m.heightM.toFixed(2)}m · ${m.grade}`
`길이 ${m.lengthM.toFixed(2)}m · 경사비율 ${m.grade}\n` +
`수평 ${m.horizontalM.toFixed(2)}m · 높이 ${m.heightM.toFixed(2)}m`
);
}
@@ -6,6 +6,7 @@
* ⭐ 2026-09-15 — 그 밑에 사면길이 3m 초과 **소단 검토 정보 한 줄**(경고 모양 아님 · 브레인 ③).
* ⭐ 138-1 — 「⚠ 포장 권장 · n측점」 줄(B05 법정 경사 제안 · 포장 범위 밖 측점 · 옛 카드 배지 자리).
* ⭐ 138-3 — 줄 전체가 카드 칸 위에 고정(sticky) · 알림 없어도 줄 유지(오른쪽 끝 열 수 칸은 부르는 쪽이 붙임).
* ⭐ 196-1 — 항목 = 짧은 이름 + 수 칩 · 긴 설명 = 호버 · 내용 칸 하나 공유(`_Notice_Chips`).
* ========================================================================== */
import type { CrossSection } from "./B06_Section_Api_Fetch";
@@ -18,83 +19,65 @@ import {
} from "./B06_Section_UI_Cross_FillSlope_Warn";
import { L, stationLabel } from "./B06_Section_UI_Section_Common";
import { pavedBySpecs } from "./B06_Section_Cross_Refresh";
import { createNoticeChips, type NoticeChipRow } from "./B06_Section_UI_Cross_Notice_Chips";
export interface FillSlopeNotice {
root: HTMLElement;
/** 측점 설계가 바뀔 때마다 부른다 — 펼침 상태는 그대로 둔다. */
/** 측점 설계가 바뀔 때마다 부른다 — 연 칩은 그대로 둔다. */
update: (sections: ReadonlyArray<CrossSection>, stationInterval: number) => void;
}
function noticeBlock(className: string): {
root: HTMLDetailsElement;
fill: (head: string, rows: FillSlopeWarning[], stationInterval: number) => void;
} {
const root = document.createElement("details");
root.className = className;
root.hidden = true;
const summary = document.createElement("summary");
const list = document.createElement("div");
list.className = "b06-section__notice-list";
root.append(summary, list);
return {
root,
fill(head, rows, stationInterval) {
root.hidden = !rows.length;
summary.textContent = `${head} · ${rows.length}측점`;
list.replaceChildren(
...rows.map(({ section, sides }) => {
const button = document.createElement("button");
button.type = "button";
const parts = sides.map(({ side, lengthM, open }) => {
const label = L(side === "left" ? "B06_Design_Ditch_Left" : "B06_Design_Ditch_Right");
return `${label} ${open ? "≥" : ""}${lengthM.toFixed(2)}m`;
});
button.textContent =
`${stationLabel(section.chainage_m, stationInterval)} ${parts.join(" · ")}`.trim();
button.dataset.stationId = section.station_id;
return button;
}),
);
},
};
/** 측점 단추 글 — 측점 + 쪽별 길이(포장 권장은 측점만) */
function rows(warnings: FillSlopeWarning[], stationInterval: number): NoticeChipRow[] {
return warnings.map(({ section, sides }) => {
const parts = sides.map(({ side, lengthM, open }) => {
const label = L(side === "left" ? "B06_Design_Ditch_Left" : "B06_Design_Ditch_Right");
return `${label} ${open ? "≥" : ""}${lengthM.toFixed(2)}m`;
});
return {
stationId: section.station_id,
text: `${stationLabel(section.chainage_m, stationInterval)} ${parts.join(" · ")}`.trim(),
};
});
}
export function createFillSlopeNotice(onPick: (stationId: string) => void): FillSlopeNotice {
const root = document.createElement("div");
root.className = "b06-section__notice-bar";
const blocks = document.createElement("div");
blocks.className = "b06-section__notice-blocks";
root.append(blocks);
const warn = noticeBlock("b06-section__notice");
const info = noticeBlock("b06-section__notice b06-section__notice--info");
const pave = noticeBlock("b06-section__notice");
pave.root.title = L("B06_Design_Paved_Suggested");
blocks.append(warn.root, pave.root, info.root);
root.addEventListener("click", (event) => {
const id = (event.target as HTMLElement).closest("button")?.dataset.stationId;
if (id) onPick(id);
});
const chips = createNoticeChips(onPick);
root.append(chips.root);
return {
root,
update(sections, stationInterval) {
warn.fill(
`⚠ ${FILL_SLOPE_WARN_TEXT}`,
fillSlopeWarnings(sections, fillSlopeLengths),
stationInterval,
);
pave.fill(
`⚠ ${L("B06_Notice_PaveSuggested")}`,
sections
.filter((section) => section.design?.pavement_suggested && !pavedBySpecs(section))
.map((section) => ({ section, sides: [] })),
stationInterval,
);
info.fill(
`ⓘ ${BERM_INFO_TEXT}`,
bermReviewStations(sections, slopeRunLengths),
stationInterval,
);
chips.update([
{
key: "fill",
label: L("B06_Notice_FillSlope"),
hint: FILL_SLOPE_WARN_TEXT,
tone: "warn",
rows: rows(fillSlopeWarnings(sections, fillSlopeLengths), stationInterval),
},
{
key: "pave",
label: L("B06_Notice_PaveSuggested"),
hint: L("B06_Design_Paved_Suggested"),
tone: "warn",
rows: rows(
sections
.filter((section) => section.design?.pavement_suggested && !pavedBySpecs(section))
.map((section) => ({ section, sides: [] })),
stationInterval,
),
},
{
key: "berm",
label: L("B06_Notice_Berm"),
hint: BERM_INFO_TEXT,
tone: "info",
rows: rows(bermReviewStations(sections, slopeRunLengths), stationInterval),
},
]);
},
};
}
@@ -0,0 +1,95 @@
/* =============================================================================
* B06_Section_UI_Cross_Notice_Chips.ts
* 횡단 알림 줄 칩 꼴(196-1) — 항목 = 짧은 이름 + 측점 수 칩 한 줄 · 긴 설명 = 공용 호버.
* 줄 아래 내용 칸 하나를 같이 씀 — 누른 칩의 측점 목록만 · 같은 칩 다시 누르면 닫힘 ·
* 다른 칩 누르면 바뀜(한 번에 하나). 측점 단추 누름 = `onPick`(그 카드로).
* 무엇을 셀지는 부르는 쪽(`_FillSlope_Notice`) — 여기는 그리기만.
* ========================================================================== */
import { attachHoverInfo } from "@ui/ui_template_hover_info";
export interface NoticeChipRow {
stationId: string;
text: string;
}
export interface NoticeChipItem {
key: string;
/** 짧은 이름 — 칩 글 = 이름 + 측점 수 */
label: string;
/** 긴 설명 — 호버 */
hint: string;
tone: "warn" | "info";
rows: NoticeChipRow[];
}
export interface NoticeChips {
root: HTMLElement;
/** 측점 설계가 바뀔 때마다 — 연 칩은 그대로(측점이 0 이 되면 닫힘) */
update: (items: NoticeChipItem[]) => void;
}
export function createNoticeChips(onPick: (stationId: string) => void): NoticeChips {
const root = document.createElement("div");
root.className = "b06-section__notice-blocks";
const chips = document.createElement("div");
chips.className = "b06-section__notice-chips";
const panel = document.createElement("div");
panel.className = "b06-section__notice-list";
panel.hidden = true;
root.append(chips, panel);
let items: NoticeChipItem[] = [];
let openKey: string | null = null;
const render = (): void => {
const open = items.find((item) => item.key === openKey);
if (!open) openKey = null;
chips.replaceChildren(
...items.map((item) => {
const chip = document.createElement("button");
chip.type = "button";
chip.className = `b06-section__notice-chip b06-section__notice-chip--${item.tone}`;
chip.dataset.noticeKey = item.key;
chip.setAttribute("aria-expanded", String(item === open));
chip.textContent = `${item.label} ${item.rows.length}`;
return chip;
}),
);
panel.hidden = !open;
panel.classList.toggle("is-info", open?.tone === "info");
panel.replaceChildren(
...(open?.rows ?? []).map((row) => {
const button = document.createElement("button");
button.type = "button";
button.textContent = row.text;
button.dataset.stationId = row.stationId;
return button;
}),
);
};
chips.addEventListener("click", (event) => {
const key = (event.target as HTMLElement).closest<HTMLElement>(".b06-section__notice-chip")
?.dataset.noticeKey;
if (!key) return;
openKey = openKey === key ? null : key;
render();
});
panel.addEventListener("click", (event) => {
const id = (event.target as HTMLElement).closest("button")?.dataset.stationId;
if (id) onPick(id);
});
attachHoverInfo(
chips,
(target) => items.find((item) => item.key === target.dataset.noticeKey)?.hint,
".b06-section__notice-chip",
);
return {
root,
update(next) {
items = next.filter((item) => item.rows.length);
render();
},
};
}
@@ -14,6 +14,7 @@
import { API_BASE_URL } from "@config/config_frontend";
import { createButton, showToast } from "@ui/ui_template_elements";
import { attachHoverInfo } from "@ui/ui_template_hover_info";
interface MissingStation {
chainage_m: number;
@@ -67,8 +68,9 @@ export async function mountMissingStationNotice(
.join(" · ");
text.textContent =
`측점이 없는 구조물 ${missing.length}개 — ${where}` +
(missing.length > 6 ? ` 외 ${missing.length - 6}개` : "") +
". 이 자리는 횡단도에도 안 서고 수량에서도 빠집니다.";
(missing.length > 6 ? ` 외 ${missing.length - 6}개` : "");
// 긴 까닭 글 = 호버(196-1)
attachHoverInfo(text, "이 자리는 횡단도에도 안 서고 수량에서도 빠집니다.");
box.append(text);
if (data.can_create) {
+30 -19
View File
@@ -142,10 +142,6 @@
min-width: 0;
}
.b06-section__notice-blocks > .b06-section__notice:last-of-type {
margin-bottom: 0;
}
/* 열 수 칸 — 라벨 왼쪽 · 한 줄(150-1) */
.ui-field.b06-section__columns {
flex: none;
@@ -168,33 +164,48 @@
display: none;
}
/* 펼친 목록이 길면 줄 안에서 스크롤 — 고정 줄이 카드 칸을 다 덮지 않게 */
.b06-section__notice-list {
max-height: 30vh;
overflow: auto;
/* 알림 칩 한 줄(196-1) — 짧은 이름 + 수 · 긴 설명 = 호버 · 누른 칩 = 눌림 꼴 */
.b06-section__notice-chips {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-4);
}
/* 성토사면 5m 초과 경고 줄(2026-09-14) — 요약 한 줄, 펼치면 측점 단추 목록. */
.b06-section__notice {
margin-bottom: var(--spacing-8);
.b06-section__notice-chip {
padding: 0 var(--spacing-8);
border: 1px solid currentColor;
border-radius: var(--radius-sm);
background: transparent;
color: var(--color-warning);
font-size: var(--text-caption);
}
/* 소단 검토 — 정보 한 줄(경고 색 아님 · 2026-09-15 브레인 ③). */
.b06-section__notice--info {
color: var(--color-text-secondary);
}
.b06-section__notice > summary {
white-space: nowrap;
cursor: pointer;
}
/* 소단 검토 — 정보(경고 색 아님 · 2026-09-15 브레인 ③). */
.b06-section__notice-chip--info,
.b06-section__notice-list.is-info {
color: var(--color-text-secondary);
}
.b06-section__notice-chip[aria-expanded="true"] {
background: var(--color-surface-raised);
font-weight: 600;
}
/* 내용 칸 하나(누른 칩의 측점 목록) — 길면 줄 안에서 스크롤 · 고정 줄이 카드 칸을 다 덮지 않게 */
.b06-section__notice-list {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-4);
max-height: 30vh;
overflow: auto;
padding-top: var(--spacing-4);
color: var(--color-warning);
}
.b06-section__notice-list[hidden] {
display: none;
}
.b06-section__notice-list > button {
@@ -223,7 +223,7 @@
background: color-mix(in srgb, var(--color-surface-raised) 88%, transparent);
}
/* 고른 보라 선 치수(137-2) — 카드 폭 무관 늘 [+ − ⤢] 왼쪽(179-1) · 두 줄(151-1 · 길이 · 수평 / 높이 · 구배). */
/* 고른 보라 선 치수(137-2) — 카드 폭 무관 늘 [+ − ⤢] 왼쪽(179-1) · 두 줄(151-1 · 길이 · 경사비율 / 수평 · 높이). */
.b06-cross-card__line-dims {
align-self: center;
padding: 1px var(--spacing-4);
+4 -4
View File
@@ -57,9 +57,9 @@ def test_치수_사면길이_수평_높이_구배(out) -> None:
assert abs(fill["lengthM"] - 13**0.5) < 1e-9
assert out["measure"]["road"]["grade"] == "2.0%" # 노면 = %
assert out["measure"]["flatSlope"] == "0.0%"
# 두 줄(151-1) — 1줄 = 길이 · 수평 · 2줄 = 높이 · 구배
assert out["label"]["fill"] == "사면길이 3.61m · 수평 3.00m\n높이 2.00m · 1:1.50"
assert out["label"]["road"] == "길이 2.50m · 수평 2.50m\n높이 0.05m · 2.0%"
# 두 줄(151-1 · 195-1) — 1줄 = 길이 · 경사비율 · 2줄 = 수평 · 높이
assert out["label"]["fill"] == "길이 3.61m · 경사비율 1:1.50\n수평 3.00m · 높이 2.00m"
assert out["label"]["road"] == "길이 2.50m · 경사비율 2.0%\n수평 2.50m · 높이 0.05m"
def test_구역_도로_절토_성토(out) -> None:
@@ -84,7 +84,7 @@ def test_실제_배관_측점_조각과_판정_선(out) -> None:
assert real["hitsBelowStructure"] is True # 구조물 판정면이 위
assert real["picked"] == [0] and real["stopped"] is True # 카드 고름으로 안 번짐
assert real["activeCount"] == 1 and real["activeIndex"] == 1
assert real["labelOn"]["hidden"] is False and real["labelOn"]["text"].startswith("사면길이 ")
assert real["labelOn"]["hidden"] is False and real["labelOn"]["text"].startswith("길이 ")
assert real["labelOff"] == {"hidden": True, "text": ""}
assert set(real["zones"]) <= {"road", "cut", "fill"}
+19
View File
@@ -0,0 +1,19 @@
"""PLAN 195-1 — B06 보라 선 고름 정보 글: 「사면 길이」 → 「길이」 · 차례 = 길이 → 경사비율 → 수평 → 높이.
짝: `B06_Section/B06_Section_UI_Cross_Design_Pick.ts` `pieceLabel`. 실제 TS 를 `helper_137_design_pick.cjs` 가 Node 로 돌린다.
"""
from __future__ import annotations
import re
from test_137_design_pick import out # noqa: F401 — 같은 Node 도우미 결과를 씀
def test_글_차례_넷_길이_경사비율_수평_높이(out) -> None: # noqa: F811
for key in ("fill", "road"):
label = out["label"][key]
assert "사면" not in label
names = re.findall(r"(길이|경사비율|수평|높이)", label)
assert names == ["길이", "경사비율", "수평", "높이"], label
assert out["label"]["fill"].startswith("길이 ")
@@ -0,0 +1,195 @@
"""196-1 B06 횡단 맨 위 알림 줄 — 칩 꼴 · 짧은 이름 + 수 · 긴 설명 = 호버 · 내용 칸 하나 공유.
`B06_Section_UI_Cross_Notice_Chips.ts` 를 Node 로 그대로 돌림(작은 가짜 DOM · 호버는 받은 글만 기록).
줄 짜임(sticky · 열 수 칸 · 카드 scroll-margin)과 이름 · 설명 자리는 원문 대조.
"""
import json
import subprocess
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
TS = ROOT / "config" / "node_modules" / "typescript"
B06 = ROOT / "B06_Section"
MOD = B06 / "B06_Section_UI_Cross_Notice_Chips.ts"
PRELUDE = f"""
const fs = require("fs");
const ts = require({json.dumps(str(TS))});
const js = ts.transpileModule(fs.readFileSync({json.dumps(str(MOD))}, "utf8"), {{
compilerOptions: {{ module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }},
}}).outputText;
class El {{
constructor(tag) {{
this.tag = tag; this.parentElement = null; this.children = []; this.on = {{}};
this.attrs = {{}}; this.dataset = {{}}; this.hidden = false; this.textContent = ""; this.cls = new Set();
const cls = this.cls;
this.classList = {{ toggle: (c, on) => (on ? cls.add(c) : cls.delete(c)), contains: (c) => cls.has(c) }};
}}
set className(v) {{ this.cls.clear(); v.split(/\\s+/).filter(Boolean).forEach((c) => this.cls.add(c)); }}
get className() {{ return [...this.cls].join(" "); }}
append(...cs) {{ cs.forEach((c) => {{ c.parentElement = this; this.children.push(c); }}); }}
replaceChildren(...cs) {{ this.children = []; this.append(...cs); }}
setAttribute(k, v) {{ this.attrs[k] = String(v); }}
getAttribute(k) {{ return this.attrs[k] ?? null; }}
addEventListener(t, f) {{ (this.on[t] ||= []).push(f); }}
closest(sel) {{
for (let x = this; x; x = x.parentElement)
if (sel.startsWith(".") ? x.cls.has(sel.slice(1)) : x.tag === sel) return x;
return null;
}}
click() {{
const e = {{ type: "click", target: this }};
for (let x = this; x; x = x.parentElement) (x.on.click || []).forEach((f) => f(e));
}}
}}
const document = {{ createElement: (t) => new El(t) }};
const hovers = [];
const fakeRequire = (name) => {{
if (name !== "@ui/ui_template_hover_info") throw new Error("뜻밖의 import " + name);
return {{ attachHoverInfo: (root, text, sel) => hovers.push({{ root, text, sel }}) }};
}};
const mod = {{ exports: {{}} }};
new Function("exports", "module", "require", "document", js)(mod.exports, mod, fakeRequire, document);
const picks = [];
const bar = mod.exports.createNoticeChips((id) => picks.push(id));
const [chipsRow, panel] = bar.root.children;
const chip = (key) => chipsRow.children.find((c) => c.dataset.noticeKey === key);
const view = () => ({{
chips: chipsRow.children.map((c) => [c.textContent, c.getAttribute("aria-expanded"), c.className]),
panelHidden: panel.hidden,
panelInfo: panel.classList.contains("is-info"),
rows: panel.children.map((b) => [b.textContent, b.dataset.stationId]),
}});
const row = (id) => ({{ stationId: id, text: "STA " + id }});
const items = (fill, pave, berm) => [
{{ key: "fill", label: "성토 5m↑", hint: "긴 성토 설명", tone: "warn", rows: fill.map(row) }},
{{ key: "pave", label: "포장 권장", hint: "긴 포장 설명", tone: "warn", rows: pave.map(row) }},
{{ key: "berm", label: "소단 검토", hint: "긴 소단 설명", tone: "info", rows: berm.map(row) }},
];
const out = {{}};
"""
def _node(body: str):
script = PRELUDE + body + "\nconsole.log(JSON.stringify(out));"
got = subprocess.run(
["node", "-e", script], capture_output=True, text=True, encoding="utf-8", timeout=60
)
assert got.returncode == 0, got.stderr
return json.loads(got.stdout)
def test_칩_이름은_짧은_이름_더하기_측점_수_0이면_안_보임():
got = _node(
"""
bar.update(items(["a", "b", "c"], [], ["a", "d", "e", "f", "g"]));
out.v = view();
out.bar = bar.root.className;
out.tags = chipsRow.children.map((c) => [c.tag, c.type]);
"""
)
assert got["v"]["chips"] == [
["성토 5m↑ 3", "false", "b06-section__notice-chip b06-section__notice-chip--warn"],
["소단 검토 5", "false", "b06-section__notice-chip b06-section__notice-chip--info"],
]
assert got["tags"] == [["button", "button"], ["button", "button"]]
assert got["v"]["panelHidden"] is True and got["v"]["rows"] == []
def test_긴_설명은_공용_호버로_칩마다():
got = _node(
"""
bar.update(items(["a"], ["b"], ["c"]));
out.count = hovers.length;
out.root = hovers[0].root === chipsRow;
out.sel = hovers[0].sel;
out.hints = chipsRow.children.map((c) => hovers[0].text(c));
out.other = hovers[0].text({ dataset: {} }) ?? null;
"""
)
assert got["count"] == 1 and got["root"] and got["sel"] == ".b06-section__notice-chip"
assert got["hints"] == ["긴 성토 설명", "긴 포장 설명", "긴 소단 설명"]
assert got["other"] is None
def test_내용_칸_하나_한_번에_하나_다시_누르면_닫힘():
got = _node(
"""
bar.update(items(["a", "b"], ["p"], ["c"]));
out.panels = bar.root.children.length;
chip("fill").click();
out.open1 = view();
chip("berm").click();
out.open2 = view();
chip("berm").click();
out.closed = view();
"""
)
assert got["panels"] == 2 # 칩 줄 + 내용 칸 하나
o1 = got["open1"]
assert o1["panelHidden"] is False and o1["panelInfo"] is False
assert o1["rows"] == [["STA a", "a"], ["STA b", "b"]]
assert [c[1] for c in o1["chips"]] == ["true", "false", "false"]
o2 = got["open2"]
assert o2["rows"] == [["STA c", "c"]] and o2["panelInfo"] is True
assert [c[1] for c in o2["chips"]] == ["false", "false", "true"]
c = got["closed"]
assert c["panelHidden"] is True and c["rows"] == []
assert [x[1] for x in c["chips"]] == ["false", "false", "false"]
def test_측점_누르면_그_카드로_다시_그려도_연_칩_유지_0이면_닫힘():
got = _node(
"""
bar.update(items(["a", "b"], [], []));
chip("fill").click();
panel.children[1].click();
chipsRow.children[0].click(); chipsRow.children[0].click(); // 칩 누름은 측점 고름 아님
out.picks = picks.slice();
bar.update(items(["a", "b", "x"], [], []));
out.kept = view();
bar.update(items([], ["p"], []));
out.gone = view();
"""
)
assert got["picks"] == ["b"]
assert got["kept"]["rows"] == [["STA a", "a"], ["STA b", "b"], ["STA x", "x"]]
assert got["kept"]["chips"][0][:2] == ["성토 5m↑ 3", "true"]
assert got["gone"]["panelHidden"] is True and got["gone"]["chips"][0][:2] == [
"포장 권장 1",
"false",
]
def test_알림_줄은_칩을_쓰고_긴_설명은_호버_짧은_이름은_사전():
src = (B06 / "B06_Section_UI_Cross_FillSlope_Notice.ts").read_text(encoding="utf-8")
assert "<details" not in src and '"details"' not in src and "summary" not in src
assert "createNoticeChips(onPick)" in src
for label, hint in (
('L("B06_Notice_FillSlope")', "FILL_SLOPE_WARN_TEXT"),
('L("B06_Notice_PaveSuggested")', 'L("B06_Design_Paved_Suggested")'),
('L("B06_Notice_Berm")', "BERM_INFO_TEXT"),
):
assert f"label: {label}" in src and f"hint: {hint}" in src
loc = (ROOT / "ui_template" / "ui_template_locale_b2.ts").read_text(encoding="utf-8")
assert 'B06_Notice_FillSlope: ["성토 5m↑"' in loc
assert 'B06_Notice_PaveSuggested: ["포장 권장"' in loc
assert 'B06_Notice_Berm: ["소단 검토"' in loc
def test_sticky_열_수_칸_카드_scroll_margin_그대로():
view = (B06 / "B06_Section_UI_Section_View.ts").read_text(encoding="utf-8")
assert "fillSlopeNotice.root.append(columnsField.root)" in view
assert '"--b06-notice-h"' in view
css = (B06 / "B06_Section_UI_Style_Cross.css").read_text(encoding="utf-8")
bar = css[css.index(".b06-section__notice-bar {") :]
assert "position: sticky" in bar[: bar.index("}")]
assert "scroll-margin-top: calc(var(--b06-notice-h" in css
assert ".b06-section__notice-list[hidden]" in css and "max-height: 30vh" in css
def test_측점_없는_구조물_줄_긴_까닭은_호버():
src = (B06 / "B06_Section_UI_Missing_Stations.ts").read_text(encoding="utf-8")
assert '". 이 자리는' not in src
assert 'attachHoverInfo(text, "이 자리는 횡단도에도 안 서고 수량에서도 빠집니다.")' in src
+2
View File
@@ -472,7 +472,9 @@ export const ui_locales_b2 = {
"측구를 생성한 경우에만 형식을 선택할 수 있습니다.",
"Available only when a ditch is created.",
],
B06_Notice_FillSlope: ["성토 5m↑", "Fill slope >5m"],
B06_Notice_PaveSuggested: ["포장 권장", "Pavement recommended"],
B06_Notice_Berm: ["소단 검토", "Berm review"],
B06_Cross_Columns: ["가로 개수", "Per row"],
B06_Design_Detail: ["상세", "Detail"],
B06_Design_Paved_Suggested: [