- 기슭막이는 기슭(사면)을 막는 공정이라 그 아래 돌붙임이 붙을 자리가 아니다 (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>
480 lines
21 KiB
TypeScript
480 lines
21 KiB
TypeScript
/* =============================================================================
|
|
* B05_Profile_UI_Drainage_Facility.ts
|
|
* 계곡 통과 시설(배관/BOX암거/물넘이포장/세월교)의 부속 옵션 서브폼 + 시설 보관소.
|
|
*
|
|
* 자동 배관이든 수동 배관이든 같은 구조물이다 — 별도 편집 UI를 두지 않고 사이드
|
|
* 「구조물 배치」 폼의 옵션 자리에 이 서브폼이 끼워진다(2026-08-17 사용자 지시).
|
|
* 시설 종류는 구조물 배치의 "종류" 드롭다운이, 위치(시작·기준·종료 측점)는 위치
|
|
* 칸이 담당하므로 여기에는 부속 옵션 필드만 있다.
|
|
*
|
|
* 배관 부속(2026-08-17 UI 개편): 관종·관경(기본 1000) 한 행 + 유입구·유출구
|
|
* 소그룹. 유입구는 집수정/기슭막이 택일이고, 유출구는 기슭막이뿐이다(현재 다른
|
|
* 선택지 없음). 기슭막이 조각은 형태·길이·높이·바닥 보호공을 한 벌로 묶어
|
|
* 유입·유출·독립 구조물(D4 기슭막이)이 **같은 인터페이스**를 쓴다.
|
|
*
|
|
* 보호공(2026-08-17 사용자 지시): 구 "돌붙임 있음/없음 + 표면처리(찰/메)" 두 축을
|
|
* `돌붙임(찰)·돌붙임(메)·도수로` 한 축으로 합쳤다. 도수로·산비탈수로(B4)는 유출부에서
|
|
* 돌붙임 대신 쓰는 공종이라 별도 구조물이 아니라 이 선택지로 받는다. 치수는 돌붙임이면
|
|
* 면적(㎡), 도수로면 폭(m)이며 둘은 배타다.
|
|
* 유입·유출 길이가 정해지면 위치 칸(시작·종료 측점)을 기준 − 유입 ~ 기준 + 유출로
|
|
* 자동 채운다. 형식·치수 제원은 B06/B07 필수 승격 대상이라 보이되 비필수다.
|
|
*
|
|
* BOX암거(2026-08-17 사용자 확정): 본체는 실무 관측 규격 2.0×2.0·3.0×3.0을 프리셋
|
|
* 셀렉트로 받고(기본 2.0×2.0), 그 밖의 규격만 "사용자 지정"으로 폭·높이를 직접
|
|
* 받는다 — 저장 키는 어느 쪽이든 body_width_m·body_height_m 숫자 그대로다.
|
|
* 날개벽은 유입·유출이 같은 조각(createWingFields)을 쓰고 설치 있음·짧은쪽 높이
|
|
* 1m·길이 2m·각도 45°가 기본이며, 설치를 "없음"으로 바꾸면 제원 칸이 접힌다.
|
|
* ========================================================================== */
|
|
|
|
import type {
|
|
DetailPipeInput,
|
|
PipeFacility,
|
|
PipeSource,
|
|
} from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
|
|
|
import {
|
|
BOX_SIZE_CUSTOM,
|
|
BOX_SIZE_PRESETS,
|
|
createRevetmentFields,
|
|
createWingFields,
|
|
fordSection,
|
|
grid,
|
|
group,
|
|
INLET_REVET_KEYS,
|
|
labeled,
|
|
numberInput,
|
|
optionalSelect,
|
|
OUTLET_REVET_KEYS,
|
|
putNumber,
|
|
REVET_COMMON_DEFAULTS,
|
|
WING_IN_KEYS,
|
|
WING_OUT_KEYS,
|
|
} from "./B05_Profile_UI_Drainage_Facility_Fields";
|
|
|
|
/** 시설 확장 정보 한 건 — 관 지점(chainage)에 얹힌다. 배관(pipe)도 부속 옵션을
|
|
* 가지면 저장한다. */
|
|
export interface FacilityAttributes {
|
|
facility: PipeFacility;
|
|
start_m?: number;
|
|
end_m?: number;
|
|
options?: Record<string, string | number>;
|
|
}
|
|
|
|
/** 확장 정보가 실제로 있는지 — 배관이면서 아무 부속도 없으면 보관할 이유가 없다. */
|
|
function hasExtras(attributes: FacilityAttributes): boolean {
|
|
if (attributes.facility !== "pipe") return true;
|
|
if (attributes.start_m !== undefined || attributes.end_m !== undefined) return true;
|
|
return !!attributes.options && Object.keys(attributes.options).length > 0;
|
|
}
|
|
|
|
/** 관 이동·재계산 왕복에서 시설 정보를 잃지 않게 chainage 최근접으로 되붙이는 보관소.
|
|
* 백엔드 `carry_facility_attributes`와 같은 기준(정확 일치 → 최근접)이다. */
|
|
export function createFacilityStore() {
|
|
let entries: Array<{ chainage: number; attributes: FacilityAttributes }> = [];
|
|
|
|
return {
|
|
/** 서버 응답으로 전체를 다시 채운다 — 응답이 정본이다. */
|
|
replaceFromResponse(
|
|
points: ReadonlyArray<{
|
|
chainage_m: number;
|
|
facility?: PipeFacility;
|
|
start_m?: number;
|
|
end_m?: number;
|
|
options?: Record<string, string | number>;
|
|
}>,
|
|
): void {
|
|
entries = points
|
|
.map((point) => ({
|
|
chainage: point.chainage_m,
|
|
attributes: {
|
|
facility: (point.facility ?? "pipe") as PipeFacility,
|
|
start_m: point.start_m,
|
|
end_m: point.end_m,
|
|
options: point.options,
|
|
},
|
|
}))
|
|
.filter((entry) => hasExtras(entry.attributes));
|
|
},
|
|
/** 한 지점의 시설을 바꾼다(맨 배관으로 되돌리면 엔트리 삭제). */
|
|
set(chainageM: number, attributes: FacilityAttributes | null): void {
|
|
entries = entries.filter((entry) => Math.abs(entry.chainage - chainageM) > 0.005);
|
|
if (attributes && hasExtras(attributes)) {
|
|
entries.push({ chainage: chainageM, attributes });
|
|
}
|
|
},
|
|
get(chainageM: number): FacilityAttributes | null {
|
|
const hit = entries.find((entry) => Math.abs(entry.chainage - chainageM) <= 0.005);
|
|
return hit ? hit.attributes : null;
|
|
},
|
|
/** 요청 목록에 시설 정보를 되붙인다. 관이 끌려 이동한 뒤에도(정확 일치 없음)
|
|
* 가장 가까운 엔트리를 한 번씩만 소비해 승계한다. */
|
|
attach(points: ReadonlyArray<{ chainage_m: number; source: PipeSource }>): DetailPipeInput[] {
|
|
const pool = [...entries];
|
|
return points.map((point) => {
|
|
if (!pool.length) return { ...point };
|
|
let best = 0;
|
|
for (let index = 1; index < pool.length; index += 1) {
|
|
if (
|
|
Math.abs(pool[index].chainage - point.chainage_m) <
|
|
Math.abs(pool[best].chainage - point.chainage_m)
|
|
)
|
|
best = index;
|
|
}
|
|
const [entry] = pool.splice(best, 1);
|
|
return { ...point, ...entry.attributes };
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
export type FacilityStore = ReturnType<typeof createFacilityStore>;
|
|
|
|
interface FacilityOptionsFormCallbacks {
|
|
/** 아무 옵션이나 바뀌었을 때 — 패널이 저장 흐름(수정 확정)을 안내하는 데 쓴다. */
|
|
onChange?: () => void;
|
|
}
|
|
|
|
export interface FacilityOptionsForm {
|
|
root: HTMLElement;
|
|
/** 시설 종류에 맞는 옵션 필드를 보여주고 값을 채운다. null이면 통째로 숨긴다.
|
|
* 독립 기슭막이(D4)는 레지스트리 옵션 방식으로 옮겨 서브폼 대상이 아니다(2026-08-19). */
|
|
setFacility: (
|
|
facility: PipeFacility | null,
|
|
options?: Record<string, string | number>,
|
|
/** 담당 유역의 설계유량(㎥/s) — 물넘이·세월교 개략 단면의 입력. 없으면 안내만. */
|
|
designFlow?: number | null,
|
|
) => void;
|
|
/** 현재 필드 값을 시설 옵션으로 읽는다(빈 값 생략). */
|
|
readOptions: () => Record<string, string | number>;
|
|
/** 설계유량만 갱신한다 — 필드 값은 건드리지 않는다. 임시 배치(2026-08-18)의
|
|
* 유역 재계산 결과를 입력 중인 폼에 살짝 반영하는 용도. */
|
|
setDesignFlow: (designFlow: number | null) => void;
|
|
}
|
|
|
|
/** 구조물 배치 폼의 옵션 자리에 끼워지는 계곡 통과 시설 부속 옵션 서브폼. */
|
|
export function createFacilityOptionsForm(
|
|
callbacks: FacilityOptionsFormCallbacks = {},
|
|
): FacilityOptionsForm {
|
|
const root = document.createElement("div");
|
|
root.className = "b05-drainage__facility";
|
|
root.hidden = true;
|
|
|
|
// ── 배관 본체 — 관종 > 관경 한 행(2026-08-17 사용자 지시 4) ──────────────
|
|
// 관종 기본값 = 파형강관(2026-08-17 사용자 확정) — 빈 항목 없이 셋 중 하나.
|
|
const pipeMaterial = document.createElement("select");
|
|
pipeMaterial.replaceChildren(
|
|
...["흄관", "VR관", "파형강관"].map((value) => new Option(value, value)),
|
|
);
|
|
pipeMaterial.value = "파형강관";
|
|
const pipeDiameter = document.createElement("select");
|
|
pipeDiameter.replaceChildren(
|
|
...["800", "1000", "1200", "1500"].map((size) => new Option(`Ø${size}`, size)),
|
|
);
|
|
pipeDiameter.value = "1000"; // 별표2 원칙값 + 사용자 확정 기본 (2026-08-17)
|
|
const pipeRow = grid(labeled("관종", pipeMaterial), labeled("관경", pipeDiameter));
|
|
|
|
// ── 유입구 — 집수정 또는 기슭막이 택일(사용자 지시 5~9) ─────────────────
|
|
const inletGroup = group("유입구");
|
|
// 빈값(미지정)을 두면 하위 칸이 전부 숨어 기본값이 안 보인다 — 기슭막이를 기본으로
|
|
// 두고 저유량 지점에서 집수정으로 바꾼다(2026-08-17 사용자 지시).
|
|
const inletType = document.createElement("select");
|
|
inletType.replaceChildren(...["기슭막이", "집수정"].map((value) => new Option(value, value)));
|
|
inletType.value = "기슭막이";
|
|
const basinForm = optionalSelect("선택", [
|
|
"동물이동형",
|
|
"□형(기본형)",
|
|
"돌집수정 ㄴ형",
|
|
"돌집수정 ㄷ형",
|
|
]);
|
|
// 집수정도 재질·표면처리를 형태 한 축으로 합친다(사용자 지시 1).
|
|
const basinMaterial = optionalSelect("선택", [
|
|
"콘크리트",
|
|
"조립식 주철맨홀",
|
|
"돌쌓기(찰)",
|
|
"돌쌓기(메)",
|
|
]);
|
|
const basinMaterialField = labeled("형태", basinMaterial);
|
|
const basinRows = [grid(labeled("형식", basinForm))];
|
|
const inletRevet = createRevetmentFields(INLET_REVET_KEYS, {
|
|
...REVET_COMMON_DEFAULTS,
|
|
form: "돌쌓기(찰)",
|
|
});
|
|
// 첫 행 = [구조][형태] — 구조에 따라 집수정 형태와 기슭막이 형태가 갈아 끼워진다
|
|
// (2026-08-17 사용자 지시 3).
|
|
inletGroup.body.append(
|
|
grid(labeled("구조", inletType), basinMaterialField, inletRevet.formField),
|
|
...basinRows,
|
|
...inletRevet.rows,
|
|
);
|
|
|
|
// ── 유출구 — 구조 선택지는 기슭막이뿐이지만 양식을 맞춘다(사용자 지시 2·4) ──
|
|
const outletGroup = group("유출구");
|
|
const outletType = document.createElement("select");
|
|
outletType.replaceChildren(new Option("기슭막이", "기슭막이"));
|
|
const outletRevet = createRevetmentFields(OUTLET_REVET_KEYS, {
|
|
...REVET_COMMON_DEFAULTS,
|
|
form: "돌쌓기(메)",
|
|
});
|
|
outletGroup.body.append(
|
|
grid(labeled("구조", outletType), outletRevet.formField),
|
|
...outletRevet.rows,
|
|
);
|
|
|
|
// 독립 기슭막이(D4)는 서브폼을 떠났다 — C군과 같은 레지스트리 옵션 방식
|
|
// (측점 범위 + 기준측점 전/후, 2026-08-19 사용자 지시 3).
|
|
|
|
// ── BOX암거 — 본체 규격(프리셋 + 사용자 지정)·날개벽(유입·유출 개별) ────
|
|
// 폭·높이 자유 입력은 "사용자 지정"을 골랐을 때만 펼친다(2026-08-17 사용자 확정).
|
|
const boxSize = optionalSelect("미지정", [
|
|
...BOX_SIZE_PRESETS.map(([label]) => label),
|
|
BOX_SIZE_CUSTOM,
|
|
]);
|
|
const boxWidth = numberInput("0.1");
|
|
const boxHeight = numberInput("0.1");
|
|
const boxCustomRow = grid(labeled("본체 폭 (m)", boxWidth), labeled("본체 높이 (m)", boxHeight));
|
|
const boxWrap = document.createElement("div");
|
|
boxWrap.append(grid(labeled("본체 규격 (m)", boxSize)), boxCustomRow);
|
|
const wingInFields = createWingFields("날개벽(유입)", WING_IN_KEYS);
|
|
const wingOutFields = createWingFields("날개벽(유출)", WING_OUT_KEYS);
|
|
|
|
/** 고른 프리셋을 폭·높이 칸에 실어 둔다 — 저장도, "사용자 지정"으로 바꿨을 때의
|
|
* 출발값도 이 두 칸이 정본이다. 사용자 지정 상태에서는 손대지 않는다. */
|
|
function syncBoxSize(): void {
|
|
const preset = BOX_SIZE_PRESETS.find(([label]) => label === boxSize.value);
|
|
if (preset) {
|
|
boxWidth.value = String(preset[1]);
|
|
boxHeight.value = String(preset[2]);
|
|
}
|
|
boxCustomRow.hidden = !!preset;
|
|
}
|
|
|
|
// ── 세월교 — 구체 내 배관은 배수관과 같은 관종·관경 칸을 쓰고(2026-08-17 사용자
|
|
// 지시) 수량만 따로 받는다.
|
|
const fordCount = numberInput("1", "1", "련");
|
|
const fordRow = grid(labeled("수량 (련)", fordCount));
|
|
|
|
// ── 물넘이·세월교 개략 단면 — 월류 폭 + 월류 높이 한 행(2026-08-18 사용자 지시).
|
|
// 폭 기본값은 세월교 10m·물넘이 포장 5m(사용자 확정 — 지식DB 폭 수치 근거 없음).
|
|
// 높이는 설계유량·폭으로 되짚은 필요 수심을 자동으로 채우고, 사용자가 키울 수는
|
|
// 있으나 계산값(필요 최소 수심) 미만은 되돌린다.
|
|
const fordWidth = numberInput("0.1");
|
|
const fordHeight = numberInput("0.01");
|
|
const fordWidthRow = grid(
|
|
labeled("월류 폭 (m)", fordWidth),
|
|
labeled("월류 높이 (m)", fordHeight),
|
|
);
|
|
const fordSummary = document.createElement("p");
|
|
fordSummary.className = "b05-drainage__facility-note";
|
|
/** 담당 유역 설계유량(㎥/s) — 개략 단면의 입력. 유역이 없으면 null. */
|
|
let designFlowM3s: number | null = null;
|
|
/** 현재 조건(설계유량·월류 폭)의 필요 최소 수심(m). 계산 불가면 null. */
|
|
let fordMinDepthM: number | null = null;
|
|
|
|
function syncFordSummary(): void {
|
|
const section =
|
|
designFlowM3s !== null
|
|
? fordSection(designFlowM3s, Number.parseFloat(fordWidth.value))
|
|
: null;
|
|
fordMinDepthM = section ? section.depthM : null;
|
|
if (section) {
|
|
// 표시 정밀도(0.01m)로 맞춘 최소값 — 비었거나 그보다 작으면 계산값으로 채운다.
|
|
const min = Number(section.depthM.toFixed(2));
|
|
const current = Number.parseFloat(fordHeight.value);
|
|
if (!Number.isFinite(current) || current < min) fordHeight.value = min.toFixed(2);
|
|
}
|
|
if (designFlowM3s === null) {
|
|
fordSummary.textContent =
|
|
"담당 유역의 설계유량이 아직 없습니다 — [유역 분석] 후 개략 단면이 나옵니다.";
|
|
return;
|
|
}
|
|
const head = `설계유량 ${designFlowM3s.toFixed(3)} ㎥/s`;
|
|
if (!section) {
|
|
fordSummary.textContent = `${head} · 월류 폭을 넣으면 필요 수심·단면을 계산합니다.`;
|
|
return;
|
|
}
|
|
// 필요 최소 수심이다 — 월류 높이는 이 값 아래로 내릴 수 없다.
|
|
fordSummary.textContent =
|
|
`${head} · 필요 수심 ${section.depthM.toFixed(2)} m · ` +
|
|
`필요 단면 ${section.areaM2.toFixed(2)} ㎡ · 유속 ${section.velocityMs.toFixed(2)} m/s — ` +
|
|
`월류 높이는 필요 수심 이상만 입력됩니다.`;
|
|
}
|
|
|
|
// 계산값 미만은 입력을 받지 않는다 — 계산값으로 되돌리고 잠깐 붉힌다.
|
|
fordHeight.addEventListener("change", () => {
|
|
if (fordMinDepthM !== null) {
|
|
const min = Number(fordMinDepthM.toFixed(2));
|
|
const value = Number.parseFloat(fordHeight.value);
|
|
if (!Number.isFinite(value) || value < min) {
|
|
fordHeight.value = min.toFixed(2);
|
|
fordHeight.classList.add("is-invalid");
|
|
window.setTimeout(() => fordHeight.classList.remove("is-invalid"), 900);
|
|
}
|
|
}
|
|
emit();
|
|
});
|
|
|
|
root.append(
|
|
pipeRow,
|
|
inletGroup.root,
|
|
outletGroup.root,
|
|
boxWrap,
|
|
wingInFields.root,
|
|
wingOutFields.root,
|
|
fordWidthRow,
|
|
fordRow,
|
|
fordSummary,
|
|
);
|
|
|
|
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";
|
|
// 관종·관경은 배수관과 세월교가 공유한다.
|
|
pipeRow.hidden = !isPipe && current !== "ford_bridge";
|
|
inletGroup.root.hidden = !isPipe;
|
|
outletGroup.root.hidden = !isPipe;
|
|
boxWrap.hidden = !isBox;
|
|
wingInFields.root.hidden = !isBox;
|
|
wingOutFields.root.hidden = !isBox;
|
|
const isFord = current === "ford_pavement" || current === "ford_bridge";
|
|
fordRow.hidden = current !== "ford_bridge";
|
|
fordWidthRow.hidden = !isFord;
|
|
fordSummary.hidden = !isFord;
|
|
if (isFord) syncFordSummary();
|
|
if (isBox) {
|
|
syncBoxSize();
|
|
wingInFields.syncVisibility();
|
|
wingOutFields.syncVisibility();
|
|
}
|
|
if (isPipe) {
|
|
// 유입구 구조에 따라 집수정 칸과 기슭막이 칸을 갈아 끼운다(사용자 지시 6~8).
|
|
const basin = inletType.value === "집수정";
|
|
const revet = inletType.value === "기슭막이";
|
|
basinRows.forEach((row) => (row.hidden = !basin));
|
|
basinMaterialField.hidden = !basin;
|
|
inletRevet.formField.hidden = !revet;
|
|
inletRevet.rows.forEach((row) => (row.hidden = !revet));
|
|
}
|
|
}
|
|
|
|
/** 월류 높이는 cm 단위 수심이라 0.01m 정밀도로 싣는다(putNumber는 0.1m 반올림). */
|
|
function putFordHeight(options: Record<string, string | number>): void {
|
|
const value = Number.parseFloat(fordHeight.value);
|
|
if (Number.isFinite(value) && value > 0) options.ford_height_m = Number(value.toFixed(2));
|
|
}
|
|
|
|
function emit(): void {
|
|
syncVisibility();
|
|
callbacks.onChange?.();
|
|
}
|
|
|
|
[inletType, outletType, basinForm, basinMaterial].forEach((input) =>
|
|
input.addEventListener("change", emit),
|
|
);
|
|
inletRevet.onChange(emit);
|
|
outletRevet.onChange(emit);
|
|
[
|
|
pipeMaterial,
|
|
pipeDiameter,
|
|
boxSize,
|
|
boxWidth,
|
|
boxHeight,
|
|
...wingInFields.inputs,
|
|
...wingOutFields.inputs,
|
|
fordCount,
|
|
fordWidth,
|
|
].forEach((input) => input.addEventListener("change", emit));
|
|
|
|
return {
|
|
root,
|
|
setFacility(facility, options = {}, designFlow = null) {
|
|
current = facility;
|
|
designFlowM3s = designFlow ?? null;
|
|
if (facility === null) {
|
|
syncVisibility();
|
|
return;
|
|
}
|
|
const text = (key: string): string =>
|
|
options[key] !== undefined ? String(options[key]) : "";
|
|
const isFord = facility === "ford_bridge";
|
|
pipeMaterial.value = text("pipe_kind") || "파형강관";
|
|
pipeDiameter.value = text("pipe_diameter_mm") || "1000";
|
|
inletType.value = text("inlet_type") || "기슭막이";
|
|
outletType.value = text("outlet_type") || "기슭막이";
|
|
basinForm.value = text("inlet_basin_form");
|
|
basinMaterial.value = text("inlet_basin_material");
|
|
inletRevet.write(options);
|
|
outletRevet.write(options);
|
|
// 저장된 폭·높이가 프리셋과 맞으면 그 프리셋을, 아니면 "사용자 지정"을 고른다.
|
|
// 값이 없으면 첫 프리셋(2.0×2.0)이 기본이다.
|
|
const [firstLabel, firstWidth, firstHeight] = BOX_SIZE_PRESETS[0];
|
|
const savedWidth = Number.parseFloat(text("body_width_m"));
|
|
const savedHeight = Number.parseFloat(text("body_height_m"));
|
|
const hasSaved =
|
|
Number.isFinite(savedWidth) &&
|
|
savedWidth > 0 &&
|
|
Number.isFinite(savedHeight) &&
|
|
savedHeight > 0;
|
|
const preset = hasSaved
|
|
? BOX_SIZE_PRESETS.find(
|
|
([, width, height]) =>
|
|
Math.abs(width - savedWidth) < 0.05 && Math.abs(height - savedHeight) < 0.05,
|
|
)
|
|
: undefined;
|
|
boxSize.value = hasSaved ? (preset?.[0] ?? BOX_SIZE_CUSTOM) : firstLabel;
|
|
boxWidth.value = String(hasSaved ? savedWidth : firstWidth);
|
|
boxHeight.value = String(hasSaved ? savedHeight : firstHeight);
|
|
wingInFields.write(options);
|
|
wingOutFields.write(options);
|
|
fordCount.value = isFord ? text("pipe_count") : "";
|
|
fordWidth.value = text("ford_width_m");
|
|
fordHeight.value = text("ford_height_m");
|
|
// 월류 폭 기본값 — 세월교 10m·물넘이 포장 5m(2026-08-18 사용자 확정).
|
|
if (!fordWidth.value && (facility === "ford_pavement" || facility === "ford_bridge")) {
|
|
fordWidth.value = facility === "ford_bridge" ? "10" : "5";
|
|
}
|
|
syncVisibility();
|
|
},
|
|
setDesignFlow(designFlow) {
|
|
designFlowM3s = designFlow ?? null;
|
|
syncVisibility();
|
|
},
|
|
readOptions() {
|
|
const options: Record<string, string | number> = {};
|
|
if (current === "pipe") {
|
|
options.pipe_diameter_mm = Number(pipeDiameter.value);
|
|
options.pipe_kind = pipeMaterial.value;
|
|
options.inlet_type = inletType.value;
|
|
if (inletType.value === "집수정") {
|
|
if (basinForm.value) options.inlet_basin_form = basinForm.value;
|
|
if (basinMaterial.value) options.inlet_basin_material = basinMaterial.value;
|
|
} else if (inletType.value === "기슭막이") {
|
|
inletRevet.read(options);
|
|
}
|
|
options.outlet_type = outletType.value;
|
|
outletRevet.read(options);
|
|
} else if (current === "box_culvert") {
|
|
// 프리셋을 골랐든 사용자 지정을 적었든 정본은 폭·높이 두 칸이다.
|
|
putNumber(options, "body_width_m", boxWidth.value);
|
|
putNumber(options, "body_height_m", boxHeight.value);
|
|
wingInFields.read(options);
|
|
wingOutFields.read(options);
|
|
} else if (current === "ford_pavement") {
|
|
putNumber(options, "ford_width_m", fordWidth.value);
|
|
putFordHeight(options);
|
|
} else if (current === "ford_bridge") {
|
|
options.pipe_kind = pipeMaterial.value;
|
|
options.pipe_diameter_mm = Number(pipeDiameter.value);
|
|
const count = Number.parseInt(fordCount.value, 10);
|
|
if (Number.isFinite(count) && count > 0) options.pipe_count = count;
|
|
putNumber(options, "ford_width_m", fordWidth.value);
|
|
putFordHeight(options);
|
|
}
|
|
return options;
|
|
},
|
|
};
|
|
}
|