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:
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 길이 옵션이
|
||||
|
||||
Reference in New Issue
Block a user