feat(B05): 기슭막이 보호공 삭제·바닥막이 부활·지형 기준 즉시 반영

- 기슭막이는 기슭(사면)을 막는 공정이라 그 아래 돌붙임이 붙을 자리가 아니다
  (2026-08-19 사용자 지시). 배관 유입·유출부와 독립 기슭막이에서 보호공
  (돌붙임 찰/메·도수로)·보호공 면적·도수로 폭 옵션을 전면 삭제.
- 바닥막이(bed_sill) 부활 — D군 점형, 형태(돌붙임 찰/메)·면적·높이
  (기본 메·10㎡·0.3m). 바닥 보호는 이 구조물이 담당한다.
- 독립 기슭막이(D4)를 시설 서브폼에서 떼어 C군과 같은 레지스트리 옵션 방식으로
  전환: 측점 범위 + 기준측점 전/후, 기본 돌쌓기(메)·높이 2.5·길이 10·전 5·후 5.
- 페이지 설정의 지형 구분·등급·최대 종단기울기를 바꾸면 안내문·placeholder와
  종단 그래프 위반 판정이 즉시 갱신된다(profilePanel.setGradeLimit 신설).
  계획선 자체 재산출은 서버 재계산 몫이라 건드리지 않는다.
- 지식DB 재점검(설계제원_총괄 §4·§6·§7): 종단기울기·종단곡선 반경 표는 일치.
  미해결 3건(폐지된 "지선" 선택지·등급↔설계속도 고정 매핑·최소 곡선반경 안내가
  종단곡선 반경)은 값을 임의로 바꾸지 않고 주석으로 남겼다 — 사용자 협의 대기.
- 검증: pytest 113 통과, tsc, 헤드 브라우저(D군 목록 바닥막이, 기슭막이 범위+
  전/후·보호공 없음, 배관 서브폼 보호공 라벨 소멸, 지형 변경 7%→10%·위반 재판정).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 19:12:37 +09:00
co-authored by Claude Fable 5
parent 0b1a66b8d1
commit 0cb772a351
7 changed files with 134 additions and 210 deletions
+66 -77
View File
@@ -83,32 +83,6 @@
"required": false,
"phase": "detail"
},
{
"key": "inlet_protection",
"label": "유입 보호공",
"input": "select",
"choices": ["돌붙임(찰)", "돌붙임(메)", "도수로"],
"default": "돌붙임(찰)",
"required": false
},
{
"key": "inlet_protection_area_m2",
"label": "유입 보호공 면적",
"input": "number",
"unit": "㎡",
"default": 10,
"required": false,
"phase": "detail"
},
{
"key": "inlet_protection_width_m",
"label": "유입 도수로 폭",
"input": "number",
"unit": "m",
"default": 1,
"required": false,
"phase": "detail"
},
{
"key": "outlet_type",
"label": "유출구 구조",
@@ -142,32 +116,6 @@
"default": 2.5,
"required": false,
"phase": "detail"
},
{
"key": "outlet_protection",
"label": "유출 보호공",
"input": "select",
"choices": ["돌붙임(찰)", "돌붙임(메)", "도수로"],
"default": "돌붙임(메)",
"required": false
},
{
"key": "outlet_protection_area_m2",
"label": "유출 보호공 면적",
"input": "number",
"unit": "㎡",
"default": 10,
"required": false,
"phase": "detail"
},
{
"key": "outlet_protection_width_m",
"label": "유출 도수로 폭",
"input": "number",
"unit": "m",
"default": 1,
"required": false,
"phase": "detail"
}
]
},
@@ -827,6 +775,46 @@
}
]
},
{
"type_id": "bed_sill",
"group": "D",
"name": "바닥막이",
"placement": "point",
"style": {
"color": "#7c5f3b",
"abbr": "바닥"
},
"drawing_views": ["profile", "cross_section", "quantity"],
"options": [
{
"key": "form",
"label": "형태",
"input": "select",
"choices": ["돌붙임(찰)", "돌붙임(메)"],
"default": "돌붙임(메)",
"required": false,
"phase": "b05"
},
{
"key": "area_m2",
"label": "면적",
"input": "number",
"unit": "㎡",
"default": 10,
"required": false,
"phase": "b05"
},
{
"key": "height_m",
"label": "높이",
"input": "number",
"unit": "m",
"default": 0.3,
"required": false,
"phase": "b05"
}
]
},
{
"type_id": "check_dam_small",
"group": "D",
@@ -865,9 +853,9 @@
"label": "형태",
"input": "select",
"choices": ["돌쌓기(찰)", "돌쌓기(메)", "콘크리트", "돌망태", "통나무·목재틀", "바자"],
"default": null,
"required": true,
"phase": "detail"
"default": "돌쌓기(메)",
"required": false,
"phase": "b05"
},
{
"key": "height_m",
@@ -875,34 +863,35 @@
"input": "number",
"unit": "m",
"default": 2.5,
"required": true,
"phase": "detail"
"required": false,
"phase": "b05"
},
{
"key": "protection",
"label": "보호공",
"input": "select",
"choices": ["돌붙임(찰)", "돌붙임(메)", "도수로"],
"default": "돌붙임(메)",
"required": false
},
{
"key": "protection_width_m",
"label": "도수로 폭",
"key": "length_m",
"label": "길이",
"input": "number",
"unit": "m",
"default": 1,
"required": false,
"phase": "detail"
},
{
"key": "protection_area_m2",
"label": "보호공 면적",
"input": "number",
"unit": "㎡",
"default": 10,
"required": false,
"phase": "detail"
"phase": "b05"
},
{
"key": "before_m",
"label": "기준측점 전",
"input": "number",
"unit": "m",
"default": 5,
"required": false,
"phase": "b05"
},
{
"key": "after_m",
"label": "기준측점 후",
"input": "number",
"unit": "m",
"default": 5,
"required": false,
"phase": "b05"
}
]
},
@@ -47,7 +47,6 @@ import {
OUTLET_REVET_KEYS,
putNumber,
REVET_COMMON_DEFAULTS,
STANDALONE_REVET_KEYS,
WING_IN_KEYS,
WING_OUT_KEYS,
} from "./B05_Profile_UI_Drainage_Facility_Fields";
@@ -138,9 +137,9 @@ interface FacilityOptionsFormCallbacks {
export interface FacilityOptionsForm {
root: HTMLElement;
/** 시설 종류에 맞는 옵션 필드를 보여주고 값을 채운다. null이면 통째로 숨긴다.
* "revetment"는 독립 기슭막이 구조물(D4) — 배관 유입·유출부와 같은 조각을 쓴다. */
* 독립 기슭막이(D4)는 레지스트리 옵션 방식으로 옮겨 서브폼 대상이 아니다(2026-08-19). */
setFacility: (
facility: PipeFacility | "revetment" | null,
facility: PipeFacility | null,
options?: Record<string, string | number>,
/** 담당 유역의 설계유량(㎥/s) — 물넘이·세월교 개략 단면의 입력. 없으면 안내만. */
designFlow?: number | null,
@@ -199,7 +198,6 @@ export function createFacilityOptionsForm(
const inletRevet = createRevetmentFields(INLET_REVET_KEYS, {
...REVET_COMMON_DEFAULTS,
form: "돌쌓기(찰)",
protection: "돌붙임(찰)",
});
// 첫 행 = [구조][형태] — 구조에 따라 집수정 형태와 기슭막이 형태가 갈아 끼워진다
// (2026-08-17 사용자 지시 3).
@@ -216,21 +214,14 @@ export function createFacilityOptionsForm(
const outletRevet = createRevetmentFields(OUTLET_REVET_KEYS, {
...REVET_COMMON_DEFAULTS,
form: "돌쌓기(메)",
protection: "돌붙임(메)",
});
outletGroup.body.append(
grid(labeled("구조", outletType), outletRevet.formField),
...outletRevet.rows,
);
// ── 독립 기슭막이(D4) — 같은 조각을 그대로 쓴다(사용자 지시 12) ─────────
const standaloneRevet = createRevetmentFields(STANDALONE_REVET_KEYS, {
...REVET_COMMON_DEFAULTS,
form: "돌쌓기(메)",
protection: "돌붙임(메)",
});
const standaloneWrap = document.createElement("div");
standaloneWrap.append(grid(standaloneRevet.formField), ...standaloneRevet.rows);
// 독립 기슭막이(D4)는 서브폼을 떠났다 — C군과 같은 레지스트리 옵션 방식
// (측점 범위 + 기준측점 전/후, 2026-08-19 사용자 지시 3).
// ── BOX암거 — 본체 규격(프리셋 + 사용자 지정)·날개벽(유입·유출 개별) ────
// 폭·높이 자유 입력은 "사용자 지정"을 골랐을 때만 펼친다(2026-08-17 사용자 확정).
@@ -326,7 +317,6 @@ export function createFacilityOptionsForm(
pipeRow,
inletGroup.root,
outletGroup.root,
standaloneWrap,
boxWrap,
wingInFields.root,
wingOutFields.root,
@@ -335,19 +325,17 @@ export function createFacilityOptionsForm(
fordSummary,
);
let current: PipeFacility | "revetment" | null = null;
let current: PipeFacility | null = null;
function syncVisibility(): void {
root.hidden = current === null;
if (current === null) return;
const isPipe = current === "pipe";
const isBox = current === "box_culvert";
const isRevetment = current === "revetment";
// 관종·관경은 배수관과 세월교가 공유한다.
pipeRow.hidden = !isPipe && current !== "ford_bridge";
inletGroup.root.hidden = !isPipe;
outletGroup.root.hidden = !isPipe;
standaloneWrap.hidden = !isRevetment;
boxWrap.hidden = !isBox;
wingInFields.root.hidden = !isBox;
wingOutFields.root.hidden = !isBox;
@@ -369,7 +357,6 @@ export function createFacilityOptionsForm(
basinMaterialField.hidden = !basin;
inletRevet.formField.hidden = !revet;
inletRevet.rows.forEach((row) => (row.hidden = !revet));
if (revet) inletRevet.syncVisibility();
}
}
@@ -389,7 +376,6 @@ export function createFacilityOptionsForm(
);
inletRevet.onChange(emit);
outletRevet.onChange(emit);
standaloneRevet.onChange(emit);
[
pipeMaterial,
pipeDiameter,
@@ -422,7 +408,6 @@ export function createFacilityOptionsForm(
basinMaterial.value = text("inlet_basin_material");
inletRevet.write(options);
outletRevet.write(options);
standaloneRevet.write(options);
// 저장된 폭·높이가 프리셋과 맞으면 그 프리셋을, 아니면 "사용자 지정"을 고른다.
// 값이 없으면 첫 프리셋(2.0×2.0)이 기본이다.
const [firstLabel, firstWidth, firstHeight] = BOX_SIZE_PRESETS[0];
@@ -471,8 +456,6 @@ export function createFacilityOptionsForm(
}
options.outlet_type = outletType.value;
outletRevet.read(options);
} else if (current === "revetment") {
standaloneRevet.read(options);
} else if (current === "box_culvert") {
// 프리셋을 골랐든 사용자 지정을 적었든 정본은 폭·높이 두 칸이다.
putNumber(options, "body_width_m", boxWidth.value);
@@ -112,108 +112,58 @@ export function group(title: string): { root: HTMLFieldSetElement; body: HTMLDiv
/** 기슭막이 형태 — 재질과 표면처리를 한 축으로 합친 목록(2026-08-17 사용자 지시 1).
* 기존 기슭막이 형식 목록에 돌붙임의 찰·메 구분만 더한 것이다. */
const REVET_FORMS = ["돌쌓기(찰)", "돌쌓기(메)", "콘크리트", "돌망태", "통나무·목재틀", "바자"];
/** 바닥 보호공 — 돌붙임의 찰·메 구분에 도수로를 더한 한 축(2026-08-17 사용자 지시).
* 도수로·산비탈수로(B4)는 별표2가 유출구~원지반 도달을 의무화한 공종인데, 실제로는
* 유출부에서 돌붙임 **대신** 쓰는 선택지라 별도 구조물이 아니라 여기에 넣는다.
* "없음"은 두지 않는다 — 물이 흐르는 자리라 보호공은 반드시 있다(사용자 확정). */
const PROTECTION_FORMS = ["돌붙임(찰)", "돌붙임(메)", "도수로"];
const PROTECTION_CHANNEL = "도수로";
/** 기슭막이 옵션 키 묶음 — 유입·유출·독립 구조물이 같은 조각을 쓰되 키만 다르다. */
/** 기슭막이 옵션 키 묶음 — 유입·유출부가 같은 조각을 쓰되 키만 다르다.
* 보호공(돌붙임) 한 벌은 뺐다 — 기슭막이는 사면 공정이라 바닥 돌붙임이 붙을 자리가
* 아니다(2026-08-19 사용자 지시 1). 독립 기슭막이(D4)는 서브폼을 떠나 C군과 같은
* 레지스트리 옵션 방식으로 옮겼다(같은 지시 3). */
export interface RevetmentKeys {
form: string;
length?: string;
height: string;
protection: string;
protectionArea: string;
protectionWidth: string;
}
/** 부위별 기본 구성 — 형태는 유입 돌쌓기(찰)·유출 돌쌓기(메), 길이 10m·높이 2.5m,
* 보호공은 유입 돌붙임(찰)·유출 돌붙임(메) (2026-08-17 사용자 확정). */
/** 부위별 기본 구성 — 형태는 유입 돌쌓기(찰)·유출 돌쌓기(메), 길이 10m·높이 2.5m. */
export interface RevetmentDefaults {
form: string;
length: string;
height: string;
protection: string;
protectionArea: string;
protectionWidth: string;
}
/** 부위와 무관한 공통 기본 치수 — 길이 10m·높이 2.5m·보호공 면적 10㎡·도수로 폭 1m.
* 도수로 폭 1m는 지식DB에 수치 근거가 없어 2026-08-17 사용자가 확정한 값이다
* (실무 확인 후 개선 예정). */
/** 부위와 무관한 공통 기본 치수 — 길이 10m·높이 2.5m. */
export const REVET_COMMON_DEFAULTS = {
length: "10",
height: "2.5",
protectionArea: "10",
protectionWidth: "1",
} as const;
/** 보호공 개편(2026-08-17) 전 키 — `*_pitching` 계열이었다. 구 저장분을 읽을 때만 쓴다. */
const legacyProtectionKey = (key: string): string => key.replace("protection", "pitching");
export const INLET_REVET_KEYS: RevetmentKeys = {
form: "inlet_revet_form",
length: "inlet_revet_length_m",
height: "inlet_revet_height_m",
protection: "inlet_protection",
protectionArea: "inlet_protection_area_m2",
protectionWidth: "inlet_protection_width_m",
};
export const OUTLET_REVET_KEYS: RevetmentKeys = {
form: "outlet_revet_form",
length: "outlet_revet_length_m",
height: "outlet_revet_height_m",
protection: "outlet_protection",
protectionArea: "outlet_protection_area_m2",
protectionWidth: "outlet_protection_width_m",
};
/** 독립 기슭막이(D4) — 길이는 구간형 측점이 정하므로 길이 칸이 없다. */
export const STANDALONE_REVET_KEYS: RevetmentKeys = {
form: "form",
height: "height_m",
protection: "protection",
protectionArea: "protection_area_m2",
protectionWidth: "protection_width_m",
};
export interface RevetmentFields {
/** 형태 칸 — 유입·유출 그룹에서 "구조" 옆 2열째에 놓는다(2026-08-17 지시 3·4). */
formField: HTMLLabelElement;
/** 형태를 뺀 나머지 행(길이·높이, 보호공). */
/** 형태를 뺀 나머지 행(길이·높이). */
rows: HTMLElement[];
/** 길이 칸(없는 구성이면 null). */
lengthInput: HTMLInputElement | null;
write: (options: Record<string, string | number>) => void;
read: (target: Record<string, string | number>) => void;
onChange: (handler: () => void) => void;
syncVisibility: () => void;
}
/** 구 저장분의 보호공 값을 새 한 축으로 옮긴다 — 개편 전에는 `*_pitching`(있음/없음)과
* `*_pitching_finish`(찰/메) 두 축이었다. "없음"은 선택지가 사라졌으므로 부위
* 기본값으로 올린다(물이 흐르는 자리라 보호공은 필수 — 2026-08-17 사용자 확정). */
function migrateProtection(
options: Record<string, string | number>,
keys: RevetmentKeys,
fallback: string,
): string {
const current = options[keys.protection];
if (typeof current === "string" && PROTECTION_FORMS.includes(current)) return current;
const legacy = options[legacyProtectionKey(keys.protection)];
const finish = options[`${legacyProtectionKey(keys.protection)}_finish`];
if (legacy === "있음" && (finish === "찰" || finish === "메")) return `돌붙임(${finish})`;
return fallback;
}
/** 기슭막이 한 벌(형태·길이·높이 + 보호공) — 배수관 유입·유출부와 독립 구조물이
* 공유하는 입력 인터페이스(2026-08-17 사용자 지시 9·11·12). 재질과 표면처리는
* "형태" 한 축으로 합쳤고(지시 1), 바닥 보호공도 돌붙임 찰·메와 도수로를 한 축으로
* 합쳤다. 기본값은 부위마다 다르다 — 유입 돌붙임(찰) / 유출 돌붙임(메).
* 치수 칸은 보호공에 따라 갈아 끼운다: 돌붙임이면 면적(㎡), 도수로면 폭(m). */
/** 기슭막이 한 벌(형태·길이·높이) — 배수관 유입·유출부가 공유하는 입력 인터페이스
* (2026-08-17 사용자 지시 9·11). 재질과 표면처리는 "형태" 한 축(지시 1). 바닥
* 보호공(돌붙임) 한 벌은 뺐다 — 기슭막이는 사면 공정이라 그 아래 돌붙임이 붙을
* 자리가 아니다(2026-08-19 사용자 지시 1). */
export function createRevetmentFields(
keys: RevetmentKeys,
defaults: RevetmentDefaults,
@@ -225,36 +175,16 @@ export function createRevetmentFields(
if (length) length.value = defaults.length;
const height = numberInput("0.1");
height.value = defaults.height;
const protection = document.createElement("select");
protection.replaceChildren(...PROTECTION_FORMS.map((value) => new Option(value, value)));
protection.value = defaults.protection;
const protectionArea = numberInput("0.1");
protectionArea.value = defaults.protectionArea;
const protectionWidth = numberInput("0.1");
protectionWidth.value = defaults.protectionWidth;
const formField = labeled("형태", form);
const lengthField = length ? labeled("길이 (m)", length) : null;
const heightField = labeled("높이 (m)", height);
const protectionField = labeled("보호공", protection);
const protectionAreaField = labeled("보호공 면적 (㎡)", protectionArea);
const protectionWidthField = labeled("도수로 폭 (m)", protectionWidth);
// 형태는 그룹 첫 행(구조 옆)에 따로 놓이므로 여기 행에서는 뺀다.
const rows = [
grid(...[lengthField, heightField].filter((item): item is HTMLLabelElement => item !== null)),
grid(protectionField, protectionAreaField, protectionWidthField),
];
function syncVisibility(): void {
const channel = protection.value === PROTECTION_CHANNEL;
protectionAreaField.hidden = channel;
protectionWidthField.hidden = !channel;
}
// 만들자마자 기본값 상태를 맞춘다 — 첫 화면에서 면적 칸이 접혀 보이지 않게.
syncVisibility();
return {
formField,
rows,
@@ -265,13 +195,6 @@ export function createRevetmentFields(
form.value = text(keys.form) || defaults.form;
if (length && keys.length) length.value = text(keys.length) || defaults.length;
height.value = text(keys.height) || defaults.height;
protection.value = migrateProtection(options, keys, defaults.protection);
protectionArea.value =
text(keys.protectionArea) ||
text(legacyProtectionKey(keys.protectionArea)) ||
defaults.protectionArea;
protectionWidth.value = text(keys.protectionWidth) || defaults.protectionWidth;
syncVisibility();
},
read(target) {
const putNumber = (key: string, raw: string): void => {
@@ -281,24 +204,11 @@ export function createRevetmentFields(
if (form.value) target[keys.form] = form.value;
if (length && keys.length) putNumber(keys.length, length.value);
putNumber(keys.height, height.value);
target[keys.protection] = protection.value;
// 면적과 폭은 배타 — 고르지 않은 쪽 값을 남기면 수량이 두 벌로 잡힌다.
if (protection.value === PROTECTION_CHANNEL) {
putNumber(keys.protectionWidth, protectionWidth.value);
} else {
putNumber(keys.protectionArea, protectionArea.value);
}
},
onChange(handler) {
[form, height, protection, protectionArea, protectionWidth].forEach((input) =>
input.addEventListener("change", () => {
syncVisibility();
handler();
}),
);
[form, height].forEach((input) => input.addEventListener("change", handler));
length?.addEventListener("change", handler);
},
syncVisibility,
};
}
+4 -1
View File
@@ -288,9 +288,12 @@ export async function renderB05Route(root: HTMLElement): Promise<void> {
},
});
/** 입력·마커 변경 시 측점 라인만 다시 그린다 — 확정 게이트는 폐지(B06 통합 확정). */
/** 입력·마커 변경 시 측점 라인만 다시 그린다 — 확정 게이트는 폐지(B06 통합 확정).
* 지형 구분·등급·기울기 세부 입력은 종단 그래프의 위반 판정 기준까지 **즉시**
* 갈아 끼운다(2026-08-19 사용자 지시 6 — 예전에는 재계산·재진입 때만 반영). */
function markStale(): void {
if (restoring || !routeReady) return;
profilePanel.setGradeLimit(panel.gradeLimitPct());
if (currentSectionDetail) renderStationLines(currentSectionDetail);
}
+32 -2
View File
@@ -36,6 +36,20 @@ export interface RoutePanelValues {
/** 측점 간격(m) — 20m 고정(2026-08-18 사용자 확정, 입력 UI 삭제). */
const STATION_INTERVAL_M = 20;
/* 종단기울기·종단곡선 반경 — 지식DB `설계제원_총괄 §6·§7`(간선·산불진화 공통,
* 설계속도 40/30/20 → 7·10 / 8·12 / 9·14 %, 반경 450/250/100 m) 값과 일치한다
* (2026-08-19 재점검). 서버 `legal_grade_limit_pct`가 같은 표를 쓰므로 화면 안내와
* 실제 적용값이 어긋나지 않는다.
*
* ⚠ 미해결(사용자 협의 필요, 2026-08-19 재점검에서 확인):
* ① 등급 선택지가 간선·지선·작업인데 현행 규칙은 **지선 폐지**, 3종은 간선·
* 산불진화·작업이다(설계제원_총괄 §1 [구현] 지침).
* ② 지식DB의 40/30/20 표는 **설계속도**별이고 간선·산불진화 공통이다. 지금은
* 등급에 속도를 고정 매핑(trunk=40·branch=30·work=20)했는데, 작업임도는
* 설계속도 20 이하 + 종단기울기 최대 20% 범위 조정이라는 별도 규정이다.
* ③ "최소 곡선반경" 입력 칸의 안내값이 **종단**곡선 반경(450/250/100)이다.
* 평면 최소곡선반지름은 일반지형 60/30/15 (설계제원_총괄 §4).
* 값을 임의로 바꾸지 않고 남긴다 — 기본값 확정은 사용자 협의 영역이다. */
const PROFILE_CRITERIA: Record<
RoutePanelValues["gradeClass"],
{ speed: number; grade: Record<RoutePanelValues["terrainType"], number>; radius: number }
@@ -376,6 +390,16 @@ export function createRoutePanel(callbacks: PanelCallbacks) {
onPipeSelect: callbacks.onPipeFacilitySelect,
});
/** 지금 적용되는 종단기울기 상한(%) — 사용자가 값을 넣었으면 그 값, 비웠으면
* 등급·지형의 법정 기준. 종단 그래프의 위반 판정이 이 값을 즉시 따라온다
* (2026-08-19 사용자 지시 6). 특수지형+포장 예외(18%)는 서버가 최종 판정한다. */
function gradeLimitPct(): number {
const criteria = PROFILE_CRITERIA[gradeClass.value as RoutePanelValues["gradeClass"]];
const terrain = terrainType.value as RoutePanelValues["terrainType"];
const typed = parseOptional(maxGradePct);
return typed != null && typed > 0 ? typed : criteria.grade[terrain];
}
/** 등급·지형 선택에 맞춰 법정 기준값을 placeholder와 안내문에 반영한다. */
function syncCriteria(): void {
const criteria = PROFILE_CRITERIA[gradeClass.value as RoutePanelValues["gradeClass"]];
@@ -387,8 +411,12 @@ export function createRoutePanel(callbacks: PanelCallbacks) {
`설계속도 ${criteria.speed}km/h 기준 — 종단기울기 ${criteria.grade[terrain]}% 이하, ` +
`종단곡선 반경 ${criteria.radius}m 이상. 비워두면 이 기준이 적용됩니다.`;
}
gradeClass.addEventListener("change", syncCriteria);
terrainType.addEventListener("change", syncCriteria);
// 지형·등급뿐 아니라 기울기 세부 입력까지 — 바꾸는 즉시 안내문과 위반 기준이
// 따라온다(2026-08-19 사용자 지시 6). onInputChange가 그래프 쪽 갱신을 잇는다.
[gradeClass, terrainType, maxGradePct].forEach((input) =>
input.addEventListener("change", syncCriteria),
);
maxGradePct.addEventListener("input", syncCriteria);
syncCriteria();
// 하단 고정 액션 행: [초기화][임시저장][횡단 이동] — 경로 확정 개념 폐지, 종·횡 통합
@@ -451,6 +479,8 @@ export function createRoutePanel(callbacks: PanelCallbacks) {
structures: structures as StructuresSection,
/** 이어 공사 시작 기준(측점번호·누가거리 오프셋) 현재값. */
stationDisplayOffset,
/** 지금 적용되는 종단기울기 상한(%) — 그래프 위반 판정 즉시 반영용(2026-08-19). */
gradeLimitPct,
values(): RoutePanelValues {
return {
contourInterval: Number(contourInterval.value) || 1,
@@ -567,6 +567,16 @@ export function createRouteProfilePanel(
selectedStructureId = structureIdAtStation(stationId);
draw();
},
/** 종단기울기 상한(%)을 즉시 갈아 끼운다 — 페이지 설정의 지형 구분·등급·세부
* 입력을 바꾼 순간 위반 표시와 편집 차단 기준이 따라와야 한다(2026-08-19
* 사용자 지시 6). 계획선 자체는 서버 재계산 몫이라 건드리지 않는다. */
setGradeLimit(maxGradePct: number) {
if (!base || !Number.isFinite(maxGradePct) || maxGradePct <= 0) return;
if (Math.abs(base.policy.max_grade_pct - maxGradePct) < 1e-9) return;
base = { ...base, policy: { ...base.policy, max_grade_pct: maxGradePct } };
alignment = buildAlignment(base, store.edits());
draw();
},
/** 확정된 노선 평면 선형을 우측 배수유역 지도에 겹친다(사업지 좌표계 m). */
setRoutePolyline(points: ReadonlyArray<{ x: number; y: number }>) {
drainagePanel.setRoute(points);
@@ -157,13 +157,12 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
// 넣으면 시작·종료 측점이 기준 − 앞 ~ 기준 + 뒤로 자동 채워진다.
const facilityOptions = createFacilityOptionsForm();
/** 이 타입이 서브폼(계곡 통과 시설 부속·독립 기슭막이)을 쓰는지 — 쓰면 레지스트리
* 옵션 칸 대신 서브폼이 그린다(2026-08-17 사용자 지시 12: 독립 기슭막이도 같은
* 인터페이스). */
function facilityFormKind(type: StructureType | null): PipeFacility | "revetment" | null {
/** 이 타입이 서브폼(계곡 통과 시설 부속)을 쓰는지 — 쓰면 레지스트리 옵션 칸 대신
* 서브폼이 그린다. 독립 기슭막이(D4)는 서브폼을 떠나 C군과 같은 레지스트리 옵션
* 방식(측점 범위 + 기준측점 전/후)으로 옮겼다(2026-08-19 사용자 지시 3). */
function facilityFormKind(type: StructureType | null): PipeFacility | null {
if (!type) return null;
if (type.managed_by) return type.type_id as PipeFacility;
return type.type_id === "revetment" ? "revetment" : null;
return type.managed_by ? (type.type_id as PipeFacility) : null;
}
/** 구간을 직접 입력하지 않는 타입인가 — C군 사면안정 5종처럼 B05 길이 옵션이