fix(B06): 포장 단추 지움 · 포장 = 노면공 범위만 · 포장 권장 알림 줄(138-1)
- 서버 paved_at 저장값 인자 지움 · enforce_pavement_ranges 켬↔끔 양쪽 · 단건 계산도 범위 규칙 - 브라우저 재계산 pavedBySpecs(같은 규칙) · CARRY_INPUT_KEYS 에서 paved 뺌 - 카드 「⚠ 포장 권장」 배지 → 알림 줄 「⚠ 포장 권장 · n측점」 · 로케일 정리 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JywHxQuWPk6UYXTebyrBoU
This commit is contained in:
@@ -108,6 +108,14 @@ const PRESERVED_KEYS = [
|
||||
...SPOIL_KEYS,
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* 포장 = 노면공 · 물넘이 범위 안만(138-1) — 측점에 얹힌 제원(`pavement` · `ford_pavement`)이 곧 범위다.
|
||||
* 서버 짝 = `B06_Section_Router_Design.paved_at` · 저장값 · 카드 선택은 쓰지 않는다.
|
||||
*/
|
||||
export function pavedBySpecs(section: Pick<CrossSection, "pavement" | "ford_pavement">): boolean {
|
||||
return Boolean(section.pavement?.length) || Boolean(section.ford_pavement);
|
||||
}
|
||||
|
||||
function preserveUserFields(
|
||||
next: NonNullable<CrossSection["design"]>,
|
||||
previous: CrossSection["design"],
|
||||
@@ -293,7 +301,7 @@ function refreshLocally(input: CrossRefreshInput): number[] | null {
|
||||
ditchType:
|
||||
choice?.ditch_type ??
|
||||
(typeof design.ditch_type === "string" ? design.ditch_type : "standard"),
|
||||
paved: choice?.paved ?? Boolean(design.paved),
|
||||
paved: pavedBySpecs(section),
|
||||
standard,
|
||||
berm: bermAt(section.chainage_m),
|
||||
rockBoundaryOffsetM,
|
||||
|
||||
@@ -44,6 +44,8 @@ from B06_Section.B06_Section_Router_Design import (
|
||||
USER_TOUCHED_KEYS,
|
||||
ford_drop_at,
|
||||
ford_surface_drops,
|
||||
paved_at,
|
||||
pavement_ranges,
|
||||
stored_berm,
|
||||
stored_cut_slope,
|
||||
)
|
||||
@@ -640,7 +642,7 @@ async def compute_cross_section_design(
|
||||
section_mode=request.section_mode,
|
||||
ditch_side=request.ditch_side,
|
||||
ditch_type=request.ditch_type,
|
||||
paved=request.paved,
|
||||
paved=paved_at(request.chainage_m, pavement_ranges(project_root)),
|
||||
standard=request.standard_cross_section,
|
||||
rock_boundary_offset_m=request.rock_boundary_offset_m,
|
||||
two_stage_slope=request.two_stage_slope,
|
||||
|
||||
@@ -110,12 +110,17 @@ def ford_drop_at(chainage_m: float, drops: list[tuple[float, float]]) -> float:
|
||||
return 0.0
|
||||
|
||||
|
||||
def paved_at(chainage_m: float, ranges: list[tuple[float, float]], stored: Any = None) -> bool:
|
||||
"""이 측점을 포장으로 볼 것인가 — 구간 안이면 강제, 밖이면 사용자 저장값(기본 비포장)."""
|
||||
for start, end in ranges:
|
||||
if start - 1e-9 <= chainage_m <= end + 1e-9:
|
||||
return True
|
||||
return bool(stored) if isinstance(stored, bool) else False
|
||||
# 범위 끝 허용오차(m) — 브라우저 짝(`common_util_structure_pavement` · `_culvert_sets`)과 같은 값.
|
||||
_PAVED_EDGE_TOLERANCE_M = 0.02
|
||||
|
||||
|
||||
def paved_at(chainage_m: float, ranges: list[tuple[float, float]]) -> bool:
|
||||
"""이 측점을 포장으로 볼 것인가 — 노면공 · 물넘이 범위 안만 포장, 밖은 비포장(저장값 무시).
|
||||
|
||||
브라우저 짝 = `B06_Section_Cross_Refresh.ts` `pavedBySpecs`(같은 규칙 · 138-1).
|
||||
"""
|
||||
tol = _PAVED_EDGE_TOLERANCE_M
|
||||
return any(start - tol <= chainage_m <= end + tol for start, end in ranges)
|
||||
|
||||
|
||||
# 사용자 조작값 — 다시 계산해도 그대로 승계한다. **이 목록이 서버의 유일한 한 벌이다.**
|
||||
@@ -152,7 +157,6 @@ CARRY_INPUT_KEYS = (
|
||||
"ground_type",
|
||||
"ditch_type",
|
||||
"ditch_choice",
|
||||
"paved",
|
||||
"two_stage_slope",
|
||||
"rock_boundary_offset_m",
|
||||
"cut_slope_ratio_user",
|
||||
@@ -178,23 +182,23 @@ def enforce_pavement_ranges(
|
||||
project_root: Path,
|
||||
standard: dict[str, Any] | None = None,
|
||||
) -> int:
|
||||
"""포장 구간·물넘이 범위 안 측점을 포장으로 맞춘다 — 저장분이 비포장이어도 그렇다.
|
||||
"""포장 = 노면공 · 물넘이 범위 안만 — 저장분과 다르면 켬 · 끔 양쪽으로 맞춘다(138-1).
|
||||
|
||||
사용자가 구간을 지정하면 그 안 횡단도에 **자동 반영**된다(2026-08-28 사용자 확정).
|
||||
범위 밖 옛 `paved=true`(지운 노면공 · 옛 카드 단추)는 비포장으로 되돌린다.
|
||||
포장 여부는 횡단경사·포장층을 바꾸므로 플래그만 갈아 끼우지 않고 다시 계산한다.
|
||||
사용자 조작값(반폭·구조물 조정)은 그대로 승계한다.
|
||||
"""
|
||||
ranges = pavement_ranges(project_root)
|
||||
if not ranges:
|
||||
return 0
|
||||
ford_drops = ford_surface_drops(project_root)
|
||||
changed = 0
|
||||
for section in cross_sections:
|
||||
design = section.get("design")
|
||||
if not isinstance(design, dict) or design.get("paved"):
|
||||
if not isinstance(design, dict):
|
||||
continue
|
||||
chainage = float(section.get("chainage_m", 0.0))
|
||||
if not paved_at(chainage, ranges):
|
||||
wanted = paved_at(chainage, ranges)
|
||||
if bool(design.get("paved")) == wanted:
|
||||
continue
|
||||
try:
|
||||
recomputed = compute_cross_design(
|
||||
@@ -204,7 +208,7 @@ def enforce_pavement_ranges(
|
||||
section_mode=str(design.get("section_mode") or "left_cut"),
|
||||
ditch_side=design.get("ditch_side"),
|
||||
ditch_type=str(design.get("ditch_type") or "standard"),
|
||||
paved=True,
|
||||
paved=wanted,
|
||||
standard=standard,
|
||||
rock_boundary_offset_m=design.get(
|
||||
"rock_boundary_offset_m", STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M
|
||||
@@ -449,7 +453,7 @@ def recompute_designs_for_alignment(
|
||||
section_mode=str(stored.get("section_mode") or modes.get(key, "left_cut")),
|
||||
ditch_side=stored.get("ditch_side"),
|
||||
ditch_type=str(stored.get("ditch_type") or "standard"),
|
||||
paved=paved_at(chainage, paved_ranges, stored.get("paved")),
|
||||
paved=paved_at(chainage, paved_ranges),
|
||||
standard=standard,
|
||||
rock_boundary_offset_m=session_offsets.get(
|
||||
key,
|
||||
@@ -527,7 +531,7 @@ def compute_default_designs(
|
||||
section_mode=default_modes.get(round(chainage_m, 3), "left_cut"),
|
||||
ditch_type=str(user.get("ditch_type") or "standard"),
|
||||
# 포장은 사용자 구간 지정만 켠다 — 경사 제안은 경고로만 남는다(2026-08-28).
|
||||
paved=paved_at(chainage_m, paved_ranges, user.get("paved")),
|
||||
paved=paved_at(chainage_m, paved_ranges),
|
||||
standard=standard,
|
||||
rock_boundary_offset_m=(
|
||||
STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M if rock_offset is None else rock_offset
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* =============================================================================
|
||||
* B06_Section_UI_Cross_Design.ts
|
||||
* 측점 표준횡단 설계 지정 컨트롤(지반유형·단면유형·측구위치·측구형식·포장)과
|
||||
* 측점 표준횡단 설계 지정 컨트롤(지반유형·단면유형·측구위치·측구형식)과
|
||||
* 설계선·암 경계선·포장층 오버레이.
|
||||
*
|
||||
* 카드 헤더 아래에 세그먼트 버튼을 배치하고, 선택이 바뀌면 onChange로 계산을
|
||||
@@ -439,25 +439,13 @@ export function buildDesignControls(
|
||||
!rockCut,
|
||||
L("B06_Design_Disabled_RockCut"),
|
||||
);
|
||||
// 포장 토글: 라벨 삭제, 버튼명 활성="포장"/비활성="비포장"(컬러 유지) — E-7.
|
||||
const paved = toggle("", state.paved, L("B06_Design_Paved_On"), L("B06_Design_Paved_Off"), () => {
|
||||
state.paved = !state.paved;
|
||||
emit();
|
||||
});
|
||||
// B05 법정 경사 분석이 포장을 제안한 측점은 근거 문구를 배지·툴팁으로 표기한다.
|
||||
if (design?.pavement_suggested) {
|
||||
paved.button.title = L("B06_Design_Paved_Suggested");
|
||||
const badge = document.createElement("span");
|
||||
badge.className = "b06-design__paved-badge";
|
||||
badge.textContent = "⚠";
|
||||
badge.title = L("B06_Design_Paved_Suggested");
|
||||
paved.wrap.append(badge);
|
||||
}
|
||||
// 포장 단추는 없다(138-1) — 포장 = 노면공 · 물넘이 범위만(`pavedBySpecs`) · 「포장 권장」은 알림 줄.
|
||||
// `state.paved` 는 저장값 그대로 실어 보낸다(재계산이 범위로 다시 정함).
|
||||
|
||||
// 오버플로 드롭다운(N-2-2): 컨트롤이 카드 폭에서 한 행을 넘치면 뒤쪽부터 우측 "⋯"
|
||||
// 드롭다운(세로 배치)으로 옮겨 다음 행으로 밀리는 것을 막는다. 경사 방향은 항상 인라인,
|
||||
// 이동 우선순위(뒤에서부터): 포장 → 복합경사 → 측구형식 → 측구 토글.
|
||||
const moveable = [ditchToggle.wrap, ditchTypeSeg, twoStage.wrap, paved.wrap];
|
||||
// 이동 우선순위(뒤에서부터): 복합경사 → 측구형식 → 측구 토글.
|
||||
const moveable = [ditchToggle.wrap, ditchTypeSeg, twoStage.wrap];
|
||||
const more = document.createElement("details");
|
||||
more.className = "b06-design__more";
|
||||
const moreSummary = document.createElement("summary");
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* (2026-09-14 브레인 승인 (나)). 판정은 `_Cross_FillSlope_Warn`, 여기는 그리기만.
|
||||
* 측점을 누르면 그 카드로 간다. 경고만 — 구조물을 세우거나 값을 바꾸지 않는다.
|
||||
* ⭐ 2026-09-15 — 그 밑에 사면길이 3m 초과 **소단 검토 정보 한 줄**(경고 모양 아님 · 브레인 ③).
|
||||
* ⭐ 138-1 — 「⚠ 포장 권장 · n측점」 줄(B05 법정 경사 제안 · 포장 범위 밖 측점 · 옛 카드 배지 자리).
|
||||
* ========================================================================== */
|
||||
|
||||
import type { CrossSection } from "./B06_Section_Api_Fetch";
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
type FillSlopeWarning,
|
||||
} from "./B06_Section_UI_Cross_FillSlope_Warn";
|
||||
import { L, stationLabel } from "./B06_Section_UI_Section_Common";
|
||||
import { pavedBySpecs } from "./B06_Section_Cross_Refresh";
|
||||
|
||||
export interface FillSlopeNotice {
|
||||
root: HTMLElement;
|
||||
@@ -46,7 +48,8 @@ function noticeBlock(className: string): {
|
||||
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(" · ")}`;
|
||||
button.textContent =
|
||||
`${stationLabel(section.chainage_m, stationInterval)} ${parts.join(" · ")}`.trim();
|
||||
button.dataset.stationId = section.station_id;
|
||||
return button;
|
||||
}),
|
||||
@@ -59,7 +62,9 @@ export function createFillSlopeNotice(onPick: (stationId: string) => void): Fill
|
||||
const root = document.createElement("div");
|
||||
const warn = noticeBlock("b06-section__notice");
|
||||
const info = noticeBlock("b06-section__notice b06-section__notice--info");
|
||||
root.append(warn.root, info.root);
|
||||
const pave = noticeBlock("b06-section__notice");
|
||||
pave.root.title = L("B06_Design_Paved_Suggested");
|
||||
root.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);
|
||||
@@ -73,6 +78,13 @@ export function createFillSlopeNotice(onPick: (stationId: string) => void): Fill
|
||||
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),
|
||||
|
||||
@@ -404,13 +404,6 @@
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
/* 포장 제안 배지: B05 법정 경사 분석이 포장을 권장한 측점 표시 */
|
||||
.b06-design__paved-badge {
|
||||
font-size: 0.72rem;
|
||||
color: var(--color-warning);
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
/* 세월교 구체(2026-08-25) — 바닥판은 콘크리트. 측벽은 ㄴ형 집수정 형상을 그대로
|
||||
쓰므로 `.b06-chart__culvert-basin` 색을 공유한다. */
|
||||
.b06-chart__ford-slab {
|
||||
|
||||
@@ -25,7 +25,8 @@ def test_이월은_사용자_입력만() -> None:
|
||||
"ditch_side",
|
||||
):
|
||||
assert result_key not in CARRY_INPUT_KEYS
|
||||
assert {"ground_type", "paved", "ditch_choice", "cut_slope_ratio_user"} <= set(CARRY_INPUT_KEYS)
|
||||
assert {"ground_type", "ditch_choice", "cut_slope_ratio_user"} <= set(CARRY_INPUT_KEYS)
|
||||
assert "paved" not in CARRY_INPUT_KEYS # 포장 = 노면공 범위로 새로 정함(138-1)
|
||||
|
||||
|
||||
def test_체인이_옛_설계를_통째로_옮기지_않는다() -> None:
|
||||
|
||||
@@ -126,17 +126,42 @@ def test_missing_truth_files_are_not_an_error(tmp_path):
|
||||
# ── 포장 판정 규칙 ─────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_outside_range_keeps_user_value_and_defaults_unpaved():
|
||||
"""경사 제안은 더 이상 포장을 켜지 않는다 — 구간 밖은 사용자 값(없으면 비포장)."""
|
||||
def test_outside_range_is_unpaved():
|
||||
"""포장 = 노면공 · 물넘이 범위만(138-1) — 밖은 비포장(옛 저장값 인자 없음)."""
|
||||
ranges = [(10.0, 20.0)]
|
||||
assert paved_at(30.0, ranges) is False
|
||||
assert paved_at(30.0, ranges, True) is True
|
||||
assert paved_at(30.0, ranges, False) is False
|
||||
assert paved_at(15.0, ranges) is True
|
||||
assert paved_at(20.02, ranges) is True # 끝 허용오차 = 브라우저 짝 0.02 m
|
||||
assert paved_at(20.03, ranges) is False
|
||||
assert paved_at(15.0, []) is False
|
||||
|
||||
|
||||
def test_inside_range_overrides_user_off_switch():
|
||||
"""구간 안은 강제 포장 — 물넘이는 콘크리트 노면이고 지정 구간은 자동 반영이다."""
|
||||
assert paved_at(15.0, [(10.0, 20.0)], False) is True
|
||||
def test_enforce_turns_paved_on_and_off(monkeypatch):
|
||||
"""재계산은 켬↔끔 양쪽 — 범위 밖 옛 `paved=true` 는 비포장으로 되돌림(138-1)."""
|
||||
from pathlib import Path
|
||||
|
||||
from B06_Section import B06_Section_Router_Design as design_mod
|
||||
|
||||
monkeypatch.setattr(design_mod, "pavement_ranges", lambda root: [(0.0, 50.0)])
|
||||
monkeypatch.setattr(design_mod, "ford_surface_drops", lambda root: [])
|
||||
monkeypatch.setattr(design_mod, "design_elevation_from_longitudinal", lambda lon, ch: 100.0)
|
||||
monkeypatch.setattr(design_mod, "curve_widening_args", lambda section: {})
|
||||
monkeypatch.setattr(
|
||||
design_mod, "compute_cross_design", lambda samples, elev, **kw: {"paved": kw["paved"]}
|
||||
)
|
||||
sections = [
|
||||
{"chainage_m": 20.0, "design": {"paved": False}}, # 범위 안 · 끔 → 켬
|
||||
{"chainage_m": 80.0, "design": {"paved": True}}, # 범위 밖 · 켬 → 끔
|
||||
{"chainage_m": 30.0, "design": {"paved": True}}, # 그대로
|
||||
{"chainage_m": 90.0, "design": {"paved": False}}, # 그대로
|
||||
]
|
||||
assert design_mod.enforce_pavement_ranges({}, sections, Path("."), None) == 2
|
||||
assert [s["design"]["paved"] for s in sections] == [True, False, True, False]
|
||||
|
||||
# 노면공을 다 지워도(범위 없음) 옛 켬은 꺼진다
|
||||
monkeypatch.setattr(design_mod, "pavement_ranges", lambda root: [])
|
||||
assert design_mod.enforce_pavement_ranges({}, sections, Path("."), None) == 2
|
||||
assert not any(s["design"]["paved"] for s in sections)
|
||||
|
||||
|
||||
# ── 독립 기슭막이 ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -447,9 +447,6 @@ export const ui_locales_b2 = {
|
||||
B06_Design_DitchType_Legend: ["측구형식", "Ditch type"],
|
||||
B06_Design_DitchType_Standard: ["일반", "Standard"],
|
||||
B06_Design_DitchType_LType: ["L형", "L-type"],
|
||||
B06_Design_Paved_Legend: ["포장", "Pavement"],
|
||||
B06_Design_Paved_On: ["포장", "Paved"],
|
||||
B06_Design_Paved_Off: ["비포장", "Unpaved"],
|
||||
B06_Design_TwoStage_Legend: ["2단계 경사", "Two-stage slope"],
|
||||
B06_Design_TwoStage_On: ["복합경사", "Compound slope"],
|
||||
B06_Design_TwoStage_Off: ["단경사", "Single slope"],
|
||||
@@ -475,6 +472,7 @@ export const ui_locales_b2 = {
|
||||
"측구를 생성한 경우에만 형식을 선택할 수 있습니다.",
|
||||
"Available only when a ditch is created.",
|
||||
],
|
||||
B06_Notice_PaveSuggested: ["포장 권장", "Pavement recommended"],
|
||||
B06_Design_Paved_Suggested: [
|
||||
"종단경사 법정 상한 초과 — 포장 권장 (임도설치 및 관리 등에 관한 규정 별표 1-2)",
|
||||
"Grade exceeds legal limit — pavement recommended (Forest Road Regulation, Annex 1-2)",
|
||||
|
||||
Reference in New Issue
Block a user