From 0c22b7d4a165a9cc6d8f53625928756d721d0d09 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 9 Oct 2026 14:49:06 +0900 Subject: [PATCH] =?UTF-8?q?feat(B05):=20=EB=85=B8=EB=A9=B4=EA=B3=B5=20?= =?UTF-8?q?=ED=99=95=ED=8F=AD=20=EA=B5=AC=EA=B0=84=20=EB=8B=A8=EC=B6=94?= =?UTF-8?q?=EC=97=90=20=ED=99=95=ED=8F=AD=20=EB=A9=B4=EC=A0=81=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C(138-2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - wideningArea — 측점 사이 사다리꼴 합 · 구간 앞뒤 확폭 0 측점까지 · 노면공 범위로 자름 - 단추 오른쪽 끝 「n.nn ㎡」 · 끈 구간 회색 · 수량엔 안 넣음 - test_87 면적 경우 셋 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JywHxQuWPk6UYXTebyrBoU --- .../design/design_structure_widening.ts | 29 +++++++++++++++++++ .../B05_Profile_UI_Structures_Widening.ts | 19 ++++++++++-- .../B05_Profile_UI_Style_Structures.css | 17 +++++++++++ resources/tester/helper_87_widening.cjs | 13 +++++++++ resources/tester/test_87_widening_runs.py | 16 ++++++++++ 5 files changed, 91 insertions(+), 3 deletions(-) diff --git a/A00_Common/design/design_structure_widening.ts b/A00_Common/design/design_structure_widening.ts index 6b841ea38..5e73e062b 100644 --- a/A00_Common/design/design_structure_widening.ts +++ b/A00_Common/design/design_structure_widening.ts @@ -63,6 +63,35 @@ export function wideningRuns( return runs; } +/** + * 확폭 면적(㎡ · 138-2 · 표시만) — 확폭(좌 + 우) 측점 사이 사다리꼴 합 Σ(wᵢ + wᵢ₊₁)/2 × Δ. + * 구간 앞뒤 이웃 측점(확폭 0)까지 이어 붙이고 · 노면공 범위 `[start, end]` 밖은 잘라 냄(자른 끝은 직선 보간). + */ +export function wideningArea( + stations: readonly WideningStation[], + run: WideningRun, + range: { start: number; end: number }, +): number { + const sorted = [...stations].sort((a, b) => a.chainage_m - b.chainage_m); + const first = sorted.findIndex((s) => Math.abs(s.chainage_m - run.start_m) <= WIDENING_TOL_M); + const last = sorted.findIndex((s) => Math.abs(s.chainage_m - run.end_m) <= WIDENING_TOL_M); + if (first < 0 || last < 0) return 0; + const points = sorted + .slice(Math.max(first - 1, 0), last + 2) + .map((s) => [s.chainage_m, wideningOf(s)] as const); + let area = 0; + for (let i = 0; i + 1 < points.length; i += 1) { + const [x0, w0] = points[i]; + const [x1, w1] = points[i + 1]; + const a = Math.max(x0, range.start); + const b = Math.min(x1, range.end); + if (b <= a || x1 <= x0) continue; + const at = (x: number): number => w0 + ((w1 - w0) * (x - x0)) / (x1 - x0); + area += ((at(a) + at(b)) / 2) * (b - a); + } + return area; +} + const round = (value: number): number => Number(value.toFixed(2)); /** 저장 글 → 끈 구간 쌍. 틀린 조각은 버림. */ diff --git a/B05_Profile/B05_Profile_UI_Structures_Widening.ts b/B05_Profile/B05_Profile_UI_Structures_Widening.ts index b649eb18f..3ef019b40 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Widening.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Widening.ts @@ -10,6 +10,7 @@ import { isRunOff, offForRuns, toggleRun, + wideningArea, wideningRuns, type WideningRun, } from "../A00_Common/design/design_structure_widening"; @@ -37,18 +38,24 @@ export function createWideningBox(hooks: { let off = ""; /** 지금 범위의 구간들 — 횡단 자료 · 범위가 없으면 null(+ 안내 글). */ - const runs = (): { list: WideningRun[] | null; note: string } => { + const runs = (): { + list: WideningRun[] | null; + note: string; + area?: (run: WideningRun) => number; + } => { const sections = pipeSections(); if (!sections?.length) return { list: null, note: "횡단 자료 없음" }; const { start, end } = hooks.range(); if (start === null || end === null || end <= start) return { list: null, note: "시작 · 종료 측점을 넣으면 보임" }; const list = wideningRuns(sections, start, end); - return { list, note: list.length ? "" : "범위 안 확폭 없음" }; + // 확폭 면적(138-2) — 표시만 · 수량엔 안 넣음 + const area = (run: WideningRun) => wideningArea(sections, run, { start, end }); + return { list, note: list.length ? "" : "범위 안 확폭 없음", area }; }; const paintBody = (): void => { - const { list, note } = runs(); + const { list, note, area } = runs(); if (!list?.length) { const text = document.createElement("span"); text.className = "b05-structure__widening-note"; @@ -68,6 +75,12 @@ export function createWideningBox(hooks: { const from = stationLabel(run.start_m, step); const to = stationLabel(run.end_m, step); button.textContent = `${from === to ? from : `${from} ~ ${to}`} · 확폭 ${Number(run.widening_m.toFixed(2))} m`; + const size = area?.(run); + const areaText = document.createElement("span"); + areaText.className = "b05-structure__widening-area"; + areaText.textContent = + size !== undefined && Number.isFinite(size) ? `${size.toFixed(2)} ㎡` : ""; + button.append(areaText); button.title = on ? "확폭 포함 — 누르면 규격 폭만" : "규격 폭만 — 누르면 확폭 포함"; button.addEventListener("click", () => { off = toggleRun(list, off, run); diff --git a/B05_Profile/B05_Profile_UI_Style_Structures.css b/B05_Profile/B05_Profile_UI_Style_Structures.css index bb0919662..a322b7cc9 100644 --- a/B05_Profile/B05_Profile_UI_Style_Structures.css +++ b/B05_Profile/B05_Profile_UI_Style_Structures.css @@ -606,3 +606,20 @@ .b05-structure__widening [data-widening-run][aria-pressed="true"]::before { content: "✓ "; } + +/* 확폭 면적(138-2) — 단추 안 오른쪽 끝 · 끈 구간 회색 */ +.b05-structure__widening [data-widening-run] { + display: flex; + align-items: center; + column-gap: 4px; +} + +.b05-structure__widening-area { + margin-left: auto; + padding-left: var(--spacing-8, 8px); + white-space: nowrap; +} + +.b05-structure__widening [aria-pressed="false"] .b05-structure__widening-area { + color: var(--color-text-secondary); +} diff --git a/resources/tester/helper_87_widening.cjs b/resources/tester/helper_87_widening.cjs index 4aea3aeda..5f7a53262 100644 --- a/resources/tester/helper_87_widening.cjs +++ b/resources/tester/helper_87_widening.cjs @@ -50,5 +50,18 @@ const out = { toggledOn: w.toggleRun(runs, off, runs[0]), stale: w.offForRuns(runs, "20-60,0-5,100-130"), empty: w.offForRuns([], "20-60"), + // 확폭 면적(138-2) — 앞뒤 확폭 0 측점까지 사다리꼴 · 범위로 자름 + area: runs.map((r) => w.wideningArea(stations, r, { start: 0, end: 140 })), + areaClipped: w.wideningArea(stations, runs[0], { start: 30, end: 140 }), + areaSingle: w.wideningArea( + [at(0), at(20, 1, 0), at(40)], + { start_m: 20, end_m: 20, widening_m: 1 }, + { start: 0, end: 40 }, + ), + areaMissing: w.wideningArea( + stations, + { start_m: 999, end_m: 999, widening_m: 1 }, + { start: 0, end: 140 }, + ), }; process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/test_87_widening_runs.py b/resources/tester/test_87_widening_runs.py index 05a91b85d..e0bdf026f 100644 --- a/resources/tester/test_87_widening_runs.py +++ b/resources/tester/test_87_widening_runs.py @@ -58,3 +58,19 @@ def test_끈_구간_글_왕복(out): def test_안_맞는_끈_값은_무시_저장_때_빠짐(out): assert out["stale"] == "20-60" assert out["empty"] == "" + + +def test_확폭_면적_사다리꼴_앞뒤_0_측점까지(out): + # 0~20: (0+0.3)/2×20=3 · 20~40: 10.5 · 40~60: 11.5 · 60~80: 4 → 29 + # 80~100: 5 · 100~120: 11 · 120~140: 6 → 22 + assert [round(a, 6) for a in out["area"]] == [29.0, 22.0] + + +def test_확폭_면적_노면공_범위로_자름(out): + # 30 에서 자름: w(30)=0.525 → (0.525+0.75)/2×10=6.375 + 11.5 + 4 = 21.875 + assert round(out["areaClipped"], 6) == 21.875 + + +def test_한_측점_구간도_면적_자료_없으면_0(out): + assert round(out["areaSingle"], 6) == 20.0 # (0+1)/2×20 × 2 + assert out["areaMissing"] == 0