feat(B05,B06): 독립 기슭막이를 좌·우 칸으로 나누고 단 수를 도면에 잇는다
- 설치 측·단 수를 컨테이너 밖 한 행으로 빼고, 벽 제원은 [기슭막이 (좌)]· [기슭막이 (우)] 두 칸으로 나눈다. 설치 측이 한쪽이면 그 칸만 뜬다. 좌·우 이름표는 그 측점 오르막 방향(uphill_side)이 정한다. - 두 칸은 배관 유출구와 같은 양식이다 — [구조][형태] + 길이·높이·전/후. 구조 선택지는 기슭막이 하나뿐이지만 레이아웃을 맞추려 그대로 둔다. - 값은 유입/유출 키로 좌·우 따로 저장한다(옛 한 벌 저장분은 양쪽으로 펼쳐 읽는다). - 단 수에 [-][값][+] 조정칸을 붙이고, 폼 단 수를 다단 채널(extra·bextra)로 보낸다 — 배수관 측점처럼 횡단도에 단이 선다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,7 @@ import {
|
||||
BOX_SIZE_CUSTOM,
|
||||
BOX_SIZE_PRESETS,
|
||||
createRevetmentFields,
|
||||
createRevetSideGroup,
|
||||
createWingFields,
|
||||
fordSection,
|
||||
grid,
|
||||
@@ -186,6 +187,8 @@ export interface FacilityOptionsForm {
|
||||
/** 유입구 "구조"에 합쳐진 B06 형식 값(2026-08-29 지시 5). B06이 조정창 값과 맞춘다. */
|
||||
inletStructure: () => InletStructureKind;
|
||||
setInletStructure: (value: InletStructureKind) => void;
|
||||
/** 독립 기슭막이 좌·우 칸의 이름표를 그 측점 방향으로 맞춘다(B06 전용). */
|
||||
setRevetSideLabels: (labels: { inlet: "좌" | "우"; outlet: "좌" | "우" }) => void;
|
||||
/** 구조가 바뀔 때 B06 유입측 형식 제어에 알린다(B05는 등록하지 않는다). */
|
||||
onInletStructureChange: (handler: (value: InletStructureKind) => void) => void;
|
||||
}
|
||||
@@ -281,42 +284,51 @@ export function createFacilityOptionsForm(
|
||||
extraGroup.root.hidden = true;
|
||||
|
||||
// ── 독립 기슭막이(2026-08-28 이관: 배관처럼 pipe_points 관리) ─────────────
|
||||
// 배관 없는 기슭막이 — 설치 측(양쪽/좌/우) + 형태·높이·길이·전/후(배관 유입·유출과
|
||||
// 같은 조각) + 다단 단 수. B06 횡단도·3D·조정창이 배관 기슭막이 경로를 그대로 탄다.
|
||||
const revetGroup = group("기슭막이");
|
||||
// 설치 측·단 수는 **컨테이너 밖**(배관의 관종·관경과 같은 층)에 두고, 벽 제원은
|
||||
// 좌·우 두 칸으로 나눈다(2026-08-30 사용자 지시 1). 설치 측이 한쪽이면 그 칸만 뜬다.
|
||||
// 좌·우가 저장 채널(유입/유출) 중 어느 쪽인지는 측점 지형이 정하므로 이름표만
|
||||
// 바꿔 단다(`setRevetSideLabels`) — 값은 언제나 그 채널 키로 오간다.
|
||||
const revetSide = optionalSelect("양쪽", ["양쪽", "좌", "우"]);
|
||||
revetSide.value = "양쪽";
|
||||
const revetTiers = numberInput("1", "1");
|
||||
revetTiers.value = "1";
|
||||
// 옵션 키는 배관 유출 기슭막이와 **같은 한 벌**을 쓴다 — B06 조정창의 구간값도 이 키로
|
||||
// 저장되므로 원천이 하나여야 한다(2026-08-28: 전용 키 `length_m`을 쓰던 동안 조정창
|
||||
// 값이 매번 되돌아갔다). 읽을 때만 옛 전용 키를 폴백으로 본다.
|
||||
const standaloneRevet = createRevetmentFields(OUTLET_REVET_KEYS, {
|
||||
...REVET_COMMON_DEFAULTS,
|
||||
form: "돌쌓기(메)",
|
||||
});
|
||||
/** 옛 전용 키(`form`·`length_m`…)로 저장된 값을 배관 키 자리로 옮겨 읽는다. */
|
||||
const revetTopRow = grid(
|
||||
labeled("설치 측", revetSide),
|
||||
labeled("단 수(다단)", stepper(revetTiers, 1)),
|
||||
);
|
||||
const revetInlet = createRevetSideGroup(INLET_REVET_KEYS, "돌쌓기(메)");
|
||||
const revetOutlet = createRevetSideGroup(OUTLET_REVET_KEYS, "돌쌓기(메)");
|
||||
/** 좌·우 이름표 — B06이 그 측점의 벽 방향을 알려 주면 바뀐다. 기본은 유입=좌. */
|
||||
let revetSideLabels: { inlet: "좌" | "우"; outlet: "좌" | "우" } = {
|
||||
inlet: "좌",
|
||||
outlet: "우",
|
||||
};
|
||||
/** 옛 전용 키(`form`·`length_m`…)와 한 벌 저장분을 **양쪽 칸**으로 펼쳐 읽는다. */
|
||||
const legacyRevetOptions = (
|
||||
options: Record<string, string | number>,
|
||||
): Record<string, string | number> => {
|
||||
const merged = { ...options };
|
||||
const pairs: Array<[string, string]> = [
|
||||
["outlet_revet_form", "form"],
|
||||
["outlet_revet_length_m", "length_m"],
|
||||
["outlet_revet_height_m", "height_m"],
|
||||
["outlet_revet_before_m", "before_m"],
|
||||
["outlet_revet_after_m", "after_m"],
|
||||
const pairs: Array<[string, string, string]> = [
|
||||
["outlet_revet_form", "inlet_revet_form", "form"],
|
||||
["outlet_revet_length_m", "inlet_revet_length_m", "length_m"],
|
||||
["outlet_revet_height_m", "inlet_revet_height_m", "height_m"],
|
||||
["outlet_revet_before_m", "inlet_revet_before_m", "before_m"],
|
||||
["outlet_revet_after_m", "inlet_revet_after_m", "after_m"],
|
||||
];
|
||||
for (const [key, legacy] of pairs) {
|
||||
if (merged[key] === undefined && options[legacy] !== undefined) merged[key] = options[legacy];
|
||||
for (const [outletKey, inletKey, legacy] of pairs) {
|
||||
if (merged[outletKey] === undefined && options[legacy] !== undefined) {
|
||||
merged[outletKey] = options[legacy];
|
||||
}
|
||||
// 한쪽만 저장된 옛 자료 — 반대쪽 칸도 같은 값으로 세운다.
|
||||
if (merged[inletKey] === undefined && merged[outletKey] !== undefined) {
|
||||
merged[inletKey] = merged[outletKey];
|
||||
}
|
||||
if (merged[outletKey] === undefined && merged[inletKey] !== undefined) {
|
||||
merged[outletKey] = merged[inletKey];
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
};
|
||||
revetGroup.body.append(
|
||||
grid(labeled("설치 측", revetSide), standaloneRevet.formField),
|
||||
...standaloneRevet.rows,
|
||||
grid(labeled("단 수(다단)", revetTiers)),
|
||||
);
|
||||
|
||||
// ── BOX암거 — 본체 규격(프리셋 + 사용자 지정)·날개벽(유입·유출 개별) ────
|
||||
// 폭·높이 자유 입력은 "사용자 지정"을 골랐을 때만 펼친다(2026-08-17 사용자 확정).
|
||||
@@ -418,7 +430,9 @@ export function createFacilityOptionsForm(
|
||||
inletGroup.root,
|
||||
outletGroup.root,
|
||||
extraGroup.root,
|
||||
revetGroup.root,
|
||||
revetTopRow,
|
||||
revetInlet.root,
|
||||
revetOutlet.root,
|
||||
boxWrap,
|
||||
wingInFields.root,
|
||||
wingOutFields.root,
|
||||
@@ -450,7 +464,15 @@ export function createFacilityOptionsForm(
|
||||
// 바닥 경사는 물넘이포장만 쓴다 — 세월교는 구체 위 노면이라 파임이 없다.
|
||||
fordSlopeRow.hidden = current !== "ford_pavement";
|
||||
fordSummary.hidden = !isFord;
|
||||
revetGroup.root.hidden = current !== "revetment";
|
||||
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);
|
||||
revetInlet.legend.textContent = `기슭막이 (${revetSideLabels.inlet})`;
|
||||
revetOutlet.legend.textContent = `기슭막이 (${revetSideLabels.outlet})`;
|
||||
// 추가 기슭막이 칸은 **내용이 있을 때만** 선다 — 고른 다단 벽의 행이 여기 오면 뜨고,
|
||||
// 선택이 풀리면 스스로 사라진다.
|
||||
extraGroup.root.hidden = extraSlot.childElementCount === 0;
|
||||
@@ -489,7 +511,8 @@ export function createFacilityOptionsForm(
|
||||
);
|
||||
inletRevet.onChange(emit);
|
||||
outletRevet.onChange(emit);
|
||||
standaloneRevet.onChange(emit);
|
||||
revetInlet.fields.onChange(emit);
|
||||
revetOutlet.fields.onChange(emit);
|
||||
[revetSide, revetTiers].forEach((input) => input.addEventListener("change", emit));
|
||||
[
|
||||
pipeMaterial,
|
||||
@@ -514,6 +537,10 @@ export function createFacilityOptionsForm(
|
||||
inletSlot,
|
||||
outletSlot,
|
||||
extraSlot,
|
||||
setRevetSideLabels(labels) {
|
||||
revetSideLabels = labels;
|
||||
syncVisibility();
|
||||
},
|
||||
inletStructure: () => inletKindOf(inletType.value).structure,
|
||||
setInletStructure(value) {
|
||||
// 지금 고른 정본 종류(기슭막이/집수정)는 지키고 형식만 맞춘다.
|
||||
@@ -581,7 +608,9 @@ export function createFacilityOptionsForm(
|
||||
fordSlope.value = text("ford_slope_pct");
|
||||
revetSide.value = text("side") || "양쪽";
|
||||
revetTiers.value = text("tiers") || "1";
|
||||
standaloneRevet.write(legacyRevetOptions(options));
|
||||
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(
|
||||
@@ -635,18 +664,9 @@ export function createFacilityOptionsForm(
|
||||
options.side = revetSide.value;
|
||||
const tiers = Number.parseInt(revetTiers.value, 10);
|
||||
if (Number.isFinite(tiers) && tiers >= 1) options.tiers = tiers;
|
||||
standaloneRevet.read(options);
|
||||
// 양쪽 벽은 같은 한 벌이 정본 — 유입 키에도 같은 값을 실어 벽 둘이 갈리지 않게 한다.
|
||||
for (const [inletKey, outletKey] of [
|
||||
[INLET_REVET_KEYS.form, OUTLET_REVET_KEYS.form],
|
||||
[INLET_REVET_KEYS.length, OUTLET_REVET_KEYS.length],
|
||||
[INLET_REVET_KEYS.height, OUTLET_REVET_KEYS.height],
|
||||
[INLET_REVET_KEYS.before, OUTLET_REVET_KEYS.before],
|
||||
[INLET_REVET_KEYS.after, OUTLET_REVET_KEYS.after],
|
||||
] as Array<[string | undefined, string | undefined]>) {
|
||||
if (inletKey && outletKey && options[outletKey] !== undefined)
|
||||
options[inletKey] = options[outletKey];
|
||||
}
|
||||
// 좌·우가 각자 값을 갖는다 — 저장 채널은 유입/유출 키 그대로다(2026-08-30).
|
||||
revetInlet.fields.read(options);
|
||||
revetOutlet.fields.read(options);
|
||||
}
|
||||
return options;
|
||||
},
|
||||
|
||||
@@ -205,6 +205,23 @@ export const OUTLET_REVET_KEYS: RevetmentKeys = {
|
||||
after: "outlet_revet_after_m",
|
||||
};
|
||||
|
||||
/** 독립 기슭막이 한쪽 칸 — 배관 유출구와 **같은 양식**([구조][형태] + 길이·높이·전/후).
|
||||
* 구조 선택지는 기슭막이 하나뿐이지만 레이아웃을 맞추려 그대로 둔다(2026-08-30 사용자). */
|
||||
export interface RevetSideGroup {
|
||||
root: HTMLFieldSetElement;
|
||||
legend: HTMLLegendElement;
|
||||
fields: RevetmentFields;
|
||||
}
|
||||
|
||||
export function createRevetSideGroup(keys: RevetmentKeys, formDefault: string): RevetSideGroup {
|
||||
const box = group("기슭막이");
|
||||
const kind = document.createElement("select");
|
||||
kind.replaceChildren(new Option("기슭막이", "기슭막이"));
|
||||
const fields = createRevetmentFields(keys, { ...REVET_COMMON_DEFAULTS, form: formDefault });
|
||||
box.body.append(grid(labeled("구조", kind), fields.formField), ...fields.rows);
|
||||
return { root: box.root, legend: box.root.querySelector("legend")!, fields };
|
||||
}
|
||||
|
||||
export interface RevetmentFields {
|
||||
/** 형태 칸 — 유입·유출 그룹에서 "구조" 옆 2열째에 놓는다(2026-08-17 지시 3·4). */
|
||||
formField: HTMLLabelElement;
|
||||
|
||||
@@ -164,6 +164,22 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
const adjust = stationControls.revetOffset.adjustFor(owner, role);
|
||||
return revetWallSpec(spec, adjust, culvert.hidden_pipe === true, culvert.diameter_m).form;
|
||||
},
|
||||
// 좌·우 이름표 — +offset이 좌측이고 유입 벽은 오르막(계류) 쪽에 선다.
|
||||
wallSideLabels: (chainageM) => {
|
||||
const owner = sectionDetail?.cross_sections.find(
|
||||
(section) => Math.abs(section.chainage_m - chainageM) < 0.51,
|
||||
);
|
||||
if (!owner) return null;
|
||||
const inletLeft = (owner.uphill_side ?? "left") === "left";
|
||||
return inletLeft ? { inlet: "좌", outlet: "우" } : { inlet: "우", outlet: "좌" };
|
||||
},
|
||||
// 폼 단 수 → 다단 채널(유출 = extra, 유입·계류측 = bextra). 독립 기슭막이는 좌·우
|
||||
// 두 벽이 같은 단 수를 쓰므로 두 채널에 같이 넣는다.
|
||||
setTierCount: (chainageM, tiers) => {
|
||||
const extras = Math.max(0, Math.round(tiers) - 1);
|
||||
stationControls.extraWalls.setCount(chainageM, extras, "outlet");
|
||||
stationControls.extraWalls.setCount(chainageM, extras, "basin");
|
||||
},
|
||||
stationInterval: () => stationInterval ?? 20,
|
||||
focusChainage: focusStationAt,
|
||||
queuePipeOptions: (chainageM, patch) => stationControls.queueCulvertOptions(chainageM, patch),
|
||||
|
||||
@@ -45,6 +45,10 @@ export interface B06StructuresPanelDeps {
|
||||
wallHeight?: (chainageM: number, role: "inlet" | "outlet") => number | null;
|
||||
/** 지금 그려진 벽 형태 — 조정값(m)이 있으면 그쪽이 정본이라 폼도 이 값을 보인다. */
|
||||
wallForm?: (chainageM: number, role: "inlet" | "outlet") => string | null;
|
||||
/** 그 측점에서 유입·유출 벽이 각각 좌·우 어느 쪽인가(독립 기슭막이 칸 이름표). */
|
||||
wallSideLabels?: (chainageM: number) => { inlet: "좌" | "우"; outlet: "좌" | "우" } | null;
|
||||
/** 폼의 단 수를 다단 채널에 반영한다 — 조정창에서 세우던 것과 같은 자리. */
|
||||
setTierCount?: (chainageM: number, tiers: number) => void;
|
||||
/** 측점 간격(m) — 측점번호+잔여거리 환산용. */
|
||||
stationInterval: () => number;
|
||||
/** 목록·폼에서 고른 시설의 측점 카드를 선택·스크롤한다. */
|
||||
@@ -193,6 +197,10 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
showToast(PIPE_ADD_GUIDE, "error");
|
||||
return;
|
||||
}
|
||||
// 단 수는 옵션이자 **조작 채널**이다 — 조정창에서 세우던 그 자리로 보내야
|
||||
// 횡단도에 단이 선다(2026-08-30 사용자 지시 3: 배수관 측점과 동일하게).
|
||||
const tiers = Number(patch.tiers);
|
||||
if (Number.isFinite(tiers)) deps.setTierCount?.(fromChainageM, tiers);
|
||||
// 옵션은 **옮기기 전 자리**로 예약한다 — 저장 시점의 관 목록이 그 자리 기준이다.
|
||||
deps.queuePipeOptions(fromChainageM, patch as Record<string, number | string>);
|
||||
deps.applyPipeOptions?.(fromChainageM, patch as Record<string, number | string>);
|
||||
@@ -266,12 +274,19 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
}
|
||||
}
|
||||
|
||||
/** 폼에 올리기 직전에 좌·우 이름표를 그 측점 방향으로 맞춘다(독립 기슭막이 칸). */
|
||||
function syncSideLabels(chainageM: number): void {
|
||||
const labels = deps.wallSideLabels?.(chainageM);
|
||||
if (labels) section.facility.setRevetSideLabels(labels);
|
||||
}
|
||||
|
||||
/** 폼에 올리기 직전에 그 관의 옵션을 최신 스펙으로 보강한다(빈 칸만 채운다). */
|
||||
function refreshSpecDefaults(chainageM: number): void {
|
||||
const hit = pipeFacilities.find(
|
||||
(entry) => Math.abs(entry.chainage_m - chainageM) < PIPE_MATCH_M,
|
||||
);
|
||||
if (!hit) return;
|
||||
syncSideLabels(hit.chainage_m);
|
||||
hit.options = withSpecDefaults(
|
||||
hit.options,
|
||||
deps.detail?.() ?? null,
|
||||
|
||||
Reference in New Issue
Block a user