diff --git a/B06_Section/B06_Section_UI_Cross_Design_Pick.ts b/B06_Section/B06_Section_UI_Cross_Design_Pick.ts index 9eb85b419..36575f17f 100644 --- a/B06_Section/B06_Section_UI_Cross_Design_Pick.ts +++ b/B06_Section/B06_Section_UI_Cross_Design_Pick.ts @@ -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` ); } diff --git a/B06_Section/B06_Section_UI_Cross_FillSlope_Notice.ts b/B06_Section/B06_Section_UI_Cross_FillSlope_Notice.ts index ce8a899b3..2ceacd67b 100644 --- a/B06_Section/B06_Section_UI_Cross_FillSlope_Notice.ts +++ b/B06_Section/B06_Section_UI_Cross_FillSlope_Notice.ts @@ -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, 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), + }, + ]); }, }; } diff --git a/B06_Section/B06_Section_UI_Cross_Notice_Chips.ts b/B06_Section/B06_Section_UI_Cross_Notice_Chips.ts new file mode 100644 index 000000000..648236959 --- /dev/null +++ b/B06_Section/B06_Section_UI_Cross_Notice_Chips.ts @@ -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(".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(); + }, + }; +} diff --git a/B06_Section/B06_Section_UI_Missing_Stations.ts b/B06_Section/B06_Section_UI_Missing_Stations.ts index 8031db761..ead81240e 100644 --- a/B06_Section/B06_Section_UI_Missing_Stations.ts +++ b/B06_Section/B06_Section_UI_Missing_Stations.ts @@ -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) { diff --git a/B06_Section/B06_Section_UI_Style_Cross.css b/B06_Section/B06_Section_UI_Style_Cross.css index 0effa7db6..8a0d5fdd7 100644 --- a/B06_Section/B06_Section_UI_Style_Cross.css +++ b/B06_Section/B06_Section_UI_Style_Cross.css @@ -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 { diff --git a/B06_Section/B06_Section_UI_Style_Cross_Controls.css b/B06_Section/B06_Section_UI_Style_Cross_Controls.css index f4fee5c15..778418876 100644 --- a/B06_Section/B06_Section_UI_Style_Cross_Controls.css +++ b/B06_Section/B06_Section_UI_Style_Cross_Controls.css @@ -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); diff --git a/resources/tester/test_137_design_pick.py b/resources/tester/test_137_design_pick.py index 0e17f6d7e..af7412b9a 100644 --- a/resources/tester/test_137_design_pick.py +++ b/resources/tester/test_137_design_pick.py @@ -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"} diff --git a/resources/tester/test_195_1_pick_label.py b/resources/tester/test_195_1_pick_label.py new file mode 100644 index 000000000..428c06d64 --- /dev/null +++ b/resources/tester/test_195_1_pick_label.py @@ -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("길이 ") diff --git a/resources/tester/test_196_1_b06_notice_chips.py b/resources/tester/test_196_1_b06_notice_chips.py new file mode 100644 index 000000000..7027d4783 --- /dev/null +++ b/resources/tester/test_196_1_b06_notice_chips.py @@ -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 "5m"], B06_Notice_PaveSuggested: ["포장 권장", "Pavement recommended"], + B06_Notice_Berm: ["소단 검토", "Berm review"], B06_Cross_Columns: ["가로 개수", "Per row"], B06_Design_Detail: ["상세", "Detail"], B06_Design_Paved_Suggested: [