fix(B05,B06): 독립 기슭막이 단 수를 좌·우로 나누고 그 칸을 강조한다

- 단 수를 바깥 행에서 좌·우 칸 안으로 옮긴다. 저장 키도 좌·우 각각
  (inlet_revet_tiers·outlet_revet_tiers, 옛 tiers는 폴백·호환 기록).
  폼 값은 다단 채널(extra·bextra)로 가고, 폼은 그 채널 값을 되읽는다.
- 좌·우 칸에도 조정창 행 이식 자리를 두어, 벽을 고르면 배관 유입구·유출구와
  같이 그 칸이 강조되고 조정 항목이 옮겨 온다. 칸 이름표는 좌·우 그대로 둔다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-29 19:09:42 +09:00
co-authored by Claude Opus 5
parent 86a3266e0e
commit 091a30286e
5 changed files with 97 additions and 31 deletions
+25 -12
View File
@@ -184,6 +184,9 @@ export interface FacilityOptionsForm {
outletSlot: HTMLElement;
/** 다단(추가) 기슭막이 행이 옮겨 붙는 자리 — 비면 그룹째 숨는다(2026-08-30 지시 4). */
extraSlot: HTMLElement;
/** 독립 기슭막이 좌·우 칸의 이식 자리 — 배관 유입구·유출구 자리와 같은 몫이다. */
revetInletSlot: HTMLElement;
revetOutletSlot: HTMLElement;
/** 유입구 "구조"에 합쳐진 B06 형식 값(2026-08-29 지시 5). B06이 조정창 값과 맞춘다. */
inletStructure: () => InletStructureKind;
setInletStructure: (value: InletStructureKind) => void;
@@ -284,18 +287,13 @@ export function createFacilityOptionsForm(
extraGroup.root.hidden = true;
// ── 독립 기슭막이(2026-08-28 이관: 배관처럼 pipe_points 관리) ─────────────
// 설치 측·단 수는 **컨테이너 밖**(배관의 관종·관경과 같은 층)에 두고, 벽 제원
// 좌·우 두 칸으로 나눈다(2026-08-30 사용자 지시 1). 설치 측이 한쪽이면 그 칸만 뜬다.
// 설치 측 **컨테이너 밖**(배관의 관종·관경과 같은 층), 벽 제원과 **단 수**는 좌·우
// 두 칸이 각자 갖는다(2026-08-30 사용자). 설치 측이 한쪽이면 그 칸만 뜬다.
// 좌·우가 저장 채널(유입/유출) 중 어느 쪽인지는 측점 지형이 정하므로 이름표만
// 바꿔 단다(`setRevetSideLabels`) — 값은 언제나 그 채널 키로 오간다.
const revetSide = optionalSelect("양쪽", ["양쪽", "좌", "우"]);
revetSide.value = "양쪽";
const revetTiers = numberInput("1", "1");
revetTiers.value = "1";
const revetTopRow = grid(
labeled("설치 측", revetSide),
labeled("단 수(다단)", stepper(revetTiers, 1)),
);
const revetTopRow = grid(labeled("설치 측", revetSide));
const revetInlet = createRevetSideGroup(INLET_REVET_KEYS, "돌쌓기(메)");
const revetOutlet = createRevetSideGroup(OUTLET_REVET_KEYS, "돌쌓기(메)");
/** 좌·우 이름표 — B06이 그 측점의 벽 방향을 알려 주면 바뀐다. 기본은 유입=좌. */
@@ -513,7 +511,9 @@ export function createFacilityOptionsForm(
outletRevet.onChange(emit);
revetInlet.fields.onChange(emit);
revetOutlet.fields.onChange(emit);
[revetSide, revetTiers].forEach((input) => input.addEventListener("change", emit));
[revetSide, revetInlet.tiers, revetOutlet.tiers].forEach((input) =>
input.addEventListener("change", emit),
);
[
pipeMaterial,
pipeDiameter,
@@ -537,6 +537,8 @@ export function createFacilityOptionsForm(
inletSlot,
outletSlot,
extraSlot,
revetInletSlot: revetInlet.slot,
revetOutletSlot: revetOutlet.slot,
setRevetSideLabels(labels) {
revetSideLabels = labels;
syncVisibility();
@@ -607,7 +609,10 @@ export function createFacilityOptionsForm(
fordHeight.value = text("ford_height_m");
fordSlope.value = text("ford_slope_pct");
revetSide.value = text("side") || "양쪽";
revetTiers.value = text("tiers") || "1";
// 단 수 — 좌·우 각자 키가 먼저고, 옛 한 벌 값(`tiers`)이 폴백이다.
const legacyTiers = text("tiers") || "1";
revetInlet.tiers.value = text("inlet_revet_tiers") || legacyTiers;
revetOutlet.tiers.value = text("outlet_revet_tiers") || legacyTiers;
const spread = legacyRevetOptions(options);
revetInlet.fields.write(spread);
revetOutlet.fields.write(spread);
@@ -662,8 +667,16 @@ export function createFacilityOptionsForm(
wingOutFields.read(options);
} else if (current === "revetment") {
options.side = revetSide.value;
const tiers = Number.parseInt(revetTiers.value, 10);
if (Number.isFinite(tiers) && tiers >= 1) options.tiers = tiers;
const tierOf = (input: HTMLInputElement): number | null => {
const value = Number.parseInt(input.value, 10);
return Number.isFinite(value) && value >= 1 ? value : null;
};
const inletTiers = tierOf(revetInlet.tiers);
const outletTiers = tierOf(revetOutlet.tiers);
if (inletTiers !== null) options.inlet_revet_tiers = inletTiers;
if (outletTiers !== null) options.outlet_revet_tiers = outletTiers;
// 옛 한 벌 키도 남긴다 — 서버 `_revet_set`이 아직 이 값을 읽는다.
options.tiers = Math.max(inletTiers ?? 1, outletTiers ?? 1);
// 좌·우가 각자 값을 갖는다 — 저장 채널은 유입/유출 키 그대로다(2026-08-30).
revetInlet.fields.read(options);
revetOutlet.fields.read(options);
@@ -211,6 +211,10 @@ export interface RevetSideGroup {
root: HTMLFieldSetElement;
legend: HTMLLegendElement;
fields: RevetmentFields;
/** 단 수(다단) — 좌·우가 각자 갖는다(2026-08-30 사용자). */
tiers: HTMLInputElement;
/** 조정창 행이 옮겨 붙는 자리 — 배관 유입구·유출구 칸과 같은 규칙. */
slot: HTMLElement;
}
export function createRevetSideGroup(keys: RevetmentKeys, formDefault: string): RevetSideGroup {
@@ -218,8 +222,20 @@ export function createRevetSideGroup(keys: RevetmentKeys, formDefault: string):
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 };
const tiers = numberInput("1", "1");
tiers.value = "1";
const slot = document.createElement("div");
slot.className = "b05-structure__adjust-slot";
box.body.append(
grid(labeled("구조", kind), fields.formField),
...fields.rows,
grid(labeled("단 수(다단)", stepper(tiers, 1))),
slot,
);
const legend = box.root.querySelector("legend")!;
// 이름표는 좌·우가 정해 준다 — 고른 구조물 이름을 덧붙이지 않는다(2026-08-30).
legend.dataset.fixedLabel = "1";
return { root: box.root, legend, fields, tiers, slot };
}
export interface RevetmentFields {
+25 -9
View File
@@ -15,14 +15,18 @@ let slot: HTMLElement | null = null;
let placeholder: HTMLElement | null = null;
/** 유입구·유출구·추가 기슭막이 그룹의 이식 자리(2026-08-29 지시 4, 2026-08-30 지시 4).
* B06 패널이 등록한다. */
let sideSlots: { inlet: HTMLElement; outlet: HTMLElement; extra: HTMLElement } | null = null;
/** 행을 옮겨 붙일 자리를 등록한다(계곡 통과 시설 폼의 유입구·유출구·추가 기슭막이). */
export function setAdjustSideSlots(next: {
interface SideSlots {
inlet: HTMLElement;
outlet: HTMLElement;
extra: HTMLElement;
}): void {
/** 독립 기슭막이 좌·우 칸 — 배관 유입구·유출구 칸이 숨어 있을 때 이쪽으로 간다. */
revetInlet: HTMLElement;
revetOutlet: HTMLElement;
}
let sideSlots: SideSlots | null = null;
/** 행을 옮겨 붙일 자리를 등록한다(계곡 통과 시설 폼의 유입구·유출구·추가 기슭막이). */
export function setAdjustSideSlots(next: SideSlots): void {
sideSlots = next;
}
@@ -55,8 +59,8 @@ const movedRows: Array<{ row: HTMLElement; anchor: Comment }> = [];
/** 그룹 이름표 — 원래 문구를 한 번만 적어 두고, 선택에 따라 괄호 표기를 붙였다 뗀다. */
function setGroupLabel(group: HTMLElement, suffix: string): void {
const legend = group.querySelector("legend");
if (!legend) return;
const legend = group.querySelector<HTMLElement>("legend");
if (!legend || legend.dataset.fixedLabel) return;
legend.dataset.baseLabel ??= legend.textContent ?? "";
const base = legend.dataset.baseLabel;
legend.textContent = suffix ? `${base} (${suffix})` : base;
@@ -68,7 +72,13 @@ function clearSideSlots(): void {
const { row, anchor } = movedRows.pop()!;
anchor.parentNode?.replaceChild(row, anchor);
}
for (const target of [sideSlots?.inlet, sideSlots?.outlet, sideSlots?.extra]) {
for (const target of [
sideSlots?.inlet,
sideSlots?.outlet,
sideSlots?.extra,
sideSlots?.revetInlet,
sideSlots?.revetOutlet,
]) {
target?.replaceChildren();
const group = target?.closest("fieldset");
if (!group) continue;
@@ -87,7 +97,13 @@ function relocateSideRows(panelRoot: HTMLElement): void {
clearSideSlots();
const side = panelRoot.dataset.side;
if (!sideSlots || (side !== "inlet" && side !== "outlet" && side !== "extra")) return;
const target = sideSlots[side];
// 배관이면 유입구·유출구 칸, 독립 기슭막이면 좌·우 칸 — 지금 서 있는 쪽으로 간다.
const target =
side === "extra"
? sideSlots.extra
: slotUsable(sideSlots[side])
? sideSlots[side]
: sideSlots[side === "inlet" ? "revetInlet" : "revetOutlet"];
// 추가 기슭막이 칸은 평소 숨어 있다 — 고른 벽이 다단이면 여기서 세운다.
if (side === "extra") {
const group = target.closest<HTMLElement>("fieldset");
+14 -5
View File
@@ -173,12 +173,21 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
const inletLeft = (owner.uphill_side ?? "left") === "left";
return inletLeft ? { inlet: "좌", outlet: "우" } : { inlet: "우", outlet: "좌" };
},
// 폼 단 수 다단 채널(유출 = extra, 유입·계류측 = bextra). 독립 기슭막이는 좌·우
// 두 벽이 같은 단 수를 쓰므로 두 채널에 같이 넣는다.
setTierCount: (chainageM, tiers) => {
// 폼 단 수 다단 채널(유출 = extra, 유입·계류측 = bextra) — 좌·우가 각자 쓴다.
setTierCount: (chainageM, role, tiers) => {
const extras = Math.max(0, Math.round(tiers) - 1);
stationControls.extraWalls.setCount(chainageM, extras, "outlet");
stationControls.extraWalls.setCount(chainageM, extras, "basin");
stationControls.extraWalls.setCount(
chainageM,
extras,
role === "outlet" ? "outlet" : "basin",
);
},
tierCount: (chainageM, role) => {
const owner = sectionDetail?.cross_sections.find(
(section) => Math.abs(section.chainage_m - chainageM) < 0.51,
);
if (!owner) return null;
return stationControls.extraWalls.countFor(owner, role === "outlet" ? "outlet" : "basin") + 1;
},
stationInterval: () => stationInterval ?? 20,
focusChainage: focusStationAt,
@@ -48,7 +48,9 @@ export interface B06StructuresPanelDeps {
/** 그 측점에서 유입·유출 벽이 각각 좌·우 어느 쪽인가(독립 기슭막이 칸 이름표). */
wallSideLabels?: (chainageM: number) => { inlet: "좌" | "우"; outlet: "좌" | "우" } | null;
/** 폼의 단 수를 다단 채널에 반영한다 — 조정창에서 세우던 것과 같은 자리. */
setTierCount?: (chainageM: number, tiers: number) => void;
setTierCount?: (chainageM: number, role: "inlet" | "outlet", tiers: number) => void;
/** 지금 선 단 수(기준벽 포함) — 폼이 도면과 같은 값을 보이게 한다. */
tierCount?: (chainageM: number, role: "inlet" | "outlet") => number | null;
/** 측점 간격(m) — 측점번호+잔여거리 환산용. */
stationInterval: () => number;
/** 목록·폼에서 고른 시설의 측점 카드를 선택·스크롤한다. */
@@ -110,6 +112,7 @@ function withSpecDefaults(
chainageM: number,
wallHeight?: (chainageM: number, role: "inlet" | "outlet") => number | null,
wallForm?: (chainageM: number, role: "inlet" | "outlet") => string | null,
tierCount?: (chainageM: number, role: "inlet" | "outlet") => number | null,
): Record<string, string | number> | undefined {
const owner = detail?.cross_sections.find(
(section) => Math.abs(section.chainage_m - chainageM) < PIPE_MATCH_M,
@@ -136,6 +139,9 @@ function withSpecDefaults(
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);
// 단 수는 다단 채널이 정본이다 — 폼이 그 값을 그대로 보인다.
const tiers = tierCount?.(chainageM, prefix);
if (tiers != null) merged[`${prefix}_revet_tiers`] = tiers;
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);
@@ -199,8 +205,10 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
}
// 단 수는 옵션이자 **조작 채널**이다 — 조정창에서 세우던 그 자리로 보내야
// 횡단도에 단이 선다(2026-08-30 사용자 지시 3: 배수관 측점과 동일하게).
const tiers = Number(patch.tiers);
if (Number.isFinite(tiers)) deps.setTierCount?.(fromChainageM, tiers);
for (const role of ["inlet", "outlet"] as const) {
const tiers = Number(patch[`${role}_revet_tiers`]);
if (Number.isFinite(tiers)) deps.setTierCount?.(fromChainageM, role, tiers);
}
// 옵션은 **옮기기 전 자리**로 예약한다 — 저장 시점의 관 목록이 그 자리 기준이다.
deps.queuePipeOptions(fromChainageM, patch as Record<string, number | string>);
deps.applyPipeOptions?.(fromChainageM, patch as Record<string, number | string>);
@@ -233,6 +241,8 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
inlet: section.facility.inletSlot,
outlet: section.facility.outletSlot,
extra: section.facility.extraSlot,
revetInlet: section.facility.revetInletSlot,
revetOutlet: section.facility.revetOutletSlot,
});
async function load(): Promise<void> {
@@ -262,6 +272,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
pipe.chainage_m,
deps.wallHeight,
deps.wallForm,
deps.tierCount,
),
design_flow_m3s: null,
}));
@@ -293,6 +304,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
hit.chainage_m,
deps.wallHeight,
deps.wallForm,
deps.tierCount,
);
section.setPipeFacilities(pipeFacilities);
}