feat(B05): 노면공 시작 · 종료 측점 · 길이 출력 · 기준 · 전/후 없음(87-1)

- 노면공 명세 byRange: 위치 줄 = 시작 · 종료 측점만(기준 측점 칸 없음 · 알약 = 시점) · 상세 내용 = 포장사양 → 길이(시작~종료 출력) → 높이
- 포장사양 선택지 콘크리트 · 아스콘 · 쇄석 · 높이 기본 0.20 m
- 저장 꼴: placement interval · start_m · end_m · options.pave_spec · height_m · 옛 length_m · before_m · after_m 은 [수정] 저장 때 뺌
- 구조물정의 판 20(판_19 이력) · 노면공 길이 · 전 · 후 옵션 빼고 height_m 기본값
- 시험: test_52_10 맞춤

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ
This commit is contained in:
2026-10-08 12:03:38 +09:00
co-authored by Claude Sonnet 5.5
parent e25b9ddf2f
commit fc36f0ae4e
11 changed files with 3879 additions and 31 deletions
@@ -161,6 +161,22 @@ export function buildStructuresForm(options: {
optionRow,
{ get: groupBoxes.get, pick: groupBoxes.pick, clear: groupBoxes.clear, set: groupBoxes.setOne },
options.onFacilityChange,
// 노면공 길이 칸(87-1) = 시작~종료 출력 — 못 셈이면 빈 글(「—」)
(_c, item) => {
if (item.id !== "length") return "";
const step = options.getInterval();
const start = startFields.read(step, false);
const end = endFields.read(step, false);
return start !== null && end !== null && end > start
? String(Number((end - start).toFixed(2)))
: "";
},
);
// 시작 · 종료를 고치면 길이 출력을 다시 그림
[startFields, endFields].forEach((fields) =>
[fields.station, fields.remainder].forEach((input) =>
input.addEventListener("change", () => structureSpec.refresh()),
),
);
groupBoxes.watch(structureSpec.paintLinks);
const staleBox = document.createElement("fieldset");
@@ -56,6 +56,7 @@ import {
DEFAULT_INTERVAL_LENGTH_M,
groupOptions,
hasComputedRange,
isRangeOnly,
typeOptions,
readBeforeOption,
readLengthOption,
@@ -238,7 +239,7 @@ export function createStructuresSection(
// [?] 설명서가 먼저 여는 항목 = `구조물_<종류 이름>.md`(54-2 · 종류 안내 글은 그 파일로 옮김)
root.dataset.helpTopic = type ? `구조물_${type.name}` : "";
// 정의가 기준 측점을 끈 종류(48-3) — 기준 측점이 곧 위치인 종류(점형 · 범위 계산)는 늘 보임
anchorFields.wrap.hidden = isInterval && !showsAnchor(type!);
anchorFields.wrap.hidden = isInterval && (!showsAnchor(type!) || isRangeOnly(type));
anchorFields.wrap.querySelector("span")!.textContent = "기준 측점"; // 빈 칸 안내 없음(54-43)
syncRangeDisplay();
}
@@ -314,7 +314,10 @@ export async function commit(ctx: StructuresCommitContext, live = false): Promis
return;
}
// 기준 측점(마킹 위치) — 비우면 시작 측점. 시작~종료 밖은 서버도 거절한다.
anchor = ctx.anchorFields.read(step, false) ?? start;
// 범위 직접 입력형(노면공 · 87-1) = 기준 칸 없음 → 늘 시점.
anchor = STRUCTURE_SPECS[type.type_id]?.byRange
? start
: (ctx.anchorFields.read(step, false) ?? start);
if (anchor < start || anchor > end) {
ctx.anchorFields.station.classList.add("is-invalid");
ctx.anchorFields.station.focus();
@@ -20,6 +20,7 @@ import {
} from "./B05_Profile_Api_Structures";
import type { DesignSelection, ResolvedSelection } from "../A00_Common/design/design_selection";
import type { GroupItems, SummaryItem } from "../common_util/common_util_summary_item";
import { STRUCTURE_SPECS } from "../A00_Common/design/design_structure_pipe";
/** 구조물군 표시 이름. 리스트 기호(A~G)만으로는 무엇인지 알기 어렵다.
* 우클릭 메뉴(종단그래프·배수유역도)도 같은 이름을 쓴다(2026-08-18 일원화). */
@@ -209,6 +210,10 @@ export function readBeforeOption(inputs: OptionEntry[]): number {
return length !== null && before > length ? length : before;
}
/** 시작 · 종료 측점만 받는 명세 종류인가(노면공 · 87-1) — 기준 측점 칸 없음 · 길이 출력. */
export const isRangeOnly = (type: { type_id: string } | null): boolean =>
!!type && !!STRUCTURE_SPECS[type.type_id]?.byRange;
/** 구간을 직접 입력하지 않는 타입인가 — C군 사면안정 5종처럼 B05 길이 옵션이
* 있으면 시작·종료 입력 칸을 없애고 기준 측점 + 길이로 범위를 계산해 보여만
* 준다(2026-08-19 사용자 지시). */
@@ -217,6 +222,7 @@ export function hasComputedRange(type: StructureType | null): boolean {
!!type &&
!type.managed_by &&
type.placement === "interval" &&
!isRangeOnly(type) &&
type.options.some((option) => option.key === "length_m" && isB05Option(option))
);
}
@@ -12,6 +12,7 @@ import type { StructureType } from "./B05_Profile_Api_Structures";
import {
createSpecForm,
type SpecForm,
type SpecFormHooks,
type SpecLinks,
} from "../A00_Common/design/design_structure_form";
import { STRUCTURE_SPECS } from "../A00_Common/design/design_structure_pipe";
@@ -29,8 +30,9 @@ export function createStructureSpecForm(
optionRow: HTMLElement,
links: SpecLinks,
onChange: () => void,
auto?: SpecFormHooks["auto"],
): SpecForm {
const form = createSpecForm(links, { onChange });
const form = createSpecForm(links, { onChange, auto });
form.root.hidden = true;
forms.set(optionRow, form);
const lock = (): void => {
@@ -49,7 +51,7 @@ export function createStructureSpecForm(
/** 정의 옵션 칸으로 안 그리는 키 — 명세 키 · 명세가 뺀 옛 키. */
export function specOwnedKeys(type: StructureType | null): Set<string> {
const spec = structureSpecOf(type);
return new Set(spec ? [...specKeys(spec).keys, ...spec.retired] : []);
return new Set(spec ? [...specKeys(spec).keys, ...spec.retired, ...(spec.dropped ?? [])] : []);
}
/** 종류 · 값으로 명세 폼을 다시 그림(명세 없는 종류면 가림). */
@@ -85,5 +87,5 @@ export function specRenderedKeys(
if (!spec) return [];
const { keys, prefixes } = specKeys(spec);
const own = Object.keys(previous ?? {}).filter((key) => prefixes.some((p) => key.startsWith(p)));
return [...keys, ...own];
return [...keys, ...own, ...(spec.dropped ?? [])];
}