Merge remote-tracking branch 'origin/sub_laptop_6' into main_laptop_1
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
/* =============================================================================
|
||||
* design_structure_ford.ts
|
||||
* 횡단배수 › 세월교 명세 — 사용자 구조물 표 04(`docs/raw/구조물정의_표/04_횡단배수_세월교.md`) 그대로.
|
||||
*
|
||||
* 컨테이너 셋: 세월교(필수) · 유입 구조(필수) · 유출 구조(필수). 구조 · 정본 = 이 명세 · 이름만 정의(M02)에서 바꿈.
|
||||
* 저장 키 = pipe_points.json 옛 키 그대로(B06 세월교 세트 · B07 · B08 이 읽는 자리) — 새 키 = 유입 · 유출 구조
|
||||
* (`inlet_type` · `outlet_type` = 없음 · 날개벽 · 흙막이) · 고르면 옛 날개벽 설치 칸(`wing_in` · `wing_out`)도 함께 적음.
|
||||
* 월류 높이 = 월류 폭과 연동(폭을 바꾸면 필요 수심으로 채움 · 그 아래로는 안 받음) — 계산은 `design_structure_ford_calc`.
|
||||
* import 없음 — 시험이 이 파일만 옮겨 돌림.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { SpecContainer, SpecChoice, StructureSpec } from "./design_structure_spec";
|
||||
|
||||
/** 유입 · 유출 구조 — 날개벽만 옛 설치 칸이 「있음」. */
|
||||
const guard = (side: "in" | "out"): SpecChoice[] => [
|
||||
{ value: "없음", label: "없음", also: { [`wing_${side}`]: "없음" } },
|
||||
{ value: "날개벽", label: "날개벽", also: { [`wing_${side}`]: "있음" } },
|
||||
{ value: "흙막이", label: "흙막이", also: { [`wing_${side}`]: "없음" } },
|
||||
];
|
||||
|
||||
const guardContainer = (id: "inlet" | "outlet", title: string, link: string): SpecContainer => ({
|
||||
id,
|
||||
title,
|
||||
required: true,
|
||||
link,
|
||||
items: [
|
||||
{
|
||||
id: "structure",
|
||||
label: "구조",
|
||||
kind: "select",
|
||||
key: `${id}_type`,
|
||||
choices: guard(id === "inlet" ? "in" : "out"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const FORD_SPEC: StructureSpec = {
|
||||
typeId: "ford_bridge",
|
||||
containers: [
|
||||
{
|
||||
id: "ford",
|
||||
title: "세월교",
|
||||
required: true,
|
||||
link: "gford",
|
||||
items: [
|
||||
{
|
||||
id: "diameter",
|
||||
label: "관경",
|
||||
kind: "select",
|
||||
key: "pipe_diameter_mm",
|
||||
asNumber: true,
|
||||
choices: ["800", "1000", "1200", "1500"].map((v) => ({ value: v, label: v })),
|
||||
},
|
||||
{ id: "width", label: "월류 폭 (m)", kind: "number", key: "ford_width_m", step: 0.1 },
|
||||
{ id: "height", label: "월류 높이 (m)", kind: "number", key: "ford_height_m", step: 0.01 },
|
||||
{ id: "count", label: "수량 (련)", kind: "number", key: "pipe_count", step: 1 },
|
||||
],
|
||||
},
|
||||
guardContainer("inlet", "유입 구조", "gfordin"),
|
||||
guardContainer("outlet", "유출 구조", "gfordout"),
|
||||
],
|
||||
// 표 04 에 없는 옛 칸 — 화면에서 빼고 값은 남김(지난 정의 값)
|
||||
retired: [
|
||||
"pipe_kind",
|
||||
"wing_in_height_m",
|
||||
"wing_in_length_m",
|
||||
"wing_in_angle_deg",
|
||||
"wing_out_height_m",
|
||||
"wing_out_length_m",
|
||||
"wing_out_angle_deg",
|
||||
],
|
||||
};
|
||||
|
||||
/** 구조를 날개벽 설치 칸(옛 저장 · B06 조정창이 고치는 칸)에 맞춤 — 있음 = 날개벽 · 없음 = 날개벽이면 없음
|
||||
* (흙막이는 그대로) · 설치 칸이 없으면 구조 그대로. */
|
||||
export function withLegacyGuards(
|
||||
options: Record<string, string | number>,
|
||||
): Record<string, string | number> {
|
||||
const out = { ...options };
|
||||
for (const [type, wing] of [
|
||||
["inlet_type", "wing_in"],
|
||||
["outlet_type", "wing_out"],
|
||||
]) {
|
||||
if (out[wing] === "있음") out[type] = "날개벽";
|
||||
else if (out[wing] === "없음" && (out[type] ?? "날개벽") === "날개벽") out[type] = "없음";
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/* =============================================================================
|
||||
* design_structure_ford_calc.ts
|
||||
* 세월교 명세 계산 — 월류 폭 ↔ 월류 높이 연동(PLAN 52-4).
|
||||
*
|
||||
* 높이 = 담당 유역 설계유량을 그 월류 폭으로 넘기는 필요 수심(Manning 개수로 · `fordSection` 그대로).
|
||||
* 근거: Manning 공식 `resources/knowledge/technical_info/01_임도/03_계산정보/통수단면.md:23~33`
|
||||
* (KDS 44 40 00 2.7.1~2.7.2) · 조도계수 0.017 = 같은 파일 :59(콘크리트 수로 보통 · 울진 물넘이) ·
|
||||
* 경사 10 % = 실무 수리계산서 역산값(`config_frontend.FORD_SLOPE` · 지식DB 수치 없음).
|
||||
* 폭을 바꾸면 높이 칸을 이 값으로 채움 · 사용자는 올려 적을 수 있고 이 값 아래로는 안 받음.
|
||||
* 높이는 저장되는 입력값 — 서버는 받은 높이로 노면을 내림(`B06_Section_Engine_Culvert._ford_set`) · 재계산 안 함.
|
||||
* 설계유량이 없으면 null = 손 입력.
|
||||
* ========================================================================== */
|
||||
|
||||
import { fordSection } from "../../B05_Profile/B05_Profile_UI_Drainage_Facility_Fields";
|
||||
|
||||
/** 월류 폭(m) → 필요 월류 높이(m) · 0.01 m 올림. 유량 · 폭이 없으면 null. */
|
||||
export function fordHeightM(designFlowM3s: number | null, widthM: number): number | null {
|
||||
const section = fordSection(designFlowM3s, widthM);
|
||||
return section ? Math.ceil(section.depthM * 100 - 1e-9) / 100 : null;
|
||||
}
|
||||
|
||||
/** 높이 빈 칸 안내. */
|
||||
export function fordHeightHint(designFlowM3s: number | null, widthM: number): string {
|
||||
const need = fordHeightM(designFlowM3s, widthM);
|
||||
if (need !== null) return `필요 수심 ${need.toFixed(2)} 이상`;
|
||||
return designFlowM3s ? "월류 폭을 넣으면 셈" : "설계유량 없음 — 손 입력";
|
||||
}
|
||||
@@ -8,10 +8,11 @@
|
||||
* (`inlet_type` · `*_revet_*`)에 둠. 새 키 = 집수정 높이 · 선택 컨테이너(`soil_add_{n}_*` · `chute_add_{n}_*`).
|
||||
* 정함(PLAN 52): 유출 형태 = 흙막이 6가지 · 도수로/돌붙임 = 구조만 · 길이 = 전 + 후 맞물림.
|
||||
* 계산(관 길이 · 높이 안내)은 `design_structure_pipe_calc`.
|
||||
* import 없음 — 시험이 이 파일만 옮겨 돌림.
|
||||
* import = 같은 폴더 명세뿐 — 시험이 이 폴더 파일만 옮겨 돌림.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { SpecChoice, SpecItem, StructureSpec } from "./design_structure_spec";
|
||||
import { FORD_SPEC } from "./design_structure_ford";
|
||||
|
||||
const SOIL = "기슭막이"; // 저장 값(옛 키 그대로) — 화면 이름은 흙막이
|
||||
const BASIN = "집수정";
|
||||
@@ -184,4 +185,7 @@ export const PIPE_SPEC: StructureSpec = {
|
||||
};
|
||||
|
||||
/** 명세로 그리는 종류 — 종류 키 → 명세(표가 오는 대로 더함). */
|
||||
export const STRUCTURE_SPECS: Readonly<Record<string, StructureSpec>> = { pipe: PIPE_SPEC };
|
||||
export const STRUCTURE_SPECS: Readonly<Record<string, StructureSpec>> = {
|
||||
pipe: PIPE_SPEC,
|
||||
ford_bridge: FORD_SPEC,
|
||||
};
|
||||
|
||||
@@ -53,6 +53,8 @@ import {
|
||||
} from "./B05_Profile_UI_Drainage_Facility_Fields";
|
||||
import { createSpecForm, type SpecLinks } from "../A00_Common/design/design_structure_form";
|
||||
import { PIPE_SPEC } from "../A00_Common/design/design_structure_pipe";
|
||||
import { FORD_SPEC, withLegacyGuards } from "../A00_Common/design/design_structure_ford";
|
||||
import { fordHeightHint, fordHeightM } from "../A00_Common/design/design_structure_ford_calc";
|
||||
import { guardHeightHint, pipeLengthM } from "../A00_Common/design/design_structure_pipe_calc";
|
||||
import {
|
||||
hasLinks,
|
||||
@@ -250,45 +252,31 @@ export function createFacilityOptionsForm(
|
||||
root.className = "b05-drainage__facility";
|
||||
root.hidden = true;
|
||||
|
||||
// ── 관종 · 관경 한 행 — 세월교 몫(배관은 아래 명세 폼 · PLAN 52-1) ─────────
|
||||
const pipeMaterial = optionalSelect("— 안 정함 —", ["흄관", "VR관", "파형강관"]);
|
||||
const pipeDiameter = document.createElement("select");
|
||||
pipeDiameter.replaceChildren(
|
||||
new Option("— 안 정함 —", ""),
|
||||
...["800", "1000", "1200", "1500"].map((size) => new Option(`Ø${size}`, size)),
|
||||
);
|
||||
const pipeRow = grid(
|
||||
labeled("관종", pipeMaterial, "pipe_kind"),
|
||||
labeled("관경", pipeDiameter, "pipe_diameter_mm"),
|
||||
);
|
||||
|
||||
// ── 배관 = 사용자 구조물 표 01 명세(PLAN 52-1) — 컨테이너 다섯 · 옛 관종 · 날개벽 칸은 뺌(지난 정의 값) ──
|
||||
let linkSource: SpecLinks | null = null;
|
||||
let chainage: number | null = null;
|
||||
const pipeForm = createSpecForm(
|
||||
{
|
||||
get: () => linkSource?.get() ?? {},
|
||||
pick: (key) => linkSource?.pick(key),
|
||||
clear: (key) => linkSource?.clear(key),
|
||||
set: (key, item) => linkSource?.set(key, item),
|
||||
const specLinks: SpecLinks = {
|
||||
get: () => linkSource?.get() ?? {},
|
||||
pick: (key) => linkSource?.pick(key),
|
||||
clear: (key) => linkSource?.clear(key),
|
||||
set: (key, item) => linkSource?.set(key, item),
|
||||
};
|
||||
const pipeForm = createSpecForm(specLinks, {
|
||||
auto: (_c, item) => {
|
||||
if (item.id !== "length") return "";
|
||||
const length = pipeLengthM(chainage);
|
||||
return length === null ? "횡단 설계 뒤 셈" : length.toFixed(2);
|
||||
},
|
||||
{
|
||||
auto: (_c, item) => {
|
||||
if (item.id !== "length") return "";
|
||||
const length = pipeLengthM(chainage);
|
||||
return length === null ? "횡단 설계 뒤 셈" : length.toFixed(2);
|
||||
},
|
||||
hint: (c, item, state) => {
|
||||
const soil = c.id !== "inlet" || state.inlet?.[0]?.structure === "기슭막이";
|
||||
return item.id === "height" && soil ? guardHeightHint(state.pipe?.[0]?.diameter ?? "") : "";
|
||||
},
|
||||
onChange: (c, item) => {
|
||||
if (c.id === "inlet" && (item.id === "structure" || item.id === "form"))
|
||||
inletStructureHandler?.(inletStructureOf());
|
||||
emit();
|
||||
},
|
||||
hint: (c, item, state) => {
|
||||
const soil = c.id !== "inlet" || state.inlet?.[0]?.structure === "기슭막이";
|
||||
return item.id === "height" && soil ? guardHeightHint(state.pipe?.[0]?.diameter ?? "") : "";
|
||||
},
|
||||
);
|
||||
onChange: (c, item) => {
|
||||
if (c.id === "inlet" && (item.id === "structure" || item.id === "form"))
|
||||
inletStructureHandler?.(inletStructureOf());
|
||||
emit();
|
||||
},
|
||||
});
|
||||
// 조정창에서 옮겨 오는 행(단 수·옵션)이 붙을 자리 — 유입 · 유출 컨테이너 끝(2026-08-29 지시 4)
|
||||
const inletSlot = pipeForm.slot("inlet");
|
||||
const outletSlot = pipeForm.slot("outlet");
|
||||
@@ -301,6 +289,26 @@ export function createFacilityOptionsForm(
|
||||
: "auto";
|
||||
};
|
||||
|
||||
// ── 세월교 = 사용자 구조물 표 04 명세(PLAN 52-4) — 세월교 · 유입 구조 · 유출 구조 ──
|
||||
// 월류 높이 = 월류 폭과 연동(폭을 바꾸면 필요 수심으로 채움 · 그 아래는 안 받음 · `design_structure_ford_calc`)
|
||||
let fordFlow: number | null = null;
|
||||
const fordNeed = (): number | null =>
|
||||
fordHeightM(fordFlow, Number(fordForm.state().ford?.[0]?.width ?? ""));
|
||||
const fordForm = createSpecForm(specLinks, {
|
||||
hint: (c, item, state) =>
|
||||
c.id === "ford" && item.id === "height"
|
||||
? fordHeightHint(fordFlow, Number(state.ford?.[0]?.width ?? ""))
|
||||
: "",
|
||||
onChange: (c, item) => {
|
||||
const need = c.id === "ford" ? fordNeed() : null;
|
||||
const height = fordForm.state().ford?.[0]?.height ?? "";
|
||||
const low = height !== "" && !(Number(height) >= (need ?? 0));
|
||||
if (need !== null && (item.id === "width" || (item.id === "height" && low)))
|
||||
fordForm.setValue("ford", "height", need.toFixed(2));
|
||||
emit();
|
||||
},
|
||||
});
|
||||
|
||||
// ── 추가 기슭막이(다단) — 횡단도에서 다단 벽을 고를 때만 뜬다(2026-08-30 사용자
|
||||
// 지시 4). 유입구·유출구와 같은 양식이되, 값은 조정창 사본의 행이 그대로 옮겨 온다
|
||||
// (다단 벽은 폼에 자기 칸이 없다 — 높이·형태·구간값이 전부 조정 채널에 있다).
|
||||
@@ -370,6 +378,8 @@ export function createFacilityOptionsForm(
|
||||
);
|
||||
const wingInFields = createWingFields("날개벽(유입)", WING_IN_KEYS);
|
||||
const wingOutFields = createWingFields("날개벽(유출)", WING_OUT_KEYS);
|
||||
const wingInHome = wingInFields.slot.parentElement!;
|
||||
const wingOutHome = wingOutFields.slot.parentElement!;
|
||||
|
||||
const hiddenOptions = createHiddenOptions(root);
|
||||
|
||||
@@ -399,8 +409,8 @@ export function createFacilityOptionsForm(
|
||||
// 월류 폭·높이 → 바닥 경사 → 수량 → 개략 단면 결과. 다른 시설에서는 전부 숨는다.
|
||||
root.append(
|
||||
saveNote,
|
||||
pipeRow,
|
||||
pipeForm.root,
|
||||
fordForm.root,
|
||||
ford.widthRow,
|
||||
ford.slopeRow,
|
||||
ford.countRow,
|
||||
@@ -421,15 +431,18 @@ export function createFacilityOptionsForm(
|
||||
if (current === null) return;
|
||||
const isPipe = current === "pipe";
|
||||
const isBox = current === "box_culvert";
|
||||
pipeRow.hidden = current !== "ford_bridge";
|
||||
const isBridge = current === "ford_bridge";
|
||||
pipeForm.root.hidden = !isPipe;
|
||||
fordForm.root.hidden = !isBridge;
|
||||
boxWrap.hidden = !isBox;
|
||||
// 날개벽은 BOX암거와 세월교가 같은 옵션 한 벌을 쓴다(2026-08-25 사용자 확정).
|
||||
const hasWing = isBox || current === "ford_bridge";
|
||||
// 날개벽 칸 = BOX암거 · 세월교는 유입 · 유출 구조 컨테이너(52-4) — B06 조정 행 자리만 그 컨테이너 끝으로
|
||||
const hasWing = isBox;
|
||||
wingInFields.root.hidden = !hasWing;
|
||||
wingOutFields.root.hidden = !hasWing;
|
||||
const isFord = current === "ford_pavement" || current === "ford_bridge";
|
||||
ford.countRow.hidden = current !== "ford_bridge";
|
||||
(isBridge ? fordForm.slot("inlet") : wingInHome).append(wingInFields.slot);
|
||||
(isBridge ? fordForm.slot("outlet") : wingOutHome).append(wingOutFields.slot);
|
||||
const isFord = current === "ford_pavement";
|
||||
ford.countRow.hidden = true;
|
||||
ford.widthRow.hidden = !isFord;
|
||||
// 바닥 경사는 물넘이포장만 쓴다 — 세월교는 구체 위 노면이라 파임이 없다.
|
||||
ford.slopeRow.hidden = current !== "ford_pavement";
|
||||
@@ -462,8 +475,6 @@ export function createFacilityOptionsForm(
|
||||
revetOutlet.fields.onChange(emit);
|
||||
revetSide.addEventListener("change", emit);
|
||||
[
|
||||
pipeMaterial,
|
||||
pipeDiameter,
|
||||
boxSize,
|
||||
boxWidth,
|
||||
boxHeight,
|
||||
@@ -500,8 +511,12 @@ export function createFacilityOptionsForm(
|
||||
bindLinks(links) {
|
||||
linkSource = links;
|
||||
pipeForm.paintLinks();
|
||||
fordForm.paintLinks();
|
||||
},
|
||||
paintLinks: () => {
|
||||
pipeForm.paintLinks();
|
||||
fordForm.paintLinks();
|
||||
},
|
||||
paintLinks: () => pipeForm.paintLinks(),
|
||||
setChainage(chainageM) {
|
||||
chainage = chainageM;
|
||||
if (current === "pipe") pipeForm.refresh();
|
||||
@@ -516,6 +531,7 @@ export function createFacilityOptionsForm(
|
||||
current = facility;
|
||||
hiddenOptions.remember(options);
|
||||
ford.setDesignFlow(designFlow ?? null);
|
||||
fordFlow = designFlow ?? null;
|
||||
if (facility === null) {
|
||||
syncVisibility();
|
||||
return;
|
||||
@@ -523,12 +539,11 @@ export function createFacilityOptionsForm(
|
||||
const text = (key: string): string =>
|
||||
options[key] !== undefined ? String(options[key]) : "";
|
||||
// 받은 값만 칸에 — 없으면 「안 정함」(새 시설은 부르는 쪽이 등록부 기본값을 실어 줌 · 46-7).
|
||||
pipeMaterial.value = text("pipe_kind");
|
||||
pipeDiameter.value = text("pipe_diameter_mm");
|
||||
if (facility === "pipe") {
|
||||
const groups = Object.fromEntries((definition?.groups ?? []).map((g) => [g.id, g.name]));
|
||||
const groups = Object.fromEntries((definition?.groups ?? []).map((g) => [g.id, g.name]));
|
||||
if (facility === "pipe")
|
||||
pipeForm.write(PIPE_SPEC, options, definition?.spec_names ?? {}, groups);
|
||||
}
|
||||
if (facility === "ford_bridge")
|
||||
fordForm.write(FORD_SPEC, withLegacyGuards(options), definition?.spec_names ?? {}, groups);
|
||||
// 저장된 폭·높이가 프리셋과 맞으면 그 프리셋을, 아니면 "사용자 지정"을 고른다.
|
||||
// 값이 없으면 「안 정함」.
|
||||
const savedWidth = Number.parseFloat(text("body_width_m"));
|
||||
@@ -549,7 +564,7 @@ export function createFacilityOptionsForm(
|
||||
boxHeight.value = hasSaved ? String(savedHeight) : "";
|
||||
wingInFields.write(options);
|
||||
wingOutFields.write(options);
|
||||
if (facility === "ford_pavement" || facility === "ford_bridge") ford.write(facility, options);
|
||||
if (facility === "ford_pavement") ford.write(facility, options);
|
||||
revetSide.value = text("side");
|
||||
const spread = legacyRevetOptions(options);
|
||||
revetInlet.fields.write(spread);
|
||||
@@ -558,15 +573,13 @@ export function createFacilityOptionsForm(
|
||||
},
|
||||
setDesignFlow(designFlow) {
|
||||
ford.setDesignFlow(designFlow ?? null);
|
||||
fordFlow = designFlow ?? null;
|
||||
if (current === "ford_bridge") fordForm.refresh(); // 높이 안내
|
||||
syncVisibility();
|
||||
},
|
||||
readOptions() {
|
||||
const options: Record<string, string | number> = {};
|
||||
// 고른 칸만 싣는다 — 「안 정함」은 빈 값(기본값을 몰래 확정으로 안 바꿈).
|
||||
const putPipe = (): void => {
|
||||
if (pipeDiameter.value) options.pipe_diameter_mm = Number(pipeDiameter.value);
|
||||
if (pipeMaterial.value) options.pipe_kind = pipeMaterial.value;
|
||||
};
|
||||
if (current === "pipe") {
|
||||
Object.assign(options, pipeForm.read()); // 명세 컨테이너 값 = 옛 키 그대로(PLAN 52-1)
|
||||
} else if (current === "box_culvert") {
|
||||
@@ -578,10 +591,7 @@ export function createFacilityOptionsForm(
|
||||
} else if (current === "ford_pavement") {
|
||||
ford.read(current, options);
|
||||
} else if (current === "ford_bridge") {
|
||||
putPipe();
|
||||
ford.read(current, options);
|
||||
wingInFields.read(options);
|
||||
wingOutFields.read(options);
|
||||
Object.assign(options, fordForm.read()); // 표 04 명세 값 · 옛 관종 · 날개벽 치수는 병합이 이어 붙임(52-4)
|
||||
} else if (current === "revetment") {
|
||||
if (revetSide.value) options.side = revetSide.value;
|
||||
// 단 수는 폼이 갖지 않는다 — 횡단 설계 patch(extra_wall_counts)가 정본이고
|
||||
|
||||
@@ -42,14 +42,16 @@ export const FORM_OPTION_KEYS: Readonly<Record<string, readonly string[]>> = {
|
||||
],
|
||||
box_culvert: ["body_width_m", "body_height_m", ...wingKeys("in"), ...wingKeys("out")],
|
||||
ford_pavement: ["ford_width_m", "ford_height_m", "ford_slope_pct", "thickness_cm", "length_m"],
|
||||
// 세월교 = 표 04 명세 키(PLAN 52-4 · `design_structure_ford`) — 옛 관종 · 날개벽 치수는 폼에서 빠져 이어 붙임
|
||||
ford_bridge: [
|
||||
"pipe_kind",
|
||||
"pipe_diameter_mm",
|
||||
"pipe_count",
|
||||
"ford_width_m",
|
||||
"ford_height_m",
|
||||
...wingKeys("in"),
|
||||
...wingKeys("out"),
|
||||
"inlet_type",
|
||||
"outlet_type",
|
||||
"wing_in",
|
||||
"wing_out",
|
||||
],
|
||||
revetment: ["side", ...revetKeys("inlet"), ...revetKeys("outlet")],
|
||||
};
|
||||
|
||||
@@ -3321,7 +3321,25 @@
|
||||
"계곡통과시설",
|
||||
"월류단면",
|
||||
"날개벽"
|
||||
]
|
||||
],
|
||||
"이름": {
|
||||
"ford.diameter": "관경",
|
||||
"ford.diameter=800": "800",
|
||||
"ford.diameter=1000": "1000",
|
||||
"ford.diameter=1200": "1200",
|
||||
"ford.diameter=1500": "1500",
|
||||
"ford.width": "월류 폭 (m)",
|
||||
"ford.height": "월류 높이 (m)",
|
||||
"ford.count": "수량 (련)",
|
||||
"inlet.structure": "구조",
|
||||
"inlet.structure=없음": "없음",
|
||||
"inlet.structure=날개벽": "날개벽",
|
||||
"inlet.structure=흙막이": "흙막이",
|
||||
"outlet.structure": "구조",
|
||||
"outlet.structure=없음": "없음",
|
||||
"outlet.structure=날개벽": "날개벽",
|
||||
"outlet.structure=흙막이": "흙막이"
|
||||
}
|
||||
},
|
||||
"revetment": {
|
||||
"동작": [
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/* 세월교 명세(표 04) · 월류 폭 ↔ 높이 계산 단위검증 헬퍼(PLAN 52-4) — `design_structure_spec.ts` ·
|
||||
* `design_structure_ford.ts` · `design_structure_ford_calc.ts`(+ `fordSection` 이 든 B05 칸 조각)를 그 자리에서
|
||||
* 트랜스파일해 Node 로 돌림. 결과 = JSON 한 벌. 인자 `names` 면 세월교 이름 표(정의 `종류.ford_bridge.이름`)만 냄.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const cache = new Map();
|
||||
/** TS 한 파일을 CommonJS 로 — 상대 import 는 그 파일 자리 기준 · `@config/` 는 config 폴더 · .json 은 그대로. */
|
||||
const load = (file) => {
|
||||
if (cache.has(file)) return cache.get(file);
|
||||
if (file.endsWith(".json")) return JSON.parse(fs.readFileSync(file, "utf8"));
|
||||
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(file, box.exports);
|
||||
const near = (m) => {
|
||||
const base = m.startsWith("@config/")
|
||||
? path.join(ROOT, "config", m.slice(8))
|
||||
: path.join(path.dirname(file), m);
|
||||
return load(base.endsWith(".json") ? base : `${base}.ts`);
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, near);
|
||||
cache.set(file, box.exports);
|
||||
return box.exports;
|
||||
};
|
||||
const D = (f) => path.join(ROOT, "A00_Common", "design", f);
|
||||
const s = load(D("design_structure_spec.ts"));
|
||||
const { FORD_SPEC, withLegacyGuards } = load(D("design_structure_ford.ts"));
|
||||
const calc = load(D("design_structure_ford_calc.ts"));
|
||||
|
||||
if (process.argv[2] === "names") {
|
||||
process.stdout.write(JSON.stringify(s.nameTable(FORD_SPEC)));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const old = {
|
||||
pipe_kind: "파형강관",
|
||||
pipe_diameter_mm: 1000,
|
||||
pipe_count: 2,
|
||||
ford_width_m: 10,
|
||||
ford_height_m: 0.3,
|
||||
wing_in: "있음",
|
||||
wing_in_height_m: 1,
|
||||
wing_out: "없음",
|
||||
};
|
||||
const legacy = withLegacyGuards(old);
|
||||
const state = s.readState(FORD_SPEC, legacy);
|
||||
const back = s.writeOptions(FORD_SPEC, state);
|
||||
const soil = JSON.parse(JSON.stringify(state));
|
||||
soil.inlet[0].structure = "흙막이";
|
||||
soil.outlet[0].structure = "날개벽";
|
||||
const soilOut = s.writeOptions(FORD_SPEC, soil);
|
||||
|
||||
const flows = [0.5, 2.54, 8];
|
||||
const widths = [3, 5, 10, 20];
|
||||
const table = flows.flatMap((q) => widths.map((b) => [q, b, calc.fordHeightM(q, b)]));
|
||||
|
||||
const out = {
|
||||
containers: FORD_SPEC.containers.map((c) => [c.id, c.title, c.required]),
|
||||
items: FORD_SPEC.containers.map((c) => c.items.map((i) => [i.id, i.kind, i.key ?? null])),
|
||||
choices: FORD_SPEC.containers.map((c) =>
|
||||
c.items.filter((i) => i.kind === "select").map((i) => s.choicesOf(i, {}).map((ch) => ch.label)),
|
||||
),
|
||||
legacy: { inlet: legacy.inlet_type, outlet: legacy.outlet_type },
|
||||
keepSoil: withLegacyGuards({ inlet_type: "흙막이", wing_in: "없음" }).inlet_type,
|
||||
b06Off: withLegacyGuards({ inlet_type: "날개벽", wing_in: "없음" }).inlet_type,
|
||||
b06On: withLegacyGuards({ inlet_type: "없음", wing_in: "있음" }).inlet_type,
|
||||
state,
|
||||
back,
|
||||
soilOut,
|
||||
links: FORD_SPEC.containers.map((c) => s.linkOf(c, 1)),
|
||||
keys: s.specKeys(FORD_SPEC),
|
||||
retired: FORD_SPEC.retired,
|
||||
table,
|
||||
none: [calc.fordHeightM(null, 10), calc.fordHeightM(2.54, 0)],
|
||||
hints: [
|
||||
calc.fordHeightHint(2.54, 10),
|
||||
calc.fordHeightHint(2.54, 0),
|
||||
calc.fordHeightHint(null, 10),
|
||||
],
|
||||
};
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -13,7 +13,8 @@ const load = (file, deps = {}) => {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
new Function("exports", "module", "require", js)(box.exports, box, (m) => deps[m] ?? require(m));
|
||||
const near = (m) => (m.startsWith("./") ? load(`${m.slice(2)}.ts`) : require(m)); // 같은 폴더 명세
|
||||
new Function("exports", "module", "require", js)(box.exports, box, (m) => deps[m] ?? near(m));
|
||||
return box.exports;
|
||||
};
|
||||
const s = load("design_structure_spec.ts");
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""횡단배수 › 세월교(표 04) 명세 + 월류 폭 ↔ 높이 연동 — PLAN 52-4.
|
||||
|
||||
순수 규칙은 Node 헬퍼(`helper_52_4_ford.cjs`)가 돌림 · 화면은 ORCA `ui_checks/52-4_세월교_컨테이너.js`.
|
||||
높이 거울 = 이 파일이 Manning(통수단면.md §2 · n 0.017 · S 0.10)을 따로 풀어 같은 값인지 봄.
|
||||
"""
|
||||
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(os.path.dirname(HERE))
|
||||
N, S = 0.017, 0.10
|
||||
|
||||
|
||||
def _node(*args):
|
||||
proc = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_52_4_ford.cjs"), *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out():
|
||||
return _node()
|
||||
|
||||
|
||||
def test_컨테이너_셋_모두_필수(out):
|
||||
assert out["containers"] == [
|
||||
["ford", "세월교", True],
|
||||
["inlet", "유입 구조", True],
|
||||
["outlet", "유출 구조", True],
|
||||
]
|
||||
assert out["items"][0] == [
|
||||
["diameter", "select", "pipe_diameter_mm"],
|
||||
["width", "number", "ford_width_m"],
|
||||
["height", "number", "ford_height_m"],
|
||||
["count", "number", "pipe_count"],
|
||||
]
|
||||
assert out["choices"] == [
|
||||
[["800", "1000", "1200", "1500"]],
|
||||
[["없음", "날개벽", "흙막이"]],
|
||||
[["없음", "날개벽", "흙막이"]],
|
||||
]
|
||||
|
||||
|
||||
def test_옛_날개벽_칸으로_구조를_세우고_같은_키로_씀(out):
|
||||
assert out["legacy"] == {"inlet": "날개벽", "outlet": "없음"}
|
||||
assert out["keepSoil"] == "흙막이" # 흙막이는 설치 칸 없음이어도 그대로
|
||||
assert out["b06Off"] == "없음" and out["b06On"] == "날개벽" # B06 조정창 설치 칸을 따름
|
||||
back = out["back"]
|
||||
assert back["pipe_diameter_mm"] == 1000 and back["pipe_count"] == 2
|
||||
assert back["ford_width_m"] == 10 and back["ford_height_m"] == 0.3
|
||||
assert back["wing_in"] == "있음" and back["wing_out"] == "없음"
|
||||
# 표 04 에 없는 옛 칸은 폼이 안 적음(병합이 이어 붙임 = 지난 정의 값)
|
||||
assert "pipe_kind" not in back and "wing_in_height_m" not in back
|
||||
assert "pipe_kind" in out["retired"]
|
||||
|
||||
|
||||
def test_흙막이_날개벽_고르면_옛_설치_칸도_맞춰_씀(out):
|
||||
s = out["soilOut"]
|
||||
assert s["inlet_type"] == "흙막이" and s["wing_in"] == "없음"
|
||||
assert s["outlet_type"] == "날개벽" and s["wing_out"] == "있음"
|
||||
|
||||
|
||||
def test_잇기_키_컨테이너마다(out):
|
||||
assert out["links"] == ["gford", "gfordin", "gfordout"]
|
||||
|
||||
|
||||
def _manning_depth(q, b):
|
||||
"""넓은 직사각 수로 Manning 을 h 에 대해 이분법으로 풂 — 헬퍼와 다른 풀이."""
|
||||
|
||||
def cap(h):
|
||||
return b * h * (1 / N) * ((b * h) / (b + 2 * h)) ** (2 / 3) * math.sqrt(S)
|
||||
|
||||
lo, hi = 0.0, 5.0
|
||||
for _ in range(100):
|
||||
mid = (lo + hi) / 2
|
||||
lo, hi = (mid, hi) if cap(mid) < q else (lo, mid)
|
||||
return hi
|
||||
|
||||
|
||||
def test_월류_높이는_폭과_연동_필요_수심_올림(out):
|
||||
for q, b, h in out["table"]:
|
||||
need = _manning_depth(q, b)
|
||||
assert h == pytest.approx(math.ceil(need * 100 - 1e-9) / 100), (q, b)
|
||||
# 폭이 넓을수록 높이가 낮아짐
|
||||
row = [h for q, b, h in out["table"] if q == 2.54]
|
||||
assert row == sorted(row, reverse=True) and len(set(row)) == len(row)
|
||||
assert out["none"] == [None, None] # 설계유량 · 폭이 없으면 손 입력
|
||||
assert out["hints"] == ["필요 수심 0.08 이상", "월류 폭을 넣으면 셈", "설계유량 없음 — 손 입력"]
|
||||
|
||||
|
||||
def test_정의_파일에_세월교_이름_표가_올라_있음():
|
||||
names = _node("names")
|
||||
with open(
|
||||
os.path.join(ROOT, "resources", "master_template", "구조물정의.json"), encoding="utf-8"
|
||||
) as f:
|
||||
doc = json.load(f)
|
||||
assert doc["종류"]["ford_bridge"]["이름"] == names
|
||||
@@ -0,0 +1,181 @@
|
||||
/* 52-4 — B05 · B06 좌측 「구조물 배치」 세월교 = 사용자 구조물 표 04 명세(컨테이너 셋)
|
||||
* 시작 화면: B05 종단설계(#/b05-profile) · 검증 프로젝트(sub6_검증) · 목록에 「세월교 1」
|
||||
* 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m524 = "단계";` 를 붙여
|
||||
* orca eval --expression "window.__m524='edit'; $(cat 이 파일)" → 고치고 [저장] (원래 관 지점은 sessionStorage 에 둠)
|
||||
* (orca reload 로 다시 열기)
|
||||
* orca eval --expression "window.__m524='verify'; $(cat 이 파일)" → 다시 연 값 · B06 같은 그리기
|
||||
* orca eval --expression "window.__m524='restore'; $(cat 이 파일)" → 원래 관 지점으로 되돌림(PUT) · 초안 비움
|
||||
* 쓰는 것: 검증 프로젝트 pipe_points.json(되돌림 단계가 원래 값으로 다시 씀)
|
||||
* 기대 수치(edit): 컨테이너 3(세월교 · 유입 구조 · 유출 구조 · 모두 필수) · 관경 800/1000/1200/1500 ·
|
||||
* 구조 없음/날개벽/흙막이 · 옛 관종 · 날개벽 칸 안 보임 · 폭 8 → 높이 = 필요 수심(설계유량 있으면) ·
|
||||
* 낮게 적으면 필요 수심으로 되돌림 · 수량 3 · 잇기 세월교 · 유입 지정 · 유출 지정 뒤 풂
|
||||
* 기대 수치(verify): 저장 값 그대로 · 같은 그리기(B05 = B06)
|
||||
*/
|
||||
(async () => {
|
||||
const MODE = window.__m524 || "edit";
|
||||
const NAME = /세월교 1$/;
|
||||
const w = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const until = async (fn, ms = 20000) => {
|
||||
for (let t = 0; t < ms; t += 200) {
|
||||
if (fn()) return true;
|
||||
await w(200);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const projectId = localStorage.getItem("frd_current_project_id");
|
||||
const modUrl = (part) =>
|
||||
performance
|
||||
.getEntriesByType("resource")
|
||||
.map((e) => e.name)
|
||||
.filter((n) => n.includes(part))
|
||||
.pop();
|
||||
const fetchApi = await import(modUrl("/B04_PreProcess/B04_PreProcess_Api_Fetch.ts"));
|
||||
const spec = () =>
|
||||
[...document.querySelectorAll(".b05-structure__spec")].find((n) => n.offsetParent !== null);
|
||||
const box = (c) => spec().querySelector(`[data-spec-container="${c}"]`);
|
||||
const ctl = (c, item) =>
|
||||
box(c)?.querySelector(`[data-spec-item="${c}.${item}"]`)?.querySelector("select, input");
|
||||
const set = async (c, item, v) => {
|
||||
const el = ctl(c, item);
|
||||
el.value = v;
|
||||
el.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await w(250);
|
||||
};
|
||||
const labels = (c, item) => [...ctl(c, item).options].filter((o) => o.value).map((o) => o.text);
|
||||
const pickFord = async () => {
|
||||
const find = () =>
|
||||
[...document.querySelectorAll(".b05-route__irregular-list li")].find(
|
||||
(l) => l.offsetParent !== null && NAME.test(l.textContent.trim()),
|
||||
);
|
||||
await until(() => !!find());
|
||||
const li = find();
|
||||
(li.querySelector("button") ?? li).click();
|
||||
await w(800);
|
||||
if (!spec()) (li.querySelector("button") ?? li).click();
|
||||
await until(() => !!spec());
|
||||
await w(500);
|
||||
};
|
||||
const shape = () =>
|
||||
[...spec().querySelectorAll("[data-spec-container]")].map((b) => ({
|
||||
c: b.dataset.specContainer,
|
||||
legend: b.querySelector("legend").firstChild.textContent,
|
||||
items: [...b.querySelectorAll("[data-spec-item]")].map((n) => [
|
||||
n.dataset.specItem,
|
||||
n.querySelector("select, input").value,
|
||||
]),
|
||||
link: b.querySelector(".b05-structure__summary-name")?.textContent,
|
||||
}));
|
||||
const assigned = (c) =>
|
||||
box(c).querySelector(".b05-structure__summary-name").classList.contains("is-assigned");
|
||||
const pickLink = async (c) => {
|
||||
box(c).querySelector('[data-b05-summary="pick"]').click();
|
||||
await until(() => document.querySelector(".ui-item-picker__tree .ui-tree__row"));
|
||||
for (let k = 0; k < 8; k++) {
|
||||
const rows = [...document.querySelectorAll(".ui-item-picker__tree .ui-tree__row")];
|
||||
const leaf = rows.find((r) => !r.classList.contains("is-branch"));
|
||||
if (leaf) {
|
||||
leaf.click();
|
||||
break;
|
||||
}
|
||||
rows.find((r) => r.querySelector(".ui-tree__caret")?.textContent === "▸")?.click();
|
||||
await w(300);
|
||||
}
|
||||
await w(300);
|
||||
[...document.querySelectorAll(".ui-item-picker__actions button")]
|
||||
.find((b) => b.textContent.includes("적용"))
|
||||
.click();
|
||||
await w(500);
|
||||
};
|
||||
const visibleKeys = () =>
|
||||
[...document.querySelectorAll(".b05-drainage__facility [data-option-keys]")]
|
||||
.filter((n) => n.offsetParent !== null)
|
||||
.map((n) => n.dataset.optionKeys);
|
||||
const out = { mode: MODE };
|
||||
|
||||
if (MODE === "edit") {
|
||||
const before = await fetchApi.fetchDetailPipePoints(projectId);
|
||||
sessionStorage.setItem("__524_original", JSON.stringify(before.pipe_points));
|
||||
await pickFord();
|
||||
const s0 = shape();
|
||||
out.containers = s0.map((b) => [b.c, b.legend]);
|
||||
out.oldFieldsVisible = visibleKeys(); // 옛 관종 · 월류 · 날개벽 칸 — 빈 목록이어야
|
||||
out.diameters = labels("ford", "diameter");
|
||||
out.inletChoices = labels("inlet", "structure");
|
||||
out.outletChoices = labels("outlet", "structure");
|
||||
out.start = s0.map((b) => b.items);
|
||||
await set("ford", "diameter", "1200");
|
||||
await set("ford", "width", "8");
|
||||
out.heightAfterWidth8 = ctl("ford", "height").value;
|
||||
out.heightHint = ctl("ford", "height").placeholder;
|
||||
await set("ford", "width", "4");
|
||||
out.heightAfterWidth4 = ctl("ford", "height").value;
|
||||
await set("ford", "height", "0.01");
|
||||
out.heightLowBounced = ctl("ford", "height").value;
|
||||
const raised = (Number(out.heightAfterWidth4 || 0) + 0.2).toFixed(2);
|
||||
await set("ford", "height", raised);
|
||||
out.heightRaisedKept = ctl("ford", "height").value === raised;
|
||||
await set("ford", "count", "3");
|
||||
await set("inlet", "structure", "흙막이");
|
||||
await set("outlet", "structure", "날개벽");
|
||||
await pickLink("ford");
|
||||
await pickLink("inlet");
|
||||
await pickLink("outlet");
|
||||
out.linksPicked = ["ford", "inlet", "outlet"].map(assigned);
|
||||
box("outlet").querySelector('[data-b05-summary="clear"]').click();
|
||||
await w(300);
|
||||
out.linkOutletCleared = !assigned("outlet");
|
||||
out.edited = shape().map((b) => b.items);
|
||||
document.querySelector('[data-dock-action="save"]').click();
|
||||
await w(500);
|
||||
out.saved = await until(() => !document.querySelector(".ui-overlay.is-active"), 60000);
|
||||
const after = await fetchApi.fetchDetailPipePoints(projectId);
|
||||
const p = after.pipe_points.find((x) => x.chainage_m > 170 && x.chainage_m < 176); // 세월교 1 = 8+13
|
||||
out.file = p && { options: p.options, group_items: Object.keys(p.group_items ?? {}) };
|
||||
}
|
||||
|
||||
if (MODE === "verify") {
|
||||
await pickFord();
|
||||
const b05 = shape();
|
||||
out.reopened = b05.map((b) => b.items);
|
||||
out.links = ["ford", "inlet", "outlet"].map(assigned);
|
||||
location.hash = "#/b06-section";
|
||||
await until(() => location.hash.includes("b06"), 5000);
|
||||
await w(6000);
|
||||
await pickFord();
|
||||
const b06 = shape();
|
||||
out.sameDrawing = JSON.stringify(b05) === JSON.stringify(b06);
|
||||
if (!out.sameDrawing) out.diff = { b05, b06 };
|
||||
out.b06WingSlots = ["inlet", "outlet"].map(
|
||||
(c) => box(c).querySelector(".b05-structure__adjust-slot")?.childElementCount ?? -1,
|
||||
);
|
||||
}
|
||||
|
||||
if (MODE === "restore") {
|
||||
const original = JSON.parse(sessionStorage.getItem("__524_original") || "null");
|
||||
if (!original) return JSON.stringify({ error: "원래 값 없음" });
|
||||
const keys = [
|
||||
"chainage_m",
|
||||
"source",
|
||||
"facility",
|
||||
"start_m",
|
||||
"end_m",
|
||||
"options",
|
||||
"summary_item",
|
||||
"group_items",
|
||||
];
|
||||
const points = original.map((p) =>
|
||||
Object.fromEntries(
|
||||
keys.filter((k) => p[k] !== undefined && p[k] !== null).map((k) => [k, p[k]]),
|
||||
),
|
||||
);
|
||||
await fetchApi.saveDetailPipePoints(projectId, points);
|
||||
for (const k of Object.keys(sessionStorage))
|
||||
if (k.startsWith("aislo:draft:pipes:") || k === "__524_original")
|
||||
sessionStorage.removeItem(k);
|
||||
const now = await fetchApi.fetchDetailPipePoints(projectId);
|
||||
const pick = (list) =>
|
||||
JSON.stringify(list.map((p) => [p.chainage_m, p.options ?? {}, p.group_items ?? {}]));
|
||||
out.restored = pick(now.pipe_points) === pick(original);
|
||||
}
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
Reference in New Issue
Block a user