feat(B06): 세월교 좌측 폼을 배관 측점과 같은 양식으로 맞춘다
사용자 지시 3건(2026-08-30).
1) 횡단도에서 세월교 측벽을 고르면 좌측 폼의 그 측 칸이 선다 — 배관 기슭막이가
쓰던 경로를 그대로 탄다. `SideSlots`에 `wingInlet`·`wingOutlet`을 더하고
`slotOf()`가 배관 칸 → 독립 기슭막이 칸 → 날개벽 칸 순으로 지금 서 있는 쪽을
고른다. 세월교 조정창은 `dataset.side`·`dataset.panelTitle`을 세워 어느 칸을
가리키는지 알린다. 벽 선택 뒤 `refreshAdjustSlots()`도 배관과 같게 붙였다.
2) 월류 폭·높이·수량·개략 단면 결과를 관경 바로 다음으로 옮겼다(바닥 경사도 같은
묶음에 둔다 — 물넘이포장 전용이라 세월교에서는 숨는다). 개략 단면 안내문은
읽는 글이라 10px·opacity 0.75로 줄였다.
3) 세월교·날개벽 숫자 칸을 기슭막이·집수정과 같은 [-][값][+] 묶음으로 바꿨다
(월류 폭·높이·바닥 경사·수량·날개 높이·길이·각도). `stepper()`에 표시 자릿수
인자를 더해 련·각도는 정수로 보인다("1.0련"·"45.0°" 방지). 기본값 1이라 기존
호출부는 그대로다.
자체검증(공용 브라우저 5174, 측점 7+9.7 세월교):
- tsc --noEmit 통과.
- 유입 측벽 선택 → `날개벽(유입) (세월교)` 칸 강조, 유출 측벽 → `날개벽(유출)
(세월교)`. 한 번에 한 칸만 선다.
- 폼 항목 순서 실측: 관종 > 관경 > 월류 폭 > 월류 높이 > 수량 > 날개벽(유입) …
안내문 font-size 10px, 보이는 스텝 묶음 9개.
- 수량 + → 폼 "2"·조정창 "2련", 각도 + → 폼 "50"·조정창 "50°" (소수점 없음).
- 회귀: 배관 측점 기슭막이 선택 → `유출구 (기슭막이(유출))` 강조 + 높이·길이·
기준측점 전/후 행 이식 정상.
- 조작값은 원래대로(수량 1·각도 45) 되돌려 놓았다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -187,6 +187,10 @@ export interface FacilityOptionsForm {
|
|||||||
/** 독립 기슭막이 좌·우 칸의 이식 자리 — 배관 유입구·유출구 자리와 같은 몫이다. */
|
/** 독립 기슭막이 좌·우 칸의 이식 자리 — 배관 유입구·유출구 자리와 같은 몫이다. */
|
||||||
revetInletSlot: HTMLElement;
|
revetInletSlot: HTMLElement;
|
||||||
revetOutletSlot: HTMLElement;
|
revetOutletSlot: HTMLElement;
|
||||||
|
/** 세월교·BOX암거 날개벽 칸의 이식 자리 — 세월교 유입·유출 측벽을 고르면 이 칸이
|
||||||
|
* 선다(2026-08-30 사용자 지시 1: 배관 측점·기슭막이와 같게). */
|
||||||
|
wingInSlot: HTMLElement;
|
||||||
|
wingOutSlot: HTMLElement;
|
||||||
/** 유입구 "구조"에 합쳐진 B06 형식 값(2026-08-29 지시 5). B06이 조정창 값과 맞춘다. */
|
/** 유입구 "구조"에 합쳐진 B06 형식 값(2026-08-29 지시 5). B06이 조정창 값과 맞춘다. */
|
||||||
inletStructure: () => InletStructureKind;
|
inletStructure: () => InletStructureKind;
|
||||||
setInletStructure: (value: InletStructureKind) => void;
|
setInletStructure: (value: InletStructureKind) => void;
|
||||||
@@ -356,7 +360,9 @@ export function createFacilityOptionsForm(
|
|||||||
// ── 세월교 — 구체 내 배관은 배수관과 같은 관종·관경 칸을 쓰고(2026-08-17 사용자
|
// ── 세월교 — 구체 내 배관은 배수관과 같은 관종·관경 칸을 쓰고(2026-08-17 사용자
|
||||||
// 지시) 수량만 따로 받는다.
|
// 지시) 수량만 따로 받는다.
|
||||||
const fordCount = numberInput("1", "1", "련");
|
const fordCount = numberInput("1", "1", "련");
|
||||||
const fordRow = grid(labeled("수량 (련)", fordCount));
|
// 숫자 칸은 기슭막이·집수정과 같은 [-][값][+] 묶음(2026-08-30 사용자 지시 3).
|
||||||
|
// 련은 정수라 소수 자릿수를 두지 않는다.
|
||||||
|
const fordRow = grid(labeled("수량 (련)", stepper(fordCount, 1, 0)));
|
||||||
|
|
||||||
// ── 물넘이·세월교 개략 단면 — 월류 폭 + 월류 높이 한 행(2026-08-18 사용자 지시).
|
// ── 물넘이·세월교 개략 단면 — 월류 폭 + 월류 높이 한 행(2026-08-18 사용자 지시).
|
||||||
// 폭 기본값은 세월교 10m·물넘이 포장 5m(사용자 확정 — 지식DB 폭 수치 근거 없음).
|
// 폭 기본값은 세월교 10m·물넘이 포장 5m(사용자 확정 — 지식DB 폭 수치 근거 없음).
|
||||||
@@ -365,14 +371,14 @@ export function createFacilityOptionsForm(
|
|||||||
const fordWidth = numberInput("0.1");
|
const fordWidth = numberInput("0.1");
|
||||||
const fordHeight = numberInput("0.01");
|
const fordHeight = numberInput("0.01");
|
||||||
const fordWidthRow = grid(
|
const fordWidthRow = grid(
|
||||||
labeled("월류 폭 (m)", fordWidth),
|
labeled("월류 폭 (m)", stepper(fordWidth, 0.1)),
|
||||||
labeled("월류 높이 (m)", fordHeight),
|
labeled("월류 높이 (m)", stepper(fordHeight, 0.1)),
|
||||||
);
|
);
|
||||||
// 물넘이 바닥은 유입(상류)이 높고 유출이 낮게 기운다(2026-08-28 사용자 확정).
|
// 물넘이 바닥은 유입(상류)이 높고 유출이 낮게 기운다(2026-08-28 사용자 확정).
|
||||||
// 비우면 그 측점의 **노면 횡단경사**를 그대로 쓴다 — 횡단도가 판단한다.
|
// 비우면 그 측점의 **노면 횡단경사**를 그대로 쓴다 — 횡단도가 판단한다.
|
||||||
const fordSlope = numberInput("0.1");
|
const fordSlope = numberInput("0.1");
|
||||||
fordSlope.placeholder = "노면 기울기";
|
fordSlope.placeholder = "노면 기울기";
|
||||||
const fordSlopeRow = grid(labeled("바닥 경사 유입→유출 (%)", fordSlope));
|
const fordSlopeRow = grid(labeled("바닥 경사 유입→유출 (%)", stepper(fordSlope, 0.1)));
|
||||||
const fordSummary = document.createElement("p");
|
const fordSummary = document.createElement("p");
|
||||||
fordSummary.className = "b05-drainage__facility-note";
|
fordSummary.className = "b05-drainage__facility-note";
|
||||||
/** 담당 유역 설계유량(㎥/s) — 개략 단면의 입력. 유역이 없으면 null. */
|
/** 담당 유역 설계유량(㎥/s) — 개략 단면의 입력. 유역이 없으면 null. */
|
||||||
@@ -423,8 +429,14 @@ export function createFacilityOptionsForm(
|
|||||||
emit();
|
emit();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 세월교·물넘이 항목은 **관종·관경 바로 다음**에 둔다(2026-08-30 사용자 지시 2) —
|
||||||
|
// 월류 폭·높이 → 바닥 경사 → 수량 → 개략 단면 결과. 다른 시설에서는 전부 숨는다.
|
||||||
root.append(
|
root.append(
|
||||||
pipeRow,
|
pipeRow,
|
||||||
|
fordWidthRow,
|
||||||
|
fordSlopeRow,
|
||||||
|
fordRow,
|
||||||
|
fordSummary,
|
||||||
inletGroup.root,
|
inletGroup.root,
|
||||||
outletGroup.root,
|
outletGroup.root,
|
||||||
extraGroup.root,
|
extraGroup.root,
|
||||||
@@ -434,10 +446,6 @@ export function createFacilityOptionsForm(
|
|||||||
boxWrap,
|
boxWrap,
|
||||||
wingInFields.root,
|
wingInFields.root,
|
||||||
wingOutFields.root,
|
wingOutFields.root,
|
||||||
fordWidthRow,
|
|
||||||
fordSlopeRow,
|
|
||||||
fordRow,
|
|
||||||
fordSummary,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let current: PipeFacility | null = null;
|
let current: PipeFacility | null = null;
|
||||||
@@ -537,6 +545,8 @@ export function createFacilityOptionsForm(
|
|||||||
extraSlot,
|
extraSlot,
|
||||||
revetInletSlot: revetInlet.slot,
|
revetInletSlot: revetInlet.slot,
|
||||||
revetOutletSlot: revetOutlet.slot,
|
revetOutletSlot: revetOutlet.slot,
|
||||||
|
wingInSlot: wingInFields.slot,
|
||||||
|
wingOutSlot: wingOutFields.slot,
|
||||||
setRevetSideLabels(labels) {
|
setRevetSideLabels(labels) {
|
||||||
revetSideLabels = labels;
|
revetSideLabels = labels;
|
||||||
syncVisibility();
|
syncVisibility();
|
||||||
|
|||||||
@@ -84,7 +84,9 @@ export function numberInput(step: string, min = "0", placeholder = ""): HTMLInpu
|
|||||||
/** 스텝 묶음 입력에 붙일 일련번호 — 라벨이 가리킬 대상을 명시하는 데 쓴다. */
|
/** 스텝 묶음 입력에 붙일 일련번호 — 라벨이 가리킬 대상을 명시하는 데 쓴다. */
|
||||||
let stepperSeq = 0;
|
let stepperSeq = 0;
|
||||||
|
|
||||||
export function stepper(input: HTMLInputElement, stepM: number): HTMLElement {
|
/** `decimals` — 표시 자릿수. 련·각도처럼 정수로 세는 칸은 0을 준다(2026-08-30 사용자:
|
||||||
|
* 세월교 상세도 같은 양식으로 맞추되 "1.0련"·"45.0°"로 보이면 안 된다). */
|
||||||
|
export function stepper(input: HTMLInputElement, stepM: number, decimals = 1): HTMLElement {
|
||||||
const wrap = document.createElement("div");
|
const wrap = document.createElement("div");
|
||||||
wrap.className = "b05-structure__stepper";
|
wrap.className = "b05-structure__stepper";
|
||||||
// 라벨이 이 입력을 가리키게 id를 붙인다. 없으면 <label>의 암묵 대상이 묶음의
|
// 라벨이 이 입력을 가리키게 id를 붙인다. 없으면 <label>의 암묵 대상이 묶음의
|
||||||
@@ -95,11 +97,12 @@ export function stepper(input: HTMLInputElement, stepM: number): HTMLElement {
|
|||||||
const current = Number.parseFloat(input.value);
|
const current = Number.parseFloat(input.value);
|
||||||
const min = Number.parseFloat(input.min);
|
const min = Number.parseFloat(input.min);
|
||||||
const floor = Number.isFinite(min) ? min : 0;
|
const floor = Number.isFinite(min) ? min : 0;
|
||||||
|
const grain = 10 ** decimals;
|
||||||
const next = Math.max(
|
const next = Math.max(
|
||||||
floor,
|
floor,
|
||||||
Math.round(((Number.isFinite(current) ? current : 0) + delta) * 10) / 10,
|
Math.round(((Number.isFinite(current) ? current : 0) + delta) * grain) / grain,
|
||||||
);
|
);
|
||||||
input.value = next.toFixed(1);
|
input.value = next.toFixed(decimals);
|
||||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
input.dispatchEvent(new Event("change", { bubbles: true }));
|
input.dispatchEvent(new Event("change", { bubbles: true }));
|
||||||
};
|
};
|
||||||
@@ -385,6 +388,9 @@ const WING_DEFAULTS = { install: "있음", height: "1", length: "2", angle: "45"
|
|||||||
|
|
||||||
interface WingFields {
|
interface WingFields {
|
||||||
root: HTMLFieldSetElement;
|
root: HTMLFieldSetElement;
|
||||||
|
/** 횡단도에서 그 측 날개벽·측벽을 고를 때 칸을 세우는 자리(B06 전용, 2026-08-30).
|
||||||
|
* 배관 유입구·유출구 칸의 `adjust-slot`과 같은 몫이다. */
|
||||||
|
slot: HTMLElement;
|
||||||
inputs: Array<HTMLInputElement | HTMLSelectElement>;
|
inputs: Array<HTMLInputElement | HTMLSelectElement>;
|
||||||
syncVisibility: () => void;
|
syncVisibility: () => void;
|
||||||
write: (options: Record<string, string | number>) => void;
|
write: (options: Record<string, string | number>) => void;
|
||||||
@@ -403,10 +409,16 @@ export function createWingFields(title: string, keys: WingKeys): WingFields {
|
|||||||
const angle = numberInput("1");
|
const angle = numberInput("1");
|
||||||
angle.value = WING_DEFAULTS.angle;
|
angle.value = WING_DEFAULTS.angle;
|
||||||
|
|
||||||
const heightField = labeled("짧은쪽 높이 (m)", height);
|
// 숫자 칸은 기슭막이·집수정과 같은 [-][값][+] 묶음으로 맞춘다(2026-08-30 사용자).
|
||||||
const dimsRow = grid(labeled("길이 (m)", length), labeled("각도 (°)", angle));
|
const heightField = labeled("짧은쪽 높이 (m)", stepper(height, 0.1));
|
||||||
|
const dimsRow = grid(
|
||||||
|
labeled("길이 (m)", stepper(length, 0.1)),
|
||||||
|
labeled("각도 (°)", stepper(angle, 5, 0)),
|
||||||
|
);
|
||||||
const box = group(title);
|
const box = group(title);
|
||||||
box.body.append(grid(labeled("설치", install), heightField), dimsRow);
|
const slot = document.createElement("div");
|
||||||
|
slot.className = "b05-structure__adjust-slot";
|
||||||
|
box.body.append(grid(labeled("설치", install), heightField), dimsRow, slot);
|
||||||
|
|
||||||
function syncVisibility(): void {
|
function syncVisibility(): void {
|
||||||
const off = install.value === "없음";
|
const off = install.value === "없음";
|
||||||
@@ -419,6 +431,7 @@ export function createWingFields(title: string, keys: WingKeys): WingFields {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
root: box.root,
|
root: box.root,
|
||||||
|
slot,
|
||||||
inputs: [install, height, length, angle],
|
inputs: [install, height, length, angle],
|
||||||
syncVisibility,
|
syncVisibility,
|
||||||
write(options) {
|
write(options) {
|
||||||
|
|||||||
@@ -344,6 +344,15 @@
|
|||||||
font-size: var(--text-caption);
|
font-size: var(--text-caption);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 물넘이·세월교 개략 단면 결과 — 읽어 보는 안내문이라 입력 칸보다 작게 둔다
|
||||||
|
(2026-08-30 사용자: 글자가 너무 크다). */
|
||||||
|
.b05-drainage__facility-note {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1.4;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
/* 스텝 묶음과 그 안의 버튼·숫자칸도 같은 높이로 선다. */
|
/* 스텝 묶음과 그 안의 버튼·숫자칸도 같은 높이로 선다. */
|
||||||
.b05-structure__stepper {
|
.b05-structure__stepper {
|
||||||
height: var(--b05-control-h);
|
height: var(--b05-control-h);
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ interface SideSlots {
|
|||||||
/** 독립 기슭막이 좌·우 칸 — 배관 유입구·유출구 칸이 숨어 있을 때 이쪽으로 간다. */
|
/** 독립 기슭막이 좌·우 칸 — 배관 유입구·유출구 칸이 숨어 있을 때 이쪽으로 간다. */
|
||||||
revetInlet: HTMLElement;
|
revetInlet: HTMLElement;
|
||||||
revetOutlet: HTMLElement;
|
revetOutlet: HTMLElement;
|
||||||
|
/** 세월교 날개벽(유입·유출) 칸 — 세월교 측점에서는 배관·독립 칸이 모두 숨어 있어
|
||||||
|
* 이쪽이 그 측의 자리다(2026-08-30 사용자 지시 1). */
|
||||||
|
wingInlet: HTMLElement;
|
||||||
|
wingOutlet: HTMLElement;
|
||||||
}
|
}
|
||||||
let sideSlots: SideSlots | null = null;
|
let sideSlots: SideSlots | null = null;
|
||||||
|
|
||||||
@@ -51,12 +55,14 @@ function slotUsable(target: HTMLElement | undefined): target is HTMLElement {
|
|||||||
return !!group && !group.hidden;
|
return !!group && !group.hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 배관이면 유입구·유출구 칸, 독립 기슭막이면 좌·우 칸 — 지금 서 있는 쪽을 준다. */
|
/** 배관이면 유입구·유출구 칸, 독립 기슭막이면 좌·우 칸, 세월교면 날개벽 칸 —
|
||||||
|
* 지금 서 있는 쪽을 준다(같은 측의 자리가 시설 종류마다 다른 칸에 있다). */
|
||||||
function slotOf(side: "inlet" | "outlet"): HTMLElement {
|
function slotOf(side: "inlet" | "outlet"): HTMLElement {
|
||||||
const slots = sideSlots!;
|
const slots = sideSlots!;
|
||||||
return slotUsable(slots[side])
|
const revet = side === "inlet" ? slots.revetInlet : slots.revetOutlet;
|
||||||
? slots[side]
|
const wing = side === "inlet" ? slots.wingInlet : slots.wingOutlet;
|
||||||
: slots[side === "inlet" ? "revetInlet" : "revetOutlet"];
|
if (slotUsable(slots[side])) return slots[side];
|
||||||
|
return slotUsable(revet) ? revet : wing;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 옮겨 온 행과 그 **제자리 표식** — 되돌릴 때 원래 순서로 꽂기 위해 짝으로 둔다.
|
/** 옮겨 온 행과 그 **제자리 표식** — 되돌릴 때 원래 순서로 꽂기 위해 짝으로 둔다.
|
||||||
@@ -85,6 +91,8 @@ function clearSideSlots(): void {
|
|||||||
sideSlots?.extra,
|
sideSlots?.extra,
|
||||||
sideSlots?.revetInlet,
|
sideSlots?.revetInlet,
|
||||||
sideSlots?.revetOutlet,
|
sideSlots?.revetOutlet,
|
||||||
|
sideSlots?.wingInlet,
|
||||||
|
sideSlots?.wingOutlet,
|
||||||
]) {
|
]) {
|
||||||
target?.replaceChildren();
|
target?.replaceChildren();
|
||||||
const group = target?.closest("fieldset");
|
const group = target?.closest("fieldset");
|
||||||
|
|||||||
@@ -265,6 +265,11 @@ export function buildFordPanel(deps: FordPanelDeps, opts?: { dock?: boolean }):
|
|||||||
show(role) {
|
show(role) {
|
||||||
current = role;
|
current = role;
|
||||||
root.classList.toggle("is-hidden", role === null);
|
root.classList.toggle("is-hidden", role === null);
|
||||||
|
// 좌측 [횡단 조정]에 어느 칸을 세울지 알린다 — 세월교는 그 측 날개벽 칸이다
|
||||||
|
// (배관 조정창이 유입구·유출구 칸을 가리키는 것과 같은 규약, 2026-08-30 지시 1).
|
||||||
|
root.dataset.side = role ?? "";
|
||||||
|
// 칸 이름이 이미 "날개벽(유입)"이라 괄호에는 구조물 이름만 붙인다.
|
||||||
|
root.dataset.panelTitle = role === null ? "" : "세월교";
|
||||||
arrows.detach();
|
arrows.detach();
|
||||||
if (role !== null) arrows.attach();
|
if (role !== null) arrows.attach();
|
||||||
render();
|
render();
|
||||||
|
|||||||
@@ -266,6 +266,8 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
|||||||
extra: section.facility.extraSlot,
|
extra: section.facility.extraSlot,
|
||||||
revetInlet: section.facility.revetInletSlot,
|
revetInlet: section.facility.revetInletSlot,
|
||||||
revetOutlet: section.facility.revetOutletSlot,
|
revetOutlet: section.facility.revetOutletSlot,
|
||||||
|
wingInlet: section.facility.wingInSlot,
|
||||||
|
wingOutlet: section.facility.wingOutSlot,
|
||||||
});
|
});
|
||||||
|
|
||||||
async function load(): Promise<void> {
|
async function load(): Promise<void> {
|
||||||
@@ -441,6 +443,9 @@ export function wireStructureSelection(
|
|||||||
markWallSelecting();
|
markWallSelecting();
|
||||||
origFord(chainageM, role);
|
origFord(chainageM, role);
|
||||||
panel.showPipeAt(role ? chainageM : null);
|
panel.showPipeAt(role ? chainageM : null);
|
||||||
|
// 폼이 이 시설로 갈아 끼워진 뒤에 이식을 다시 돌린다 — 날개벽 칸이 그제야 선다
|
||||||
|
// (배관 기슭막이와 같은 순서, 2026-08-30 사용자 지시 1).
|
||||||
|
refreshAdjustSlots();
|
||||||
};
|
};
|
||||||
const origBox = stationControls.box.select;
|
const origBox = stationControls.box.select;
|
||||||
stationControls.box.select = (chainageM, role) => {
|
stationControls.box.select = (chainageM, role) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user