feat(help): B02~B09 좌측 패널 [?] 설명서 모달 · 패널 안 설명 글을 파일로 옮김(54-2)

- [?] = 좌측 패널 아래 단추 묶음 맨 왼쪽(공용 한 벌 · B02 는 아래 단추 줄 · B08·B09 는 [?] 줄만)
- 모달 = 왼쪽 차례 · 오른쪽 글 · 고른 구조물 종류 · 연 상자 항목 먼저
- 글 = resources/help/<B0N>/*.md · 서버가 열 때마다 읽음(파일만 고치면 반영) · 설명서는 뼈대
- 옮김 = B03 안내 셋 · B05 구조물 저장 규칙 · 측구 주인 · 기슭막이 근거 · 칸 기본값 근거 · B06 표준 횡단면 두 줄

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NPJGtfXo6GMbcpiboQC4uK
This commit is contained in:
2026-10-06 10:17:07 +09:00
co-authored by Claude Opus 5.5
parent bcc3cbb407
commit 73e8d4ea56
49 changed files with 622 additions and 242 deletions
@@ -373,16 +373,11 @@ export function createFacilityOptionsForm(
// 옛 월류 폭 · 높이 · 바닥 경사 · 두께 · 길이 칸은 뺌(지난 정의 값) ──
const paveForm = createSpecForm(specLinks, { onChange: () => emit() });
// 10-A ⑲ 저장 규칙을 폼 머리에 드러냄 — 병합은 `_Drainage_Facility_Merge`(2026-09-14 브레인 판정).
const saveNote = document.createElement("p");
saveNote.className = "b05-structure__owner-note";
saveNote.textContent =
"[저장]은 이 폼에 있는 칸만 바꿈 — 폼에 없는 칸(집계표·구조물도로 적은 값)은 그대로 둠 · 시설 종류를 바꾸면 새로 씀";
// 10-A ⑲ 저장 규칙 글은 [?] 설명서 `구조물_<시설>.md` 로 옮김(54-2) — 병합은 `_Drainage_Facility_Merge`.
// 세월교·물넘이 항목은 **관종·관경 바로 다음**에 둔다(2026-08-30 사용자 지시 2) —
// 월류 폭·높이 → 바닥 경사 → 수량 → 개략 단면 결과. 다른 시설에서는 전부 숨는다.
root.append(
saveNote,
pipeForm.root,
fordForm.root,
paveForm.root,
@@ -9,7 +9,6 @@
* ========================================================================== */
import { FORD_MANNING_N, FORD_SLOPE } from "@config/config_frontend";
import revetmentSabang from "../resources/data_masonry/revetment_sabang_2026-01-01.json";
/* ── 유입구 구조 ──────────────────────────────────────────────────────────── */
@@ -245,39 +244,18 @@ export interface RevetSideGroup {
slot: HTMLElement;
}
/** 독립 기슭막이 치수 근거 줄 — `revetment_sabang_*.json` 한 벌(B08 사유와 같은 파일 · 쪽·줄 번호).
* ⚠ 파일 판이 바뀌면 이 import 도 옮길 것 — 시험(`test_b05_revetment_sabang_basis`)이 최신 판을 대조. */
function sabangBasisLines(): string[] {
const { items, scope } = revetmentSabang;
const span = (values: number[], unit = ""): string => `${values[0]}~${values[1]}${unit}`;
return [
`높이 — ${items.height.rule}(${items.height.source}) · ${items.height.input}`,
`계획비탈 1:${span(items.face_slope.standard_ratio)}(${items.face_slope.source}) — 제안값은 품셈 표준경사`,
`둑마루 두께 ${span(items.crown_thickness.standard_m, "m")}(${items.crown_thickness.source}) — 콘크리트 기준 · 돌쌓기는 뒷길이 + 0.30 식`,
`뒷채움 조약돌 두께 — ${items.backfill_pebble.source}`,
scope,
];
}
export function createRevetSideGroup(keys: RevetmentKeys): RevetSideGroup {
const box = group("기슭막이");
const kind = document.createElement("select");
kind.replaceChildren(new Option("기슭막이", "기슭막이"));
// 높이는 기본값 없음 — 설계자 입력(계획홍수위 + 여유고 · 2026-09-14 브레인 판정).
const fields = createRevetmentFields(keys, { height: "" });
const basis = document.createElement("p");
basis.className = "b05-route__note";
basis.replaceChildren(
...sabangBasisLines().flatMap((line, index) =>
index ? [document.createElement("br"), line] : [line],
),
);
// 치수 근거 글(사방기술교본)은 [?] 설명서 `구조물_기슭막이.md` 로 옮김(54-2)
const slot = document.createElement("div");
slot.className = "b05-structure__adjust-slot";
box.body.append(
grid(labeled("구조", kind), fields.formField),
...fields.rows,
basis,
// 단 수는 여기 두지 않는다 — 조정창에서 옮겨 오는 [추가 기슭막이(단)] 행과
// 같은 값이라 두 벌이면 헷갈린다(2026-08-30 사용자).
slot,
@@ -9,23 +9,13 @@
import { chainageToStation, stationToChainage } from "./B05_Profile_Util_Station";
export function field(
labelText: string,
input: HTMLElement,
basis?: string | null,
): HTMLLabelElement {
export function field(labelText: string, input: HTMLElement): HTMLLabelElement {
const wrapper = document.createElement("label");
wrapper.className = "b05-route__field";
const caption = document.createElement("span");
caption.textContent = labelText;
// 칸 밑 근거 글(`default_basis`)은 [?] 설명서 `구조물_<종류>.md` 로 옮김(54-2)
wrapper.append(caption, input);
// 칸 밑 근거 한 줄 — 등록부 `default_basis`(기본값의 뜻 · 10-A 2026-09-14 사용자 확정).
if (basis) {
const note = document.createElement("small");
note.className = "b05-structure__basis";
note.textContent = basis;
wrapper.append(note);
}
return wrapper;
}
@@ -35,8 +35,6 @@ export interface StructuresFormElements {
body: HTMLElement;
groupSelect: HTMLSelectElement;
typeSelect: HTMLSelectElement;
/** 「제원·수량은 다른 화면이 냅니다」 안내 한 줄 — `design_owner` 가 있을 때만 보인다. */
ownerNote: HTMLElement;
startFields: StationFields;
anchorFields: StationFields;
endFields: StationFields;
@@ -113,13 +111,6 @@ export function buildStructuresForm(options: {
typeRow.className = "b05-structure__grid";
typeRow.append(field("구조물군", groupSelect), field("종류", typeSelect));
// 「제원·수량은 다른 화면이 냅니다」 안내 한 줄 — 목록 항목은 [측점][이름]만 적는 규칙이라
// (2026-08-18 사용자 지시) 표시는 이 폼에 둔다. 측구(옆도랑)가 그 경우다(2026-09-07 사용자:
// 「두되 표시만 해줘」). 문구는 레지스트리 `design_owner` 값으로 만든다.
const ownerNote = document.createElement("p");
ownerNote.className = "b05-structure__owner-note";
ownerNote.hidden = true;
// 옵션 칸은 타입마다 다르므로 선택할 때마다 새로 그린다.
const optionRow = document.createElement("div");
optionRow.className = "b05-structure__grid";
@@ -178,7 +169,6 @@ export function buildStructuresForm(options: {
body.append(
typeRow,
ownerNote,
positionRow,
summary.root, // 종류 단위 「구조물 조합」 잇기 — 기준 측점 바로 아래(PLAN 52-1 · 컨테이너 없는 종류)
positionDivider,
@@ -195,7 +185,6 @@ export function buildStructuresForm(options: {
body,
groupSelect,
typeSelect,
ownerNote,
startFields,
anchorFields,
endFields,
@@ -78,7 +78,6 @@ export function createStructuresSection(
body,
groupSelect,
typeSelect,
ownerNote,
startFields,
anchorFields,
endFields,
@@ -233,12 +232,8 @@ export function createStructuresSection(
startFields.wrap.hidden = !isInterval;
endFields.wrap.hidden = !isInterval;
primary.disabled = !type;
// 제원·수량 주인이 다른 화면인 타입은 그 사실을 폼에 적는다 — 목록에서 안 보이면
// 「측구가 왜 없지」로 헤매고, 그렇다고 수량에 넣으면 이중 계상이다(2026-09-07 사용자).
ownerNote.textContent = type?.design_owner
? `${type.design_owner}에서 관리 — 여기서 넣어도 제원·수량은 그쪽 값을 씁니다.`
: "";
ownerNote.hidden = !type?.design_owner;
// [?] 설명서가 먼저 여는 항목 = `구조물_<종류 이름>.md`(54-2 · 종류 안내 글은 그 파일로 옮김)
root.dataset.helpTopic = type ? `구조물_${type.name}` : "";
// 정의가 기준 측점을 끈 종류(48-3) — 기준 측점이 곧 위치인 종류(점형 · 범위 계산)는 늘 보임
anchorFields.wrap.hidden = isInterval && !showsAnchor(type!);
anchorFields.wrap.querySelector("span")!.textContent = isInterval
@@ -101,11 +101,7 @@ export function renderOptionFields(
// 정의 그룹 상자로 옮겨 갈 숫자 칸 = 명세 컨테이너와 같은 [-][숫자][+](52-18)
const grouped =
option.input === "number" && !!type.groups?.some((g) => g.options.includes(option.key));
const wrap = field(
label,
grouped ? stepper(input as HTMLInputElement, 0.1) : input,
option.default_basis,
);
const wrap = field(label, grouped ? stepper(input as HTMLInputElement, 0.1) : input);
if (grouped) wrap.htmlFor = input.id; // 라벨 대상 = 숫자 칸(첫 버튼으로 새지 않게)
wrap.dataset.optionKeys = option.key; // 옵션 그룹 상자가 이 표식으로 칸을 옮김(PLAN 46-3)
// 배치 격자(48-3) — 정의 `배치` 의 줄 · 칸 · 너비 · 없으면 흐름대로
@@ -215,13 +215,6 @@
line-height: 1.5;
}
/* 칸 밑 근거 한 줄 — 등록부 `default_basis`(10-A). 회색 작은 글씨. */
.b05-structure__basis {
color: var(--color-text-muted, #9aa1ad);
font-size: var(--text-caption, 12px);
line-height: 1.3;
}
/* 시작·기준·종료 측점 = 3행. 한 행은 [라벨][측점][+거리] 가로 배치
* (2026-08-17 사용자 지시 2). */
.b05-structure__position-row {