diff --git a/B05_Profile/B05_Profile_Structure_Types.json b/B05_Profile/B05_Profile_Structure_Types.json index 0b5fedd0..441e3dbc 100644 --- a/B05_Profile/B05_Profile_Structure_Types.json +++ b/B05_Profile/B05_Profile_Structure_Types.json @@ -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" } ] }, diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts index 29856dc2..448b6839 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts @@ -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, /** 담당 유역의 설계유량(㎥/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); diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts index a3ad68d6..8e4406e5 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts @@ -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) => void; read: (target: Record) => void; onChange: (handler: () => void) => void; - syncVisibility: () => void; } -/** 구 저장분의 보호공 값을 새 한 축으로 옮긴다 — 개편 전에는 `*_pitching`(있음/없음)과 - * `*_pitching_finish`(찰/메) 두 축이었다. "없음"은 선택지가 사라졌으므로 부위 - * 기본값으로 올린다(물이 흐르는 자리라 보호공은 필수 — 2026-08-17 사용자 확정). */ -function migrateProtection( - options: Record, - 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, }; } diff --git a/B05_Profile/B05_Profile_UI_Page.ts b/B05_Profile/B05_Profile_UI_Page.ts index 361582f1..0385323a 100644 --- a/B05_Profile/B05_Profile_UI_Page.ts +++ b/B05_Profile/B05_Profile_UI_Page.ts @@ -288,9 +288,12 @@ export async function renderB05Route(root: HTMLElement): Promise { }, }); - /** 입력·마커 변경 시 측점 라인만 다시 그린다 — 확정 게이트는 폐지(B06 통합 확정). */ + /** 입력·마커 변경 시 측점 라인만 다시 그린다 — 확정 게이트는 폐지(B06 통합 확정). + * 지형 구분·등급·기울기 세부 입력은 종단 그래프의 위반 판정 기준까지 **즉시** + * 갈아 끼운다(2026-08-19 사용자 지시 6 — 예전에는 재계산·재진입 때만 반영). */ function markStale(): void { if (restoring || !routeReady) return; + profilePanel.setGradeLimit(panel.gradeLimitPct()); if (currentSectionDetail) renderStationLines(currentSectionDetail); } diff --git a/B05_Profile/B05_Profile_UI_Panel.ts b/B05_Profile/B05_Profile_UI_Panel.ts index 300c7033..0a7a2ed3 100644 --- a/B05_Profile/B05_Profile_UI_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Panel.ts @@ -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; 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, diff --git a/B05_Profile/B05_Profile_UI_Profile_Panel.ts b/B05_Profile/B05_Profile_UI_Profile_Panel.ts index 2beb99af..3939425c 100644 --- a/B05_Profile/B05_Profile_UI_Profile_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Profile_Panel.ts @@ -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); diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel.ts b/B05_Profile/B05_Profile_UI_Structures_Panel.ts index eaac6eb6..fcb6912e 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel.ts @@ -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 길이 옵션이