Merge remote-tracking branch 'origin/dev' into main_laptop_1
This commit is contained in:
@@ -26,10 +26,6 @@
|
||||
* 1m·길이 2m·각도 45°가 기본이며, 설치를 "없음"으로 바꾸면 제원 칸이 접힌다.
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
FORD_BRIDGE_DEFAULT_WIDTH_M,
|
||||
FORD_PAVEMENT_DEFAULT_WIDTH_M,
|
||||
} from "@config/config_frontend";
|
||||
import type {
|
||||
DetailPipeInput,
|
||||
PipeFacility,
|
||||
@@ -42,7 +38,6 @@ import {
|
||||
createRevetmentFields,
|
||||
createRevetSideGroup,
|
||||
createWingFields,
|
||||
fordSection,
|
||||
grid,
|
||||
group,
|
||||
INLET_KINDS,
|
||||
@@ -60,6 +55,7 @@ import {
|
||||
WING_OUT_KEYS,
|
||||
} from "./B05_Profile_UI_Drainage_Facility_Fields";
|
||||
import { pipeWallMinHeightM } from "../common_util/common_util_culvert_sets";
|
||||
import { createFordFields } from "./B05_Profile_UI_Drainage_Facility_Ford";
|
||||
|
||||
export type { InletStructureKind }; // 목록은 700줄 한계로 `_Fields` 로 옮김 — 부르던 곳은 그대로
|
||||
|
||||
@@ -191,17 +187,13 @@ export function createFacilityOptionsForm(
|
||||
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 = "파형강관";
|
||||
// 관종 파형강관·관경 1000(사용자 확정 기본)은 **제안으로만** — 「안 정함」이 실제 상태(판정 ③).
|
||||
const pipeMaterial = optionalSelect("— 안 정함 (제안 파형강관) —", ["흄관", "VR관", "파형강관"]);
|
||||
const pipeDiameter = document.createElement("select");
|
||||
pipeDiameter.replaceChildren(
|
||||
new Option("— 안 정함 (제안 Ø1000) —", ""),
|
||||
...["800", "1000", "1200", "1500"].map((size) => new Option(`Ø${size}`, size)),
|
||||
);
|
||||
pipeDiameter.value = "1000"; // 별표2 원칙값 + 사용자 확정 기본 (2026-08-17)
|
||||
// 관보호공 날개벽 — **개소당 원단위**가 형식마다 붙박이라 고르는 칸이 있어야 물량이 선다
|
||||
// (2026-09-09, 계획서 4-13 · 소광리 원본 탭 다섯). 비우면 날개벽을 안 센다.
|
||||
const wingWall = optionalSelect("안 놓음", ["A-TYPE", "C-TYPE", "A-TYPE+집수정"]);
|
||||
@@ -210,11 +202,12 @@ export function createFacilityOptionsForm(
|
||||
|
||||
// ── 유입구 — 집수정 또는 기슭막이 택일(사용자 지시 5~9) ─────────────────
|
||||
const inletGroup = group("유입구");
|
||||
// 빈값(미지정)을 두면 하위 칸이 전부 숨어 기본값이 안 보인다 — 기슭막이를 기본으로
|
||||
// 두고 저유량 지점에서 집수정으로 바꾼다(2026-08-17 사용자 지시).
|
||||
// 「안 정함」이면 기슭막이 칸을 보이되(제안) 값은 안 실음 — 기본값을 몰래 확정으로 안 바꿈(판정 ③).
|
||||
const inletType = document.createElement("select");
|
||||
inletType.replaceChildren(...INLET_KINDS.map((kind) => new Option(kind.label, kind.label)));
|
||||
inletType.value = INLET_KINDS[0].label;
|
||||
inletType.replaceChildren(
|
||||
new Option("— 안 정함 (제안 기슭막이) —", ""),
|
||||
...INLET_KINDS.map((kind) => new Option(kind.label, kind.label)),
|
||||
);
|
||||
const basinForm = optionalSelect("선택", [
|
||||
"동물이동형",
|
||||
"□형(기본형)",
|
||||
@@ -230,12 +223,12 @@ export function createFacilityOptionsForm(
|
||||
]);
|
||||
const basinMaterialField = labeled("형태", basinMaterial);
|
||||
// 집수정 종방향 길이 — 3D 예상형상용, 전후 동일 배분·기본 2m(2026-08-23 사용자).
|
||||
const basinLength = numberInput("0.1");
|
||||
basinLength.value = "2";
|
||||
const basinLength = numberInput("0.1", "0", "제안 2");
|
||||
const basinRows = [
|
||||
grid(labeled("형식", basinForm), labeled("길이 (m)", stepper(basinLength, 1))),
|
||||
];
|
||||
const height = (): string => String(pipeWallMinHeightM(Number(pipeDiameter.value) / 1000));
|
||||
const height = (): string =>
|
||||
String(pipeWallMinHeightM(Number(pipeDiameter.value || "1000") / 1000));
|
||||
const inletRevet = createRevetmentFields(INLET_REVET_KEYS, {
|
||||
...REVET_COMMON_DEFAULTS,
|
||||
form: "돌쌓기(찰)",
|
||||
@@ -257,8 +250,7 @@ export function createFacilityOptionsForm(
|
||||
|
||||
// ── 유출구 — 구조 선택지는 기슭막이뿐이지만 양식을 맞춘다(사용자 지시 2·4) ──
|
||||
const outletGroup = group("유출구");
|
||||
const outletType = document.createElement("select");
|
||||
outletType.replaceChildren(new Option("기슭막이", "기슭막이"));
|
||||
const outletType = optionalSelect("— 안 정함 (제안 기슭막이) —", ["기슭막이"]);
|
||||
const outletRevet = createRevetmentFields(OUTLET_REVET_KEYS, {
|
||||
...REVET_COMMON_DEFAULTS,
|
||||
form: "돌쌓기(메)",
|
||||
@@ -284,8 +276,7 @@ export function createFacilityOptionsForm(
|
||||
// 나눈다(2026-08-30 사용자). 설치 측이 한쪽이면 그 칸만 뜬다.
|
||||
// 좌·우가 저장 채널(유입/유출) 중 어느 쪽인지는 측점 지형이 정하므로 이름표만
|
||||
// 바꿔 단다(`setRevetSideLabels`) — 값은 언제나 그 채널 키로 오간다.
|
||||
const revetSide = optionalSelect("— 안 정함 —", ["양쪽", "좌", "우"]);
|
||||
revetSide.value = "양쪽";
|
||||
const revetSide = optionalSelect("— 안 정함 (제안 양쪽) —", ["양쪽", "좌", "우"]);
|
||||
const revetTopRow = grid(labeled("설치 측", revetSide));
|
||||
const revetInlet = createRevetSideGroup(INLET_REVET_KEYS, "돌쌓기(메)");
|
||||
const revetOutlet = createRevetSideGroup(OUTLET_REVET_KEYS, "돌쌓기(메)");
|
||||
@@ -323,12 +314,12 @@ export function createFacilityOptionsForm(
|
||||
|
||||
// ── BOX암거 — 본체 규격(프리셋 + 사용자 지정)·날개벽(유입·유출 개별) ────
|
||||
// 폭·높이 자유 입력은 "사용자 지정"을 골랐을 때만 펼친다(2026-08-17 사용자 확정).
|
||||
const boxSize = optionalSelect("미지정", [
|
||||
const boxSize = optionalSelect(`— 안 정함 (제안 ${BOX_SIZE_PRESETS[0][0]}) —`, [
|
||||
...BOX_SIZE_PRESETS.map(([label]) => label),
|
||||
BOX_SIZE_CUSTOM,
|
||||
]);
|
||||
const boxWidth = numberInput("0.1");
|
||||
const boxHeight = numberInput("0.1");
|
||||
const boxWidth = numberInput("0.1", "0", `제안 ${BOX_SIZE_PRESETS[0][1]}`);
|
||||
const boxHeight = numberInput("0.1", "0", `제안 ${BOX_SIZE_PRESETS[0][2]}`);
|
||||
const boxCustomRow = grid(labeled("본체 폭 (m)", boxWidth), labeled("본체 높이 (m)", boxHeight));
|
||||
const boxWrap = document.createElement("div");
|
||||
boxWrap.append(grid(labeled("본체 규격 (m)", boxSize)), boxCustomRow);
|
||||
@@ -343,106 +334,43 @@ export function createFacilityOptionsForm(
|
||||
boxWidth.value = String(preset[1]);
|
||||
boxHeight.value = String(preset[2]);
|
||||
}
|
||||
boxCustomRow.hidden = !!preset;
|
||||
// 「안 정함」도 폭·높이 칸을 접음 — 값이 없는 상태 그대로(제안은 빈 보기 이름).
|
||||
boxCustomRow.hidden = boxSize.value !== BOX_SIZE_CUSTOM;
|
||||
}
|
||||
|
||||
// ── 세월교 — 구체 내 배관은 배수관과 같은 관종·관경 칸을 쓰고(2026-08-17 사용자
|
||||
// 지시) 수량만 따로 받는다.
|
||||
const fordCount = numberInput("1", "1", "련");
|
||||
// 숫자 칸은 기슭막이·집수정과 같은 [-][값][+] 묶음(2026-08-30 사용자 지시 3).
|
||||
// 련은 정수라 소수 자릿수를 두지 않는다.
|
||||
const fordRow = grid(labeled("수량 (련)", stepper(fordCount, 1, 0)));
|
||||
// ── 물넘이·세월교 — 월류 폭·높이·경사·두께·길이·련 수(`_Ford` · 기본값은 제안으로만).
|
||||
const ford = createFordFields(() => emit());
|
||||
|
||||
// ── 물넘이·세월교 개략 단면 — 월류 폭 + 월류 높이 한 행(2026-08-18 사용자 지시).
|
||||
// 폭 기본값은 세월교 10m·물넘이 포장 5m(사용자 확정 — 지식DB 폭 수치 근거 없음).
|
||||
// 높이는 설계유량·폭으로 되짚은 필요 수심을 자동으로 채우고, 사용자가 키울 수는
|
||||
// 있으나 계산값(필요 최소 수심) 미만은 되돌린다.
|
||||
const fordWidth = numberInput("0.1");
|
||||
const fordHeight = numberInput("0.01");
|
||||
const fordWidthRow = grid(
|
||||
labeled("월류 폭 (m)", stepper(fordWidth, 0.1)),
|
||||
labeled("월류 높이 (m)", stepper(fordHeight, 0.1)),
|
||||
);
|
||||
// 물넘이 바닥은 유입(상류)이 높고 유출이 낮게 기운다(2026-08-28 사용자 확정).
|
||||
// 비우면 그 측점의 **노면 횡단경사**를 그대로 쓴다 — 횡단도가 판단한다.
|
||||
const fordSlope = numberInput("0.1");
|
||||
fordSlope.placeholder = "노면 기울기";
|
||||
// 포장 두께·노폭 방향 길이 — 수량(㎡ = 월류 폭 × 길이 · 두께로 원단위 고름)이 읽는 칸(2026-09-14 A3).
|
||||
// 비우면 표가 안 서고 그 까닭이 뜸 — 지어내지 않는다.
|
||||
const fordThickness = numberInput("1");
|
||||
const fordLength = numberInput("0.1");
|
||||
const fordSlopeRow = grid(
|
||||
labeled("바닥 경사 유입→유출 (%)", stepper(fordSlope, 0.1)),
|
||||
labeled("포장 두께 (㎝)", stepper(fordThickness, 1, 0)),
|
||||
labeled("포장 길이 노폭 방향 (m)", stepper(fordLength, 0.1)),
|
||||
);
|
||||
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;
|
||||
/** 직전에 자동으로 채워 넣은 필요 수심(m) — 칸이 아직 그 값이면 "자동"으로 보고
|
||||
* 월류 폭이 바뀔 때마다 계산값을 계속 따라가게 한다. 사용자가 다른 값을 넣으면
|
||||
* 그때부터 그 값이 이긴다(2026-08-30 사용자: 폭을 바꿔도 높이가 안 따라온다). */
|
||||
let fordAutoDepthM: 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);
|
||||
const untouched = fordAutoDepthM !== null && Math.abs(current - fordAutoDepthM) < 0.005;
|
||||
if (!Number.isFinite(current) || current < min || untouched)
|
||||
fordHeight.value = min.toFixed(2);
|
||||
fordAutoDepthM = min;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
// [제안값 넣기] — 누른 때만 빈 칸에 제안값(관종·관경·구조·집수정 길이·설치 측·BOX 규격·날개벽·월류).
|
||||
// 벽 칸(기슭막이)은 벽마다 단추가 따로 있음. 기본값을 몰래 확정으로 바꾸지 않는다(2026-09-14 판정 ③).
|
||||
const suggest = document.createElement("button");
|
||||
suggest.type = "button";
|
||||
suggest.className = "b05-structure__suggest";
|
||||
suggest.textContent = "제안값 넣기";
|
||||
suggest.addEventListener("click", () => {
|
||||
if (!pipeMaterial.value) pipeMaterial.value = "파형강관";
|
||||
if (!pipeDiameter.value) pipeDiameter.value = "1000";
|
||||
if (!inletType.value) inletType.value = INLET_KINDS[0].label;
|
||||
if (!outletType.value) outletType.value = "기슭막이";
|
||||
if (!basinLength.value) basinLength.value = "2";
|
||||
if (!revetSide.value) revetSide.value = "양쪽";
|
||||
if (current === "box_culvert" && !boxSize.value) boxSize.value = BOX_SIZE_PRESETS[0][0];
|
||||
wingInFields.fillSuggested();
|
||||
wingOutFields.fillSuggested();
|
||||
if (current === "ford_pavement" || current === "ford_bridge") ford.fillSuggested(current);
|
||||
emit();
|
||||
});
|
||||
|
||||
// 세월교·물넘이 항목은 **관종·관경 바로 다음**에 둔다(2026-08-30 사용자 지시 2) —
|
||||
// 월류 폭·높이 → 바닥 경사 → 수량 → 개략 단면 결과. 다른 시설에서는 전부 숨는다.
|
||||
root.append(
|
||||
grid(suggest),
|
||||
pipeRow,
|
||||
wingRow,
|
||||
fordWidthRow,
|
||||
fordSlopeRow,
|
||||
fordRow,
|
||||
fordSummary,
|
||||
ford.widthRow,
|
||||
ford.slopeRow,
|
||||
ford.countRow,
|
||||
ford.summary,
|
||||
inletGroup.root,
|
||||
outletGroup.root,
|
||||
extraGroup.root,
|
||||
@@ -473,24 +401,23 @@ export function createFacilityOptionsForm(
|
||||
wingInFields.root.hidden = !hasWing;
|
||||
wingOutFields.root.hidden = !hasWing;
|
||||
const isFord = current === "ford_pavement" || current === "ford_bridge";
|
||||
fordRow.hidden = current !== "ford_bridge";
|
||||
fordWidthRow.hidden = !isFord;
|
||||
ford.countRow.hidden = current !== "ford_bridge";
|
||||
ford.widthRow.hidden = !isFord;
|
||||
// 바닥 경사는 물넘이포장만 쓴다 — 세월교는 구체 위 노면이라 파임이 없다.
|
||||
fordSlopeRow.hidden = current !== "ford_pavement";
|
||||
fordSummary.hidden = !isFord;
|
||||
ford.slopeRow.hidden = current !== "ford_pavement";
|
||||
ford.summary.hidden = !isFord;
|
||||
const isRevet = current === "revetment";
|
||||
revetTopRow.hidden = !isRevet;
|
||||
// 설치 측이 한쪽이면 그쪽 칸만 남긴다 — 이름표는 측점이 정한 좌·우다.
|
||||
revetInlet.root.hidden =
|
||||
!isRevet || (revetSide.value !== "양쪽" && revetSide.value !== revetSideLabels.inlet);
|
||||
revetOutlet.root.hidden =
|
||||
!isRevet || (revetSide.value !== "양쪽" && revetSide.value !== revetSideLabels.outlet);
|
||||
// 설치 측이 한쪽이면 그쪽 칸만 남긴다 — 이름표는 측점이 정한 좌·우다(안 정함 = 양쪽처럼 보임).
|
||||
const side = revetSide.value || "양쪽";
|
||||
revetInlet.root.hidden = !isRevet || (side !== "양쪽" && side !== revetSideLabels.inlet);
|
||||
revetOutlet.root.hidden = !isRevet || (side !== "양쪽" && side !== revetSideLabels.outlet);
|
||||
revetInlet.legend.textContent = `기슭막이 (${revetSideLabels.inlet})`;
|
||||
revetOutlet.legend.textContent = `기슭막이 (${revetSideLabels.outlet})`;
|
||||
// 추가 기슭막이 칸은 **내용이 있을 때만** 선다 — 고른 다단 벽의 행이 여기 오면 뜨고,
|
||||
// 선택이 풀리면 스스로 사라진다.
|
||||
extraGroup.root.hidden = extraSlot.childElementCount === 0;
|
||||
if (isFord) syncFordSummary();
|
||||
if (isFord) ford.sync();
|
||||
if (isBox) syncBoxSize();
|
||||
if (hasWing) {
|
||||
wingInFields.syncVisibility();
|
||||
@@ -509,12 +436,6 @@ export function createFacilityOptionsForm(
|
||||
}
|
||||
}
|
||||
|
||||
/** 월류 높이는 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?.();
|
||||
@@ -536,8 +457,7 @@ export function createFacilityOptionsForm(
|
||||
boxHeight,
|
||||
...wingInFields.inputs,
|
||||
...wingOutFields.inputs,
|
||||
fordCount,
|
||||
fordWidth,
|
||||
...ford.inputs,
|
||||
].forEach((input) => input.addEventListener("change", emit));
|
||||
|
||||
/** 유입구 구조 변경 수신자(B06 유입측 형식 제어). B05는 비워 둔다. */
|
||||
@@ -574,35 +494,36 @@ export function createFacilityOptionsForm(
|
||||
},
|
||||
setFacility(facility, options = {}, designFlow = null) {
|
||||
current = facility;
|
||||
designFlowM3s = designFlow ?? null;
|
||||
ford.setDesignFlow(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";
|
||||
// ⚠ 저장된 값만 칸에 — 없으면 「안 정함」 · 제안은 빈 보기 이름·회색 글씨(2026-09-14 판정 ③).
|
||||
pipeMaterial.value = text("pipe_kind");
|
||||
pipeDiameter.value = text("pipe_diameter_mm");
|
||||
// 병합 목록: 저장된 정본 둘(inlet_type·inlet_structure)로 라벨을 되찾는다.
|
||||
const savedType = text("inlet_type") || "기슭막이";
|
||||
const savedType = text("inlet_type");
|
||||
const savedStructure = text("inlet_structure");
|
||||
inletType.value = (
|
||||
INLET_KINDS.find(
|
||||
(kind) =>
|
||||
kind.type === savedType && (!savedStructure || kind.structure === savedStructure),
|
||||
) ?? inletKindOf(savedType)
|
||||
).label;
|
||||
outletType.value = text("outlet_type") || "기슭막이";
|
||||
inletType.value = savedType
|
||||
? (
|
||||
INLET_KINDS.find(
|
||||
(kind) =>
|
||||
kind.type === savedType && (!savedStructure || kind.structure === savedStructure),
|
||||
) ?? inletKindOf(savedType)
|
||||
).label
|
||||
: "";
|
||||
outletType.value = text("outlet_type");
|
||||
wingWall.value = text("wing_wall_type");
|
||||
basinForm.value = text("inlet_basin_form");
|
||||
basinMaterial.value = text("inlet_basin_material");
|
||||
basinLength.value = text("inlet_basin_length_m") || "2";
|
||||
basinLength.value = text("inlet_basin_length_m");
|
||||
inletRevet.write(options);
|
||||
outletRevet.write(options);
|
||||
// 저장된 폭·높이가 프리셋과 맞으면 그 프리셋을, 아니면 "사용자 지정"을 고른다.
|
||||
// 값이 없으면 첫 프리셋(2.0×2.0)이 기본이다.
|
||||
const [firstLabel, firstWidth, firstHeight] = BOX_SIZE_PRESETS[0];
|
||||
// 값이 없으면 「안 정함」(첫 프리셋 2.0×2.0 은 제안으로만).
|
||||
const savedWidth = Number.parseFloat(text("body_width_m"));
|
||||
const savedHeight = Number.parseFloat(text("body_height_m"));
|
||||
const hasSaved =
|
||||
@@ -616,45 +537,38 @@ export function createFacilityOptionsForm(
|
||||
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);
|
||||
boxSize.value = hasSaved ? (preset?.[0] ?? BOX_SIZE_CUSTOM) : "";
|
||||
boxWidth.value = hasSaved ? String(savedWidth) : "";
|
||||
boxHeight.value = hasSaved ? String(savedHeight) : "";
|
||||
wingInFields.write(options);
|
||||
wingOutFields.write(options);
|
||||
fordCount.value = isFord ? text("pipe_count") : "";
|
||||
fordWidth.value = text("ford_width_m");
|
||||
fordHeight.value = text("ford_height_m");
|
||||
// 새 시설을 올리는 참이다 — 저장된 높이는 사용자 값으로 보고 자동 추적을 끊는다.
|
||||
fordAutoDepthM = null;
|
||||
fordSlope.value = text("ford_slope_pct");
|
||||
fordThickness.value = text("thickness_cm");
|
||||
fordLength.value = text("length_m");
|
||||
revetSide.value = text("side") || "양쪽";
|
||||
if (facility === "ford_pavement" || facility === "ford_bridge") ford.write(facility, options);
|
||||
revetSide.value = text("side");
|
||||
const spread = legacyRevetOptions(options);
|
||||
revetInlet.fields.write(spread);
|
||||
revetOutlet.fields.write(spread);
|
||||
// 월류 폭 기본값 — 세월교 10m·물넘이 포장 5m(2026-08-18 사용자 확정, config 정의처).
|
||||
if (!fordWidth.value && (facility === "ford_pavement" || facility === "ford_bridge")) {
|
||||
fordWidth.value = String(
|
||||
facility === "ford_bridge" ? FORD_BRIDGE_DEFAULT_WIDTH_M : FORD_PAVEMENT_DEFAULT_WIDTH_M,
|
||||
);
|
||||
}
|
||||
syncVisibility();
|
||||
},
|
||||
setDesignFlow(designFlow) {
|
||||
designFlowM3s = designFlow ?? null;
|
||||
ford.setDesignFlow(designFlow ?? null);
|
||||
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") {
|
||||
options.pipe_diameter_mm = Number(pipeDiameter.value);
|
||||
options.pipe_kind = pipeMaterial.value;
|
||||
putPipe();
|
||||
const inletKind = inletKindOf(inletType.value);
|
||||
if (wingWall.value) options.wing_wall_type = wingWall.value;
|
||||
options.inlet_type = inletKind.type;
|
||||
// B06 유입측 형식(조정창과 같은 값) — 병합 드롭다운이 함께 정한다(2026-08-29).
|
||||
options.inlet_structure = inletKind.structure;
|
||||
if (inletType.value) {
|
||||
options.inlet_type = inletKind.type;
|
||||
options.inlet_structure = inletKind.structure;
|
||||
}
|
||||
if (inletKind.type === "집수정") {
|
||||
if (basinForm.value) options.inlet_basin_form = basinForm.value;
|
||||
if (basinMaterial.value) options.inlet_basin_material = basinMaterial.value;
|
||||
@@ -662,7 +576,7 @@ export function createFacilityOptionsForm(
|
||||
} else {
|
||||
inletRevet.read(options);
|
||||
}
|
||||
options.outlet_type = outletType.value;
|
||||
if (outletType.value) options.outlet_type = outletType.value;
|
||||
outletRevet.read(options);
|
||||
} else if (current === "box_culvert") {
|
||||
// 프리셋을 골랐든 사용자 지정을 적었든 정본은 폭·높이 두 칸이다.
|
||||
@@ -671,22 +585,14 @@ export function createFacilityOptionsForm(
|
||||
wingInFields.read(options);
|
||||
wingOutFields.read(options);
|
||||
} else if (current === "ford_pavement") {
|
||||
putNumber(options, "ford_width_m", fordWidth.value);
|
||||
putFordHeight(options);
|
||||
putNumber(options, "ford_slope_pct", fordSlope.value);
|
||||
putNumber(options, "thickness_cm", fordThickness.value);
|
||||
putNumber(options, "length_m", fordLength.value);
|
||||
ford.read(current, 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);
|
||||
putPipe();
|
||||
ford.read(current, options);
|
||||
wingInFields.read(options);
|
||||
wingOutFields.read(options);
|
||||
} else if (current === "revetment") {
|
||||
options.side = revetSide.value;
|
||||
if (revetSide.value) options.side = revetSide.value;
|
||||
// 단 수는 폼이 갖지 않는다 — 횡단 설계 patch(extra_wall_counts)가 정본이고
|
||||
// 조작은 [추가 기슭막이(단)] 행이 한다(2026-08-30 사용자: 중복이라 삭제).
|
||||
// 좌·우가 각자 값을 갖는다 — 저장 채널은 유입/유출 키 그대로다(2026-08-30).
|
||||
|
||||
@@ -450,19 +450,18 @@ interface WingFields {
|
||||
syncVisibility: () => void;
|
||||
write: (options: Record<string, string | number>) => void;
|
||||
read: (target: Record<string, string | number>) => void;
|
||||
/** [제안값 넣기] — 빈 칸에만 기본 제원을 넣음. */
|
||||
fillSuggested: () => void;
|
||||
}
|
||||
|
||||
/** 날개벽 한 벌(설치 유무 + 짧은쪽 높이·길이·각도) — 유입·유출이 공유하는 조각.
|
||||
* 설치 "없음"이면 제원 칸을 접는다(기슭막이 보호공 치수와 같은 패턴). */
|
||||
* 설치 "없음"이면 제원 칸을 접는다(기슭막이 보호공 치수와 같은 패턴).
|
||||
* ⚠ 기본 제원(있음·1·2·45, 사용자 확정)은 **제안으로만** — 칸은 비워 둠(2026-09-14 판정 ③). */
|
||||
export function createWingFields(title: string, keys: WingKeys): WingFields {
|
||||
const install = optionalSelect("미지정", ["있음", "없음"]);
|
||||
install.value = WING_DEFAULTS.install;
|
||||
const height = numberInput("0.1");
|
||||
height.value = WING_DEFAULTS.height;
|
||||
const length = numberInput("0.1");
|
||||
length.value = WING_DEFAULTS.length;
|
||||
const angle = numberInput("1");
|
||||
angle.value = WING_DEFAULTS.angle;
|
||||
const install = optionalSelect(`— 안 정함 (제안 ${WING_DEFAULTS.install}) —`, ["있음", "없음"]);
|
||||
const height = numberInput("0.1", "0", `제안 ${WING_DEFAULTS.height}`);
|
||||
const length = numberInput("0.1", "0", `제안 ${WING_DEFAULTS.length}`);
|
||||
const angle = numberInput("1", "0", `제안 ${WING_DEFAULTS.angle}`);
|
||||
|
||||
// 숫자 칸은 기슭막이·집수정과 같은 [-][값][+] 묶음으로 맞춘다(2026-08-30 사용자).
|
||||
const heightField = labeled("짧은쪽 높이 (m)", stepper(height, 0.1));
|
||||
@@ -481,7 +480,6 @@ export function createWingFields(title: string, keys: WingKeys): WingFields {
|
||||
dimsRow.hidden = off;
|
||||
}
|
||||
|
||||
// 만들자마자 기본값("있음") 상태를 맞춘다.
|
||||
syncVisibility();
|
||||
|
||||
return {
|
||||
@@ -492,18 +490,25 @@ export function createWingFields(title: string, keys: WingKeys): WingFields {
|
||||
write(options) {
|
||||
const text = (key: string): string =>
|
||||
options[key] !== undefined ? String(options[key]) : "";
|
||||
install.value = text(keys.install) || WING_DEFAULTS.install;
|
||||
height.value = text(keys.height) || WING_DEFAULTS.height;
|
||||
length.value = text(keys.length) || WING_DEFAULTS.length;
|
||||
angle.value = text(keys.angle) || WING_DEFAULTS.angle;
|
||||
install.value = text(keys.install);
|
||||
height.value = text(keys.height);
|
||||
length.value = text(keys.length);
|
||||
angle.value = text(keys.angle);
|
||||
syncVisibility();
|
||||
},
|
||||
read(target) {
|
||||
target[keys.install] = install.value;
|
||||
if (install.value !== "있음") return;
|
||||
if (install.value) target[keys.install] = install.value;
|
||||
if (install.value === "없음") return;
|
||||
putNumber(target, keys.height, height.value);
|
||||
putNumber(target, keys.length, length.value);
|
||||
putNumber(target, keys.angle, angle.value);
|
||||
},
|
||||
fillSuggested() {
|
||||
if (!install.value) install.value = WING_DEFAULTS.install;
|
||||
if (!height.value) height.value = WING_DEFAULTS.height;
|
||||
if (!length.value) length.value = WING_DEFAULTS.length;
|
||||
if (!angle.value) angle.value = WING_DEFAULTS.angle;
|
||||
syncVisibility();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile_UI_Drainage_Facility_Ford.ts
|
||||
* 물넘이포장·세월교 칸 묶음 — 월류 폭·높이·바닥 경사·포장 두께·길이·련 수 + 개략 단면 줄.
|
||||
*
|
||||
* 서브폼 본체(`_Drainage_Facility.ts`)가 700줄에 닿아 떼어 냄(2026-09-14).
|
||||
* ⚠ 기본값을 칸에 안 채움(2026-09-14 브레인 판정 「기본값을 몰래 확정으로 바꾸지 않는다」):
|
||||
* 월류 폭 세월교 10m·물넘이 5m(사용자 확정)는 **회색 제안**, 월류 높이는 설계유량·폭으로 되짚은
|
||||
* **필요 최소 수심을 회색 제안**으로만 — 사용자가 적거나 [제안값 넣기]를 누른 때만 값.
|
||||
* 적은 높이가 필요 수심보다 낮으면 여전히 되돌림(계산값 미만은 안 받음).
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
FORD_BRIDGE_DEFAULT_WIDTH_M,
|
||||
FORD_PAVEMENT_DEFAULT_WIDTH_M,
|
||||
} from "@config/config_frontend";
|
||||
import {
|
||||
fordSection,
|
||||
grid,
|
||||
labeled,
|
||||
numberInput,
|
||||
putNumber,
|
||||
stepper,
|
||||
} from "./B05_Profile_UI_Drainage_Facility_Fields";
|
||||
|
||||
type FordKind = "ford_pavement" | "ford_bridge";
|
||||
|
||||
export interface FordFields {
|
||||
widthRow: HTMLElement;
|
||||
slopeRow: HTMLElement;
|
||||
countRow: HTMLElement;
|
||||
summary: HTMLElement;
|
||||
/** 바뀌면 저장 흐름을 울릴 칸(높이는 자기 검사 뒤 스스로 울림). */
|
||||
inputs: HTMLInputElement[];
|
||||
setDesignFlow: (designFlow: number | null) => void;
|
||||
sync: () => void;
|
||||
write: (kind: FordKind, options: Record<string, string | number>) => void;
|
||||
read: (kind: FordKind, target: Record<string, string | number>) => void;
|
||||
fillSuggested: (kind: FordKind) => void;
|
||||
}
|
||||
|
||||
export function createFordFields(emit: () => void): FordFields {
|
||||
// 세월교 — 구체 내 배관 수량(련). 련은 정수라 소수 자릿수를 두지 않는다.
|
||||
const count = numberInput("1", "1", "련");
|
||||
const countRow = grid(labeled("수량 (련)", stepper(count, 1, 0)));
|
||||
const width = numberInput("0.1");
|
||||
const height = numberInput("0.01");
|
||||
const widthRow = grid(
|
||||
labeled("월류 폭 (m)", stepper(width, 0.1)),
|
||||
labeled("월류 높이 (m)", stepper(height, 0.1)),
|
||||
);
|
||||
// 물넘이 바닥은 유입(상류)이 높고 유출이 낮게 기운다(2026-08-28 사용자 확정).
|
||||
// 비우면 그 측점의 **노면 횡단경사**를 그대로 쓴다 — 횡단도가 판단한다.
|
||||
const slope = numberInput("0.1");
|
||||
slope.placeholder = "노면 기울기";
|
||||
// 포장 두께·노폭 방향 길이 — 수량(㎡ = 월류 폭 × 길이 · 두께로 원단위 고름)이 읽는 칸(A3).
|
||||
const thickness = numberInput("1");
|
||||
const length = numberInput("0.1");
|
||||
const slopeRow = grid(
|
||||
labeled("바닥 경사 유입→유출 (%)", stepper(slope, 0.1)),
|
||||
labeled("포장 두께 (㎝)", stepper(thickness, 1, 0)),
|
||||
labeled("포장 길이 노폭 방향 (m)", stepper(length, 0.1)),
|
||||
);
|
||||
const summary = document.createElement("p");
|
||||
summary.className = "b05-drainage__facility-note";
|
||||
let designFlowM3s: number | null = null;
|
||||
/** 현재 조건(설계유량·월류 폭)의 필요 최소 수심(m). 계산 불가면 null. */
|
||||
let minDepthM: number | null = null;
|
||||
|
||||
const suggestedWidth = (kind: FordKind): number =>
|
||||
kind === "ford_bridge" ? FORD_BRIDGE_DEFAULT_WIDTH_M : FORD_PAVEMENT_DEFAULT_WIDTH_M;
|
||||
|
||||
function sync(): void {
|
||||
const widthM = Number.parseFloat(width.value || width.dataset.suggested || "");
|
||||
const section = designFlowM3s !== null ? fordSection(designFlowM3s, widthM) : null;
|
||||
minDepthM = section ? Number(section.depthM.toFixed(2)) : null;
|
||||
height.placeholder = minDepthM !== null ? `제안 필요 수심 ${minDepthM.toFixed(2)}` : "";
|
||||
if (designFlowM3s === null) {
|
||||
summary.textContent =
|
||||
"담당 유역의 설계유량이 아직 없습니다 — [유역 분석] 후 개략 단면이 나옵니다.";
|
||||
return;
|
||||
}
|
||||
const head = `설계유량 ${designFlowM3s.toFixed(3)} ㎥/s`;
|
||||
if (!section) {
|
||||
summary.textContent = `${head} · 월류 폭을 넣으면 필요 수심·단면을 계산합니다.`;
|
||||
return;
|
||||
}
|
||||
// 필요 최소 수심이다 — 월류 높이는 이 값 아래로 내릴 수 없다.
|
||||
summary.textContent =
|
||||
`${head} · 필요 수심 ${section.depthM.toFixed(2)} m · ` +
|
||||
`필요 단면 ${section.areaM2.toFixed(2)} ㎡ · 유속 ${section.velocityMs.toFixed(2)} m/s — ` +
|
||||
`월류 높이는 필요 수심 이상만 입력됩니다.`;
|
||||
}
|
||||
|
||||
// 적은 값이 계산값 미만이면 받지 않는다 — 계산값으로 되돌리고 잠깐 붉힌다(빈 칸은 그대로 빈 칸).
|
||||
height.addEventListener("change", () => {
|
||||
const value = Number.parseFloat(height.value);
|
||||
if (
|
||||
minDepthM !== null &&
|
||||
height.value !== "" &&
|
||||
(!Number.isFinite(value) || value < minDepthM)
|
||||
) {
|
||||
height.value = minDepthM.toFixed(2);
|
||||
height.classList.add("is-invalid");
|
||||
window.setTimeout(() => height.classList.remove("is-invalid"), 900);
|
||||
}
|
||||
emit();
|
||||
});
|
||||
|
||||
return {
|
||||
widthRow,
|
||||
slopeRow,
|
||||
countRow,
|
||||
summary,
|
||||
inputs: [count, width],
|
||||
setDesignFlow(designFlow) {
|
||||
designFlowM3s = designFlow ?? null;
|
||||
},
|
||||
sync,
|
||||
write(kind, options) {
|
||||
const text = (key: string): string =>
|
||||
options[key] !== undefined ? String(options[key]) : "";
|
||||
count.value = kind === "ford_bridge" ? text("pipe_count") : "";
|
||||
width.value = text("ford_width_m");
|
||||
width.dataset.suggested = String(suggestedWidth(kind));
|
||||
width.placeholder = `제안 ${suggestedWidth(kind)}`;
|
||||
height.value = text("ford_height_m");
|
||||
slope.value = text("ford_slope_pct");
|
||||
thickness.value = text("thickness_cm");
|
||||
length.value = text("length_m");
|
||||
},
|
||||
read(kind, target) {
|
||||
putNumber(target, "ford_width_m", width.value);
|
||||
// 월류 높이는 cm 단위 수심이라 0.01m 정밀도로 싣는다(putNumber는 0.1m 반올림).
|
||||
const value = Number.parseFloat(height.value);
|
||||
if (Number.isFinite(value) && value > 0) target.ford_height_m = Number(value.toFixed(2));
|
||||
if (kind === "ford_pavement") {
|
||||
putNumber(target, "ford_slope_pct", slope.value);
|
||||
putNumber(target, "thickness_cm", thickness.value);
|
||||
putNumber(target, "length_m", length.value);
|
||||
return;
|
||||
}
|
||||
const pipes = Number.parseInt(count.value, 10);
|
||||
if (Number.isFinite(pipes) && pipes > 0) target.pipe_count = pipes;
|
||||
},
|
||||
fillSuggested(kind) {
|
||||
if (!width.value) width.value = String(suggestedWidth(kind));
|
||||
sync();
|
||||
if (!height.value && minDepthM !== null) height.value = minDepthM.toFixed(2);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -32,6 +32,64 @@ export function select(options: ReadonlyArray<[string, string]>): HTMLSelectElem
|
||||
return element;
|
||||
}
|
||||
|
||||
/** 등록부 옵션 한 칸의 모양 — `StructureOptionField` 에서 이 칸이 쓰는 몫만. */
|
||||
interface OptionShape {
|
||||
input: "select" | "number" | "text";
|
||||
choices: string[];
|
||||
default: string | number | null;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 등록부 옵션 입력 한 칸 — **저장된 값만 칸에 넣고, 등록부 기본값은 제안으로만** 보임
|
||||
* (숫자·글 칸은 회색 글씨, 고르기 칸은 빈 보기 이름). 2026-09-14 브레인 판정 「기본값을 몰래
|
||||
* 확정으로 바꾸지 않는다」 — 칸에 채워 두면 저장이 그 값을 적어 사용자가 한 일 없이 확정이 됨.
|
||||
* 고르기 칸은 늘 빈 보기를 둠 — 첫 보기를 미리 고르지 않음.
|
||||
*/
|
||||
export function optionControl(
|
||||
option: OptionShape,
|
||||
stored: string | number | undefined,
|
||||
): HTMLInputElement | HTMLSelectElement {
|
||||
const value = stored === undefined || stored === null ? "" : String(stored);
|
||||
const suggested = option.default === null || option.default === "" ? "" : String(option.default);
|
||||
if (option.input === "select") {
|
||||
const blank = suggested
|
||||
? `— 안 정함 (제안 ${suggested}) —`
|
||||
: option.required
|
||||
? "— 선택 —"
|
||||
: "— 안 정함 —";
|
||||
const element = select([["", blank], ...option.choices.map((c) => [c, c] as [string, string])]);
|
||||
element.value = value;
|
||||
return element;
|
||||
}
|
||||
const element =
|
||||
option.input === "number" ? numberInput("0.1", "0") : document.createElement("input");
|
||||
if (option.input !== "number") element.type = "text";
|
||||
element.value = value;
|
||||
element.placeholder = suggested ? `제안 ${suggested}` : option.required ? "필수 입력" : "";
|
||||
return element;
|
||||
}
|
||||
|
||||
/** [제안값 넣기] — **누른 때만** 빈 칸에 등록부 기본값을 넣음(적은 칸은 안 건드림) · 층따기 단추와 같은 모양. */
|
||||
export function suggestButton(
|
||||
entries: ReadonlyArray<{ input: HTMLInputElement | HTMLSelectElement; suggested: string }>,
|
||||
): HTMLButtonElement {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "b05-structure__suggest";
|
||||
button.textContent = "제안값 넣기";
|
||||
button.addEventListener("click", () => {
|
||||
const filled = entries.filter((entry) => entry.suggested && !entry.input.value);
|
||||
filled.forEach((entry) => {
|
||||
entry.input.value = entry.suggested;
|
||||
entry.input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
});
|
||||
// 반영은 한 번만 — 칸마다 저장 흐름이 돌지 않게 마지막 칸만 change 를 울림.
|
||||
filled.at(-1)?.input.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
});
|
||||
return button;
|
||||
}
|
||||
|
||||
/** 측점번호 + 잔여거리 두 칸 묶음. */
|
||||
export interface StationFields {
|
||||
wrap: HTMLElement;
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
defaultOptions,
|
||||
isB05Option,
|
||||
structureAnchorM,
|
||||
type StructureInstance,
|
||||
@@ -24,7 +23,7 @@ import {
|
||||
} from "./B05_Profile_Api_Structures";
|
||||
import type { PipeFacility } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
import { type FacilityAttributes } from "./B05_Profile_UI_Drainage_Facility";
|
||||
import { field, numberInput, select } from "./B05_Profile_UI_Structures_Fields";
|
||||
import { field, optionControl, suggestButton } from "./B05_Profile_UI_Structures_Fields";
|
||||
import { buildStructuresForm } from "./B05_Profile_UI_Structures_Form";
|
||||
import {
|
||||
bindStructuresEvents,
|
||||
@@ -316,32 +315,18 @@ export function createStructuresSection(
|
||||
return;
|
||||
}
|
||||
optionRow.hidden = false;
|
||||
const suggestions: Array<{ input: HTMLInputElement | HTMLSelectElement; suggested: string }> =
|
||||
[];
|
||||
visible.forEach((option) => {
|
||||
const preset = values[option.key] ?? option.default ?? "";
|
||||
let input: HTMLInputElement | HTMLSelectElement;
|
||||
if (option.input === "select") {
|
||||
const choices = option.choices.map((choice) => [choice, choice] as [string, string]);
|
||||
// ⭐ 기본값 없는 고르기 칸은 **「안 정함」이 실제 상태** — 첫 보기를 미리 고르지 않음
|
||||
// (2026-09-14 브레인 판정, 옛 2026-08-17 「빈 보기 두지 않음」을 걷음). 사용자가 고른 적
|
||||
// 없는 첫 값이 저장돼 확정으로 굳는 뿌리였음(2026-09-13 `fill_concrete_mpa` 180 박힘도
|
||||
// 같은 병). 저장에는 빈 값이 가고 표가 사유를 보임.
|
||||
if ((option.default ?? "") === "") {
|
||||
choices.unshift(["", option.required === true ? "— 선택 —" : "— 안 정함 —"]);
|
||||
}
|
||||
input = select(choices);
|
||||
input.value = String(preset);
|
||||
} else if (option.input === "number") {
|
||||
input = numberInput("0.1", "0");
|
||||
input.value = String(preset ?? "");
|
||||
if (option.required) (input as HTMLInputElement).placeholder = "필수 입력";
|
||||
} else {
|
||||
input = document.createElement("input");
|
||||
(input as HTMLInputElement).type = "text";
|
||||
input.value = String(preset ?? "");
|
||||
}
|
||||
// ⭐ 저장된 값만 칸에 · 등록부 기본값은 제안(회색 글씨·빈 보기 이름)으로만 — 고르기 칸은
|
||||
// 첫 보기를 미리 안 고름(2026-09-14 브레인 판정 ①②, `optionControl` 주석).
|
||||
const input = optionControl(option, values[option.key]);
|
||||
suggestions.push({ input, suggested: String(option.default ?? "") });
|
||||
const label = option.unit ? `${option.label} (${option.unit})` : option.label;
|
||||
// `enabled:false` 는 칸을 남기고 잠그기만 한다 — 값은 기본값이 그대로 저장된다.
|
||||
// `enabled:false` 는 칸을 남기고 잠그기만 한다 — 값은 기본값이 그대로 저장된다
|
||||
// (고를 수 없는 칸이라 제안이 아니라 프로그램 값).
|
||||
const locked = option.enabled === false;
|
||||
if (locked && !input.value) input.value = String(option.default ?? "");
|
||||
if (locked) {
|
||||
input.disabled = true;
|
||||
input.classList.add("is-locked");
|
||||
@@ -386,6 +371,9 @@ export function createStructuresSection(
|
||||
input.addEventListener("change", () => refresh(true));
|
||||
}
|
||||
});
|
||||
if (suggestions.some((entry) => entry.suggested)) {
|
||||
optionRow.append(suggestButton(suggestions));
|
||||
}
|
||||
syncOptionLock();
|
||||
syncRangeDisplay();
|
||||
}
|
||||
@@ -673,10 +661,11 @@ export function createStructuresSection(
|
||||
endFields.write(isInterval ? chainageM + DEFAULT_INTERVAL_LENGTH_M : null, step);
|
||||
memoField.value = "";
|
||||
syncPlacementFields();
|
||||
renderOptionFields(type.managed_by ? undefined : defaultOptions(type));
|
||||
// BOX암거는 레지스트리 기본값(본체 2.0×2.0·날개벽 있음 1m/2m/45°)을 실어 폼을
|
||||
// 연다 — [추가]만 눌러도 하류가 제원을 받는다(2026-08-17 사용자 확정 유지).
|
||||
syncFacilityForm(typeId === "box_culvert" ? defaultOptions(type) : {});
|
||||
// 기본값을 값으로 안 채움 — 칸은 비우고 제안으로만(브레인 판정 ②).
|
||||
renderOptionFields();
|
||||
// BOX암거도 기본값을 안 실음 — 제안으로만(판정 ③ · 옛 「[추가]만 눌러도 제원」 걷음).
|
||||
// 비운 칸은 하류(횡단도)가 등록부 기본값으로 그림 — 저장만 안 함.
|
||||
syncFacilityForm({});
|
||||
// 임시 배치도 즉시 진행(A군).
|
||||
if (type.managed_by) commitTempPipe();
|
||||
syncButtons();
|
||||
|
||||
@@ -231,6 +231,13 @@ export async function commit(ctx: StructuresCommitContext, live = false): Promis
|
||||
ctx.optionInputs.find((entry) => entry.key === "length_m")?.input.focus();
|
||||
return;
|
||||
}
|
||||
// 높이도 비면 안 놓음 — 칸이 비면 횡단도 벽이 조용히 안 서고 수량도 안 섬. 적거나
|
||||
// [제안값 넣기]를 누를 것(2026-09-14 브레인 판정 「기본값을 몰래 확정으로 안 바꿈」).
|
||||
const height = ctx.optionInputs.find((entry) => entry.key === "height_m");
|
||||
if (height?.isEmpty()) {
|
||||
height.input.focus();
|
||||
return;
|
||||
}
|
||||
const before = ctx.readBeforeM();
|
||||
if (anchor - before < -0.005) {
|
||||
const beforeInput = ctx.optionInputs.find((entry) => entry.key === "before_m")?.input;
|
||||
|
||||
@@ -166,6 +166,9 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
if (!owner || !culvert) return null;
|
||||
const spec = role === "outlet" ? culvert.outlet : culvert.inlet;
|
||||
const adjust = stationControls.revetOffset.adjustFor(owner, role);
|
||||
// 조작(조정창 높이)이 있을 때만 — 없으면 그려진 높이는 관경 기준 제안이라 옵션에 안 적음
|
||||
// (2026-09-14 브레인 판정 ④ 「기본값을 몰래 확정으로 안 바꿈」).
|
||||
if (adjust?.h == null) return null;
|
||||
return revetWallSpec(spec, adjust, culvert.hidden_pipe === true, culvert.diameter_m)
|
||||
.pureHeight;
|
||||
},
|
||||
@@ -180,6 +183,7 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
if (!owner || !culvert) return null;
|
||||
const spec = role === "outlet" ? culvert.outlet : culvert.inlet;
|
||||
const adjust = stationControls.revetOffset.adjustFor(owner, role);
|
||||
if (!adjust?.m) return null; // 조작한 형태만 — 기본 형태는 제안(판정 ④)
|
||||
return revetWallSpec(spec, adjust, culvert.hidden_pipe === true, culvert.diameter_m).form;
|
||||
},
|
||||
// 좌·우 이름표 — +offset이 좌측이고 유입 벽은 오르막(계류) 쪽에 선다.
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
writeStructurePick,
|
||||
} from "../B05_Profile/B05_Profile_UI_Structure_Pick_Session";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { FORD_BRIDGE_DEFAULT_WIDTH_M } from "@config/config_frontend";
|
||||
import {
|
||||
adjustDockRoot,
|
||||
refreshAdjustSlots,
|
||||
@@ -125,105 +126,153 @@ function coversChainage(structure: StructureInstance, chainageM: number): boolea
|
||||
return Math.abs(structureAnchorM(structure) - chainageM) < PIPE_MATCH_M;
|
||||
}
|
||||
|
||||
type Merged = Record<string, string | number>;
|
||||
|
||||
/** 그 종류의 등록부 기본값 `{키: 기본값}` — 스펙 값이 기본값에서 왔는지 가르는 데 씀. */
|
||||
function registryDefaults(types: StructureType[], typeId: string): Record<string, unknown> {
|
||||
const options = types.find((type) => type.type_id === typeId)?.options ?? [];
|
||||
return Object.fromEntries(options.map((option) => [option.key, option.default]));
|
||||
}
|
||||
|
||||
/**
|
||||
* 세월교 폼 옵션을 **지금 그려진 스펙**(`section.ford`)으로 덮는다 — 조정창 조작이
|
||||
* 이 스펙을 제자리에서 고치므로, 폼이 저장된 옵션을 붙들면 두 창이 갈린다
|
||||
* (2026-08-30 사용자: 수량(련) 칸이 비어 있고 조정창은 1련이었다).
|
||||
* 스펙 값 한 칸을 옵션에 싣는 규칙 — **저장돼 있던 칸이거나, 조작해 기본값과 달라진 칸만.**
|
||||
* 기본값에서 온 값을 실으면 페이지를 열기만 해도 임시 저장·[저장]에 기본값이 확정으로 굳음
|
||||
* (2026-09-14 브레인 판정 ④ 「기본값을 몰래 확정으로 바꾸지 않는다」). 폼은 빈 칸에 제안을 보임.
|
||||
*/
|
||||
function keepSpec(
|
||||
merged: Merged,
|
||||
stored: Merged | undefined,
|
||||
key: string,
|
||||
spec: string | number | null | undefined,
|
||||
fromDefault: unknown,
|
||||
): void {
|
||||
if (spec === null || spec === undefined) return;
|
||||
if (stored?.[key] !== undefined || String(spec) !== String(fromDefault ?? "")) merged[key] = spec;
|
||||
}
|
||||
|
||||
type WingSpec = {
|
||||
installed: boolean;
|
||||
height_m: number | null;
|
||||
length_m: number | null;
|
||||
angle_deg: number | null;
|
||||
};
|
||||
|
||||
function keepWings(
|
||||
merged: Merged,
|
||||
stored: Merged | undefined,
|
||||
defaults: Record<string, unknown>,
|
||||
wings: ReadonlyArray<readonly [WingSpec, "wing_in" | "wing_out"]>,
|
||||
): void {
|
||||
for (const [wing, prefix] of wings) {
|
||||
// 백엔드 `_wing_spec` 과 같은 채움 — 설치는 「없음」이 아니면 있음 · 치수는 기본 0/0/45.
|
||||
const install = String(defaults[prefix] ?? "") === "없음" ? "없음" : "있음";
|
||||
keepSpec(merged, stored, prefix, wing.installed ? "있음" : "없음", install);
|
||||
keepSpec(
|
||||
merged,
|
||||
stored,
|
||||
`${prefix}_height_m`,
|
||||
wing.height_m,
|
||||
defaults[`${prefix}_height_m`] ?? 0,
|
||||
);
|
||||
keepSpec(
|
||||
merged,
|
||||
stored,
|
||||
`${prefix}_length_m`,
|
||||
wing.length_m,
|
||||
defaults[`${prefix}_length_m`] ?? 0,
|
||||
);
|
||||
keepSpec(
|
||||
merged,
|
||||
stored,
|
||||
`${prefix}_angle_deg`,
|
||||
wing.angle_deg,
|
||||
defaults[`${prefix}_angle_deg`] ?? 45,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 세월교 폼 옵션 — **지금 그려진 스펙**(`section.ford`) 중 조작·저장분만 싣는다. 조정창 조작이
|
||||
* 스펙을 제자리에서 고치므로 그 값은 폼에 보여야 함(2026-08-30 사용자: 수량(련) 칸이 비고 조정창은
|
||||
* 1련) — 다만 1련이 **기본값에서 온 값이면** 제안으로만(판정 ④).
|
||||
*/
|
||||
function withFordSpec(
|
||||
options: Record<string, string | number> | undefined,
|
||||
options: Merged | undefined,
|
||||
ford: NonNullable<SectionDetailResponse["cross_sections"][number]["ford"]>,
|
||||
): Record<string, string | number> {
|
||||
const merged: Record<string, string | number> = { ...(options ?? {}) };
|
||||
if (ford.pipe_kind) merged.pipe_kind = ford.pipe_kind;
|
||||
merged.pipe_diameter_mm = Math.round(ford.diameter_m * 1000);
|
||||
merged.pipe_count = ford.pipe_count;
|
||||
merged.ford_width_m = ford.span_m;
|
||||
for (const [wing, prefix] of [
|
||||
defaults: Record<string, unknown>,
|
||||
): Merged {
|
||||
const merged: Merged = { ...(options ?? {}) };
|
||||
keepSpec(merged, options, "pipe_kind", ford.pipe_kind, defaults.pipe_kind);
|
||||
const diameter = Math.round(ford.diameter_m * 1000);
|
||||
keepSpec(merged, options, "pipe_diameter_mm", diameter, defaults.pipe_diameter_mm ?? 1000);
|
||||
const count = Number(defaults.pipe_count ?? 0);
|
||||
keepSpec(
|
||||
merged,
|
||||
options,
|
||||
"pipe_count",
|
||||
ford.pipe_count,
|
||||
count > 0 ? Math.max(Math.trunc(count), 1) : 1,
|
||||
);
|
||||
keepSpec(
|
||||
merged,
|
||||
options,
|
||||
"ford_width_m",
|
||||
ford.span_m,
|
||||
defaults.ford_width_m ?? FORD_BRIDGE_DEFAULT_WIDTH_M,
|
||||
);
|
||||
keepWings(merged, options, defaults, [
|
||||
[ford.wing_in, "wing_in"],
|
||||
[ford.wing_out, "wing_out"],
|
||||
] as const) {
|
||||
merged[prefix] = wing.installed ? "있음" : "없음";
|
||||
if (wing.height_m !== null) merged[`${prefix}_height_m`] = wing.height_m;
|
||||
if (wing.length_m !== null) merged[`${prefix}_length_m`] = wing.length_m;
|
||||
if (wing.angle_deg !== null) merged[`${prefix}_angle_deg`] = wing.angle_deg;
|
||||
}
|
||||
]);
|
||||
return merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* BOX암거 폼 옵션을 **지금 그려진 스펙**(`section.box`)으로 덮는다 — 세월교와 같은
|
||||
* 규칙이다(2026-08-30 사용자). 본체 규격은 내공 폭·높이가 정본이고, 구체 길이·표고는
|
||||
* 조정 채널이라 여기 오지 않는다.
|
||||
*/
|
||||
/** BOX암거 폼 옵션 — 세월교와 같은 규칙(조작·저장분만). 구체 길이·표고는 조정 채널이라 여기 안 옴. */
|
||||
function withBoxSpec(
|
||||
options: Record<string, string | number> | undefined,
|
||||
options: Merged | undefined,
|
||||
box: NonNullable<SectionDetailResponse["cross_sections"][number]["box"]>,
|
||||
): Record<string, string | number> {
|
||||
const merged: Record<string, string | number> = { ...(options ?? {}) };
|
||||
merged.body_width_m = box.inner_width_m;
|
||||
merged.body_height_m = box.inner_height_m;
|
||||
for (const [wing, prefix] of [
|
||||
defaults: Record<string, unknown>,
|
||||
): Merged {
|
||||
const merged: Merged = { ...(options ?? {}) };
|
||||
keepSpec(merged, options, "body_width_m", box.inner_width_m, defaults.body_width_m ?? 2);
|
||||
keepSpec(merged, options, "body_height_m", box.inner_height_m, defaults.body_height_m ?? 2);
|
||||
keepWings(merged, options, defaults, [
|
||||
[box.wing_in, "wing_in"],
|
||||
[box.wing_out, "wing_out"],
|
||||
] as const) {
|
||||
merged[prefix] = wing.installed ? "있음" : "없음";
|
||||
if (wing.height_m !== null) merged[`${prefix}_height_m`] = wing.height_m;
|
||||
if (wing.length_m !== null) merged[`${prefix}_length_m`] = wing.length_m;
|
||||
if (wing.angle_deg !== null) merged[`${prefix}_angle_deg`] = wing.angle_deg;
|
||||
}
|
||||
]);
|
||||
return merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* 관 지점 옵션에 **횡단 스펙 값**을 채워 넣는다 — 조정창이 보여주던 값과 폼이
|
||||
* 어긋나지 않게 한다(2026-08-29 사용자). 옵션에 이미 값이 있으면 그대로 두고,
|
||||
* 비어 있을 때만 스펙(서버 계산·저장분)에서 가져온다.
|
||||
* 관 지점 옵션에 **조정창에서 조작한 값**만 싣는다 — 조정창과 폼이 어긋나지 않게(2026-08-29 사용자).
|
||||
* ⚠ 스펙의 기본값(관종·관경·구조·벽 길이·전/후·집수정 길이)은 **안 실음** — 페이지를 열기만 해도
|
||||
* 임시 저장에 기본값이 확정으로 굳던 자리(2026-09-14 브레인 판정 ④). 폼 빈 칸은 제안을 보임.
|
||||
*/
|
||||
function withSpecDefaults(
|
||||
options: Record<string, string | number> | undefined,
|
||||
options: Merged | undefined,
|
||||
detail: SectionDetailResponse | null,
|
||||
chainageM: number,
|
||||
types: StructureType[],
|
||||
wallHeight?: (chainageM: number, role: "inlet" | "outlet") => number | null,
|
||||
wallForm?: (chainageM: number, role: "inlet" | "outlet") => string | null,
|
||||
): Record<string, string | number> | undefined {
|
||||
): Merged | undefined {
|
||||
const owner = detail?.cross_sections.find(
|
||||
(section) => Math.abs(section.chainage_m - chainageM) < PIPE_MATCH_M,
|
||||
);
|
||||
// 세월교는 폼과 조정창이 **같은 캐시**(`section.ford`)를 본다 — 어느 쪽에서 만졌든
|
||||
// 그 스펙이 지금 그려진 값이라 저장된 옵션보다 앞선다(2026-08-30 사용자: 프론트는
|
||||
// 구조물 배치 상세 UI, 로직·값은 조정창 것).
|
||||
if (owner?.ford) return withFordSpec(options, owner.ford);
|
||||
if (owner?.box) return withBoxSpec(options, owner.box);
|
||||
// 세월교·BOX는 폼과 조정창이 **같은 캐시**를 봄 — 조작한 값은 저장된 옵션보다 앞섬(2026-08-30).
|
||||
if (owner?.ford) return withFordSpec(options, owner.ford, registryDefaults(types, "ford_bridge"));
|
||||
if (owner?.box) return withBoxSpec(options, owner.box, registryDefaults(types, "box_culvert"));
|
||||
const culvert = owner?.culvert;
|
||||
if (!culvert) return options;
|
||||
const merged: Record<string, string | number> = { ...(options ?? {}) };
|
||||
const put = (key: string, value: string | number | null | undefined): void => {
|
||||
if (merged[key] === undefined && value !== null && value !== undefined) merged[key] = value;
|
||||
};
|
||||
put("pipe_kind", culvert.pipe_kind);
|
||||
put("pipe_diameter_mm", Math.round(culvert.diameter_m * 1000));
|
||||
for (const [side, prefix] of [
|
||||
[culvert.inlet, "inlet"],
|
||||
[culvert.outlet, "outlet"],
|
||||
] as const) {
|
||||
put(`${prefix}_type`, side.structure);
|
||||
// 형태·높이는 **지금 그려진 값**이 이긴다 — 조정값(조작)이 저장된 옵션보다
|
||||
// 앞서기 때문이다. 저장값을 그대로 두면 폼과 그림이 갈린다(2026-08-30 사용자:
|
||||
// 배관 유출구 기슭막이 형태가 폼과 다르게 그려졌다).
|
||||
const merged: Merged = { ...(options ?? {}) };
|
||||
for (const prefix of ["inlet", "outlet"] as const) {
|
||||
// 형태·높이는 **조작한 값**이 이긴다 — 조정값이 저장된 옵션보다 앞서기 때문(2026-08-30 사용자:
|
||||
// 배관 유출구 기슭막이 형태가 폼과 다르게 그려졌다). 조작이 없으면 부르는 쪽이 null 을 줌.
|
||||
const drawnForm = wallForm?.(chainageM, prefix);
|
||||
if (drawnForm) merged[`${prefix}_revet_form`] = drawnForm;
|
||||
else put(`${prefix}_revet_form`, side.revet_form);
|
||||
const drawnHeight = wallHeight?.(chainageM, prefix);
|
||||
if (drawnHeight != null) merged[`${prefix}_revet_height_m`] = Number(drawnHeight.toFixed(1));
|
||||
else put(`${prefix}_revet_height_m`, side.revet_height_m);
|
||||
put(`${prefix}_revet_length_m`, side.revet_length_m);
|
||||
put(`${prefix}_revet_before_m`, side.revet_before_m);
|
||||
put(`${prefix}_revet_after_m`, side.revet_after_m);
|
||||
}
|
||||
put("inlet_basin_length_m", culvert.inlet.basin_length_m);
|
||||
put("inlet_basin_before_m", culvert.inlet.basin_before_m);
|
||||
put("inlet_basin_after_m", culvert.inlet.basin_after_m);
|
||||
return merged;
|
||||
}
|
||||
|
||||
@@ -374,6 +423,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
pipe.options,
|
||||
detail,
|
||||
pipe.chainage_m,
|
||||
types,
|
||||
deps.wallHeight,
|
||||
deps.wallForm,
|
||||
),
|
||||
@@ -418,6 +468,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
hit.options,
|
||||
deps.detail?.() ?? null,
|
||||
hit.chainage_m,
|
||||
markTypes,
|
||||
deps.wallHeight,
|
||||
deps.wallForm,
|
||||
);
|
||||
|
||||
@@ -23,6 +23,8 @@ from B08_Quantity.B08_Quantity_Engine_UnitQuantity_StoneSpec import (
|
||||
back_length_default_note,
|
||||
face_slope_ratio,
|
||||
fill_concrete_mpa,
|
||||
STONE_SUPPLY_DEFAULT_NOTE,
|
||||
STONE_SUPPLY_KEYS,
|
||||
is_collected_stone,
|
||||
load_stone_kind_table,
|
||||
stone_coefficients,
|
||||
@@ -487,6 +489,9 @@ def stone_masonry(
|
||||
# 채집석 — **캐서 쓰는 구조물**의 돌 체적. 사토에서 뺄 밑수이고 **여기서 빼지 않는다.**
|
||||
# ⚠ 밑수가 확정 5차로 바뀌었다 — 면석 몸통은 이제 **석적**(정면적×뒷길이×0.77)이고
|
||||
# 막자갈은 **뒷채움 사다리꼴**이다. 옛 「입적 − 몸통 − 고임돌」 몫이 아니다.
|
||||
if all(options.get(key) in (None, "") for key in STONE_SUPPLY_KEYS):
|
||||
# 폼이 기본값을 값으로 안 보내게 된 뒤(2026-09-14) 빈 조달이 조용히 「채집」이 되지 않게.
|
||||
notes.append(STONE_SUPPLY_DEFAULT_NOTE)
|
||||
if is_collected_stone(options):
|
||||
wedge = masonry_area * _num(table["wedge_stone_m3_per_m2"]) # None 이면 0
|
||||
collected = max(stone_pile + wedge + rubble, 0.0)
|
||||
|
||||
@@ -279,6 +279,8 @@ STONE_MASONRY = {
|
||||
#: ⚠ 구조물마다 바꿀 수 있다 — 저장 제원에 「구입」이라 적힌 구조물만 공제에서 빠진다.
|
||||
STONE_SUPPLY_KEYS = ("stone_supply", "stone_source")
|
||||
STONE_SUPPLY_PURCHASED = {"구입", "구입품", "사서", "purchase", "purchased", "buy"}
|
||||
#: 조달을 안 정한 구조물의 사유 — 셈은 확정 ② 「기본은 캔다」대로 채집이되 **안 정한 사실**을 보임.
|
||||
STONE_SUPPLY_DEFAULT_NOTE = "돌 조달(채집·구입)을 안 정해 「채집」으로 셈 — 사용자 확정 ② 기본 · 구조물 상세에서 고르면 바뀜"
|
||||
|
||||
#: ⚠⚠ **채집석 공제는 사토에서 한 번만 뺀다** (2026-09-09 세 창 확정 · 랩탑 메인의 통로에도
|
||||
#: 같은 문장이 박혀 있다 — 두 곳이 같은 말이라야 나중에 누가 봐도 안 갈린다).
|
||||
|
||||
@@ -24,6 +24,9 @@ if str(ROOT) not in sys.path:
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import build_table # noqa: E402
|
||||
|
||||
PANEL = (ROOT / "B05_Profile" / "B05_Profile_UI_Structures_Panel.ts").read_text(encoding="utf-8")
|
||||
#: 2026-09-14 칸 만들기가 `_Structures_Fields.optionControl` 로 옮겨감(브레인 판정 ①② — 고르기 칸은
|
||||
#: 늘 빈 보기 · 기본값은 제안으로만).
|
||||
FIELDS = (ROOT / "B05_Profile" / "B05_Profile_UI_Structures_Fields.ts").read_text(encoding="utf-8")
|
||||
API = (ROOT / "B05_Profile" / "B05_Profile_Api_Structures.ts").read_text(encoding="utf-8")
|
||||
COMMIT = (ROOT / "B05_Profile" / "B05_Profile_UI_Structures_Panel_Commit.ts").read_text(
|
||||
encoding="utf-8"
|
||||
@@ -39,22 +42,24 @@ def _registry_options(type_id: str) -> dict[str, dict]:
|
||||
|
||||
|
||||
def test_폼이_empty_means_를_보고_빈_보기를_둔다() -> None:
|
||||
"""소스 검사 — 빈 보기와 초기값이 **같은 판정**(`keepEmpty`)을 쓴다."""
|
||||
assert "option.empty_means" in PANEL
|
||||
assert "const keepEmpty = mustPick || meansEmpty;" in PANEL
|
||||
assert 'if (keepEmpty) choices.unshift(["", mustPick ? "— 선택 —" : "— 안 정함 —"]);' in PANEL
|
||||
# 빈 보기만 넣고 초기값을 첫 보기로 두면 화면은 「안 정함」인데 값은 첫 보기가 된다.
|
||||
assert 'String(preset || (keepEmpty ? "" : (option.choices[0] ?? "")))' in PANEL
|
||||
"""소스 검사 — 고르기 칸은 **늘** 빈 보기를 두고, 초기값은 저장된 값뿐(첫 보기를 안 박음)."""
|
||||
assert "optionControl(option, values[option.key])" in PANEL
|
||||
assert 'select([["", blank], ...option.choices' in FIELDS
|
||||
# 초기값은 저장된 값만 — 첫 보기·기본값으로 채우지 않음.
|
||||
assert "element.value = value;" in FIELDS
|
||||
assert "option.choices[0]" not in PANEL
|
||||
# 클라이언트 옵션 타입에 칸이 있어야 서버 응답의 `empty_means` 가 살아 온다.
|
||||
assert "empty_means?: string | null;" in API
|
||||
# 빈 칸은 아예 안 실린다 — 이 규칙이 있어야 「안 정함」이 정본에 안 박힌다.
|
||||
assert "if (input.isEmpty()) return;" in COMMIT
|
||||
|
||||
|
||||
def test_기본값_있는_칸은_빈_보기를_안_둔다() -> None:
|
||||
"""`empty_means` 가 적혀 있어도 **기본값이 있으면** 그 값이 뜬다(2026-08-17 지시 유지)."""
|
||||
def test_기본값_있는_칸도_빈_보기에_제안으로만_보인다() -> None:
|
||||
"""⭐ 2026-09-14 브레인 판정으로 뒤집힘 — 기본값이 **있어도** 칸에 채우지 않고 빈 보기 이름에
|
||||
「제안 …」으로만 보임(옛 2026-08-17 「첫 보기가 곧 기본값」 걷음). 저장 한 번에 기본값이
|
||||
확정으로 굳던 뿌리였음."""
|
||||
options = _registry_options("masonry_wet")
|
||||
# 버림 콘크리트 — empty_means 가 있으나 기본이 「넣음」이다.
|
||||
# 버림 콘크리트 — empty_means 가 있고 기본이 「넣음」.
|
||||
assert options["blinding_concrete"]["empty_means"]
|
||||
assert options["blinding_concrete"]["default"] == "넣음"
|
||||
# 채움 강도·야면석 계수 — 기본이 없어 빈 보기가 서야 하는 칸.
|
||||
@@ -62,7 +67,7 @@ def test_기본값_있는_칸은_빈_보기를_안_둔다() -> None:
|
||||
assert options[key]["empty_means"], key
|
||||
assert options[key]["default"] is None, key
|
||||
assert options[key]["required"] is False, key
|
||||
assert '(option.default ?? "") === ""' in PANEL # 기본값 있으면 빈 보기를 안 둠
|
||||
assert "`— 안 정함 (제안 ${suggested}) —`" in FIELDS
|
||||
|
||||
|
||||
def _wall(**options) -> dict:
|
||||
|
||||
@@ -39,10 +39,13 @@ def test_B05_는_종전대로_유무_종류_위치만():
|
||||
def test_기본값_없는_필수_항목은_빈_칸으로_연다():
|
||||
"""첫 항목을 슬쩍 고르면 **근거 없는 값**(뒷길이 25㎝ 같은)이 수량·단가로 흘러간다."""
|
||||
panel = _PANEL.read_text(encoding="utf-8")
|
||||
# 2026-09-13 — 빈 보기를 두는 까닭이 둘이 되어(필수 + `empty_means`) 한 줄로 합쳐졌다.
|
||||
fields = (PROJECT_ROOT / "B05_Profile" / "B05_Profile_UI_Structures_Fields.ts").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
# 2026-09-14 — 칸 만들기가 `optionControl` 로 옮겨감 · 고르기 칸은 **늘** 빈 보기(브레인 판정 ①).
|
||||
# 필수 칸의 글자는 그대로 「— 선택 —」이다.
|
||||
assert 'choices.unshift(["", mustPick ? "— 선택 —" : "— 안 정함 —"])' in panel
|
||||
assert 'option.required === true && (option.default ?? "") === ""' in panel
|
||||
assert "optionControl(option, values[option.key])" in panel
|
||||
assert '? "— 선택 —"' in fields and 'select([["", blank]' in fields
|
||||
|
||||
|
||||
def test_큰돌쌓기에_메_찰_구분이_있다():
|
||||
|
||||
Reference in New Issue
Block a user