Merge remote-tracking branch 'origin/dev' into sub_laptop_7
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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: [
|
||||
|
||||
Reference in New Issue
Block a user