merge: 구조물 패널 후속 수정 4건을 통합한다
This commit is contained in:
@@ -287,8 +287,8 @@ export function createFacilityOptionsForm(
|
||||
extraGroup.root.hidden = true;
|
||||
|
||||
// ── 독립 기슭막이(2026-08-28 이관: 배관처럼 pipe_points 관리) ─────────────
|
||||
// 설치 측은 **컨테이너 밖**(배관의 관종·관경과 같은 층), 벽 제원과 **단 수**는 좌·우
|
||||
// 두 칸이 각자 갖는다(2026-08-30 사용자). 설치 측이 한쪽이면 그 칸만 뜬다.
|
||||
// 설치 측은 **컨테이너 밖**(배관의 관종·관경과 같은 층), 벽 제원은 좌·우 두 칸으로
|
||||
// 나눈다(2026-08-30 사용자). 설치 측이 한쪽이면 그 칸만 뜬다.
|
||||
// 좌·우가 저장 채널(유입/유출) 중 어느 쪽인지는 측점 지형이 정하므로 이름표만
|
||||
// 바꿔 단다(`setRevetSideLabels`) — 값은 언제나 그 채널 키로 오간다.
|
||||
const revetSide = optionalSelect("양쪽", ["양쪽", "좌", "우"]);
|
||||
@@ -511,9 +511,7 @@ export function createFacilityOptionsForm(
|
||||
outletRevet.onChange(emit);
|
||||
revetInlet.fields.onChange(emit);
|
||||
revetOutlet.fields.onChange(emit);
|
||||
[revetSide, revetInlet.tiers, revetOutlet.tiers].forEach((input) =>
|
||||
input.addEventListener("change", emit),
|
||||
);
|
||||
revetSide.addEventListener("change", emit);
|
||||
[
|
||||
pipeMaterial,
|
||||
pipeDiameter,
|
||||
@@ -609,10 +607,6 @@ export function createFacilityOptionsForm(
|
||||
fordHeight.value = text("ford_height_m");
|
||||
fordSlope.value = text("ford_slope_pct");
|
||||
revetSide.value = text("side") || "양쪽";
|
||||
// 단 수 — 좌·우 각자 키가 먼저고, 옛 한 벌 값(`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);
|
||||
@@ -667,16 +661,8 @@ export function createFacilityOptionsForm(
|
||||
wingOutFields.read(options);
|
||||
} else if (current === "revetment") {
|
||||
options.side = revetSide.value;
|
||||
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);
|
||||
// 단 수는 폼이 갖지 않는다 — 횡단 설계 patch(extra_wall_counts)가 정본이고
|
||||
// 조작은 [추가 기슭막이(단)] 행이 한다(2026-08-30 사용자: 중복이라 삭제).
|
||||
// 좌·우가 각자 값을 갖는다 — 저장 채널은 유입/유출 키 그대로다(2026-08-30).
|
||||
revetInlet.fields.read(options);
|
||||
revetOutlet.fields.read(options);
|
||||
|
||||
@@ -211,8 +211,6 @@ export interface RevetSideGroup {
|
||||
root: HTMLFieldSetElement;
|
||||
legend: HTMLLegendElement;
|
||||
fields: RevetmentFields;
|
||||
/** 단 수(다단) — 좌·우가 각자 갖는다(2026-08-30 사용자). */
|
||||
tiers: HTMLInputElement;
|
||||
/** 조정창 행이 옮겨 붙는 자리 — 배관 유입구·유출구 칸과 같은 규칙. */
|
||||
slot: HTMLElement;
|
||||
}
|
||||
@@ -222,20 +220,19 @@ 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 });
|
||||
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))),
|
||||
// 단 수는 여기 두지 않는다 — 조정창에서 옮겨 오는 [추가 기슭막이(단)] 행과
|
||||
// 같은 값이라 두 벌이면 헷갈린다(2026-08-30 사용자).
|
||||
slot,
|
||||
);
|
||||
const legend = box.root.querySelector("legend")!;
|
||||
// 이름표는 좌·우가 정해 준다 — 고른 구조물 이름을 덧붙이지 않는다(2026-08-30).
|
||||
legend.dataset.fixedLabel = "1";
|
||||
return { root: box.root, legend, fields, tiers, slot };
|
||||
return { root: box.root, legend, fields, slot };
|
||||
}
|
||||
|
||||
export interface RevetmentFields {
|
||||
|
||||
@@ -52,6 +52,14 @@ function slotUsable(target: HTMLElement | undefined): target is HTMLElement {
|
||||
return !!group && !group.hidden;
|
||||
}
|
||||
|
||||
/** 배관이면 유입구·유출구 칸, 독립 기슭막이면 좌·우 칸 — 지금 서 있는 쪽을 준다. */
|
||||
function slotOf(side: "inlet" | "outlet"): HTMLElement {
|
||||
const slots = sideSlots!;
|
||||
return slotUsable(slots[side])
|
||||
? slots[side]
|
||||
: slots[side === "inlet" ? "revetInlet" : "revetOutlet"];
|
||||
}
|
||||
|
||||
/** 옮겨 온 행과 그 **제자리 표식** — 되돌릴 때 원래 순서로 꽂기 위해 짝으로 둔다.
|
||||
* 표식 없이 지우면 행 DOM이 사라져, 같은 창에서 다른 벽을 고를 때 그 행이 다시는
|
||||
* 나오지 않는다(2026-08-29 사용자 보고: 단 수·옵션이 사라짐). */
|
||||
@@ -97,17 +105,16 @@ function relocateSideRows(panelRoot: HTMLElement): void {
|
||||
clearSideSlots();
|
||||
const side = panelRoot.dataset.side;
|
||||
if (!sideSlots || (side !== "inlet" && side !== "outlet" && side !== "extra")) return;
|
||||
// 배관이면 유입구·유출구 칸, 독립 기슭막이면 좌·우 칸 — 지금 서 있는 쪽으로 간다.
|
||||
const target =
|
||||
side === "extra"
|
||||
? sideSlots.extra
|
||||
: slotUsable(sideSlots[side])
|
||||
? sideSlots[side]
|
||||
: sideSlots[side === "inlet" ? "revetInlet" : "revetOutlet"];
|
||||
// 추가 기슭막이 칸은 평소 숨어 있다 — 고른 벽이 다단이면 여기서 세운다.
|
||||
const target = side === "extra" ? sideSlots.extra : slotOf(side);
|
||||
// 추가 기슭막이 칸은 평소 숨어 있다 — 고른 벽이 다단이면 **그 단이 딸린 칸 바로
|
||||
// 아래**로 옮겨 세운다(2026-08-30 사용자: 좌/우 각각의 하단으로).
|
||||
if (side === "extra") {
|
||||
const group = target.closest<HTMLElement>("fieldset");
|
||||
if (!group) return;
|
||||
const owner = slotOf(panelRoot.dataset.extraSide === "inlet" ? "inlet" : "outlet").closest(
|
||||
"fieldset",
|
||||
);
|
||||
if (owner && owner !== group) owner.after(group);
|
||||
group.hidden = false;
|
||||
} else if (!slotUsable(target)) return;
|
||||
const rows = panelRoot.querySelectorAll<HTMLElement>(side === "extra" ? EXTRA_ROWS : SIDE_ROWS);
|
||||
|
||||
@@ -525,9 +525,8 @@ export function buildStructurePanel(
|
||||
: movable
|
||||
? L("B06_Cross_Revet_Inlet")
|
||||
: L("B06_Cross_Struct_InletBasin");
|
||||
// 독립 기슭막이(관 숨김)는 유입·유출이 같은 성토 벽이다 — 집수정 형식 선택을
|
||||
// 숨기고 유입측에도 단 수 행을 준다(2026-08-29 사용자: 좌우 조작 통일).
|
||||
const ownTiers = deps.hiddenPipe() && key === "inlet";
|
||||
// 독립 기슭막이(관 숨김)는 유입·유출이 같은 성토 벽이다 — 집수정 형식 선택만 숨긴다
|
||||
// (단 수 행은 아래에서 기슭막이 기준벽이면 언제나 준다).
|
||||
// 형식 선택은 유입측에서만, 이동·높이 조작은 기슭막이(집수정 제외)만.
|
||||
structureRow.classList.toggle("is-hidden", key !== "inlet" || deps.hiddenPipe());
|
||||
if (key === "inlet" && !deps.hiddenPipe()) {
|
||||
@@ -575,6 +574,8 @@ export function buildStructurePanel(
|
||||
// 붙을지 알린다(2026-08-29 지시 4). 다단 벽은 자기 채널 쪽으로 간다.
|
||||
// 다단(추가) 벽은 유입·유출 어느 칸에도 자기 자리가 없다 — 유입구·유출구 아래
|
||||
// **전용 칸**으로 보낸다(2026-08-30 사용자 지시 4).
|
||||
// 다단 벽이 어느 쪽 채널의 단인지 — 그 쪽 칸 **바로 아래**에 붙인다(2026-08-30).
|
||||
root.dataset.extraSide = isExtra ? (key.startsWith("bextra") ? "inlet" : "outlet") : "";
|
||||
root.dataset.side = isExtra
|
||||
? "extra"
|
||||
: tierChannel === "outlet"
|
||||
@@ -592,14 +593,13 @@ export function buildStructurePanel(
|
||||
: tierChannel === "basin"
|
||||
? deps.basinExtraState()
|
||||
: { canAdd: false, count: 0 };
|
||||
// 기슭막이 기준벽(배관 유입·유출, 독립 좌·우)에서는 **항상** 보인다(2026-08-30
|
||||
// 사용자). 링크 카드에서 만져도 제어기가 소유 측점 값을 고치므로 안전하다.
|
||||
// 집수정 유입은 계류측 단이 설 자리가 있을 때만 — 벽이 아니라 구체다.
|
||||
const revetWall = (key === "inlet" || key === "outlet") && !isBasin;
|
||||
extraRow.classList.toggle(
|
||||
"is-hidden",
|
||||
deps.isLinkedCard() ||
|
||||
!(
|
||||
tierChannel === "outlet" ||
|
||||
(tierChannel === "basin" &&
|
||||
(ownTiers || (isBasin && (tiers.canAdd || tiers.count > 0))))
|
||||
),
|
||||
!revetWall && !(isBasin && tierChannel === "basin" && (tiers.canAdd || tiers.count > 0)),
|
||||
);
|
||||
currentCount = tiers.count;
|
||||
countValue.textContent = `${tiers.count}단`;
|
||||
@@ -620,7 +620,12 @@ export function buildStructurePanel(
|
||||
linkToggle.button.title = levelled ? LINK_LOCKED_TITLE : LINK_TITLE;
|
||||
gradeToggle.button.classList.toggle("is-hidden", currentSpan === null);
|
||||
gradeToggle.set(!levelled);
|
||||
optionParts.row.classList.toggle("is-hidden", linkState === null && currentSpan === null);
|
||||
// 옵션(연동·경사)도 기슭막이 기준벽에서는 항상 자리를 지킨다 — 상태 따라
|
||||
// 나타났다 사라지면 아래 행들이 널뛴다(2026-08-30 사용자).
|
||||
optionParts.row.classList.toggle(
|
||||
"is-hidden",
|
||||
!revetWall && linkState === null && currentSpan === null,
|
||||
);
|
||||
const adjust = deps.adjustFor(key);
|
||||
// 정보는 항목당 한 행(2026-08-22 사용자) — 0이면 "자동", 좌우는 안/바깥,
|
||||
// 상하는 위/아래로 적는다.
|
||||
|
||||
@@ -22,6 +22,14 @@ import type { WallLayout } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
const HATCH_CLASS = "b06-chart__culvert-stone";
|
||||
|
||||
/**
|
||||
* clipPath id 일련번호 — id는 **문서 전역**이라 같은 벽을 소유 측점과 연동 측점이
|
||||
* 함께 그리면 이름이 겹쳤다. `url(#id)`는 문서에서 **처음 만난** clipPath를 쓰므로,
|
||||
* 뒤 카드의 해칭이 앞 카드 좌표로 잘려 통째로 사라졌다(2026-08-30 사용자: 기준
|
||||
* 측점 벽을 옮기니 연동 측점 형태 표현이 다 없어졌다). 카드·다시 그리기마다 새 번호.
|
||||
*/
|
||||
let clipSeq = 0;
|
||||
|
||||
/** 형태 문자열 → 표현 갈래. 판정 순서는 좁은 말(돌망태·바자)부터. */
|
||||
export type WallHatch = "dry" | "wet" | "concrete" | "gabion" | "log" | "wattle";
|
||||
|
||||
@@ -174,7 +182,8 @@ export function appendWallHatch(
|
||||
),
|
||||
);
|
||||
|
||||
const clipId = `b06-revet-clip-${keyId}-${Math.round(wall.backOffset * 100)}`;
|
||||
clipSeq += 1;
|
||||
const clipId = `b06-revet-clip-${keyId}-${clipSeq}`;
|
||||
const clip = document.createElementNS(SVG_NS, "clipPath");
|
||||
clip.setAttribute("id", clipId);
|
||||
const clipShape = document.createElementNS(SVG_NS, "polygon");
|
||||
|
||||
@@ -173,22 +173,6 @@ 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, role, tiers) => {
|
||||
const extras = Math.max(0, Math.round(tiers) - 1);
|
||||
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,
|
||||
queuePipeOptions: (chainageM, patch) => stationControls.queueCulvertOptions(chainageM, patch),
|
||||
|
||||
@@ -52,10 +52,6 @@ export interface B06StructuresPanelDeps {
|
||||
wallForm?: (chainageM: number, role: "inlet" | "outlet") => string | null;
|
||||
/** 그 측점에서 유입·유출 벽이 각각 좌·우 어느 쪽인가(독립 기슭막이 칸 이름표). */
|
||||
wallSideLabels?: (chainageM: number) => { inlet: "좌" | "우"; outlet: "좌" | "우" } | null;
|
||||
/** 폼의 단 수를 다단 채널에 반영한다 — 조정창에서 세우던 것과 같은 자리. */
|
||||
setTierCount?: (chainageM: number, role: "inlet" | "outlet", tiers: number) => void;
|
||||
/** 지금 선 단 수(기준벽 포함) — 폼이 도면과 같은 값을 보이게 한다. */
|
||||
tierCount?: (chainageM: number, role: "inlet" | "outlet") => number | null;
|
||||
/** 측점 간격(m) — 측점번호+잔여거리 환산용. */
|
||||
stationInterval: () => number;
|
||||
/** 목록·폼에서 고른 시설의 측점 카드를 선택·스크롤한다. */
|
||||
@@ -117,7 +113,6 @@ 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,
|
||||
@@ -144,9 +139,6 @@ 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);
|
||||
@@ -210,10 +202,6 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
}
|
||||
// 단 수는 옵션이자 **조작 채널**이다 — 조정창에서 세우던 그 자리로 보내야
|
||||
// 횡단도에 단이 선다(2026-08-30 사용자 지시 3: 배수관 측점과 동일하게).
|
||||
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>);
|
||||
@@ -277,7 +265,6 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
pipe.chainage_m,
|
||||
deps.wallHeight,
|
||||
deps.wallForm,
|
||||
deps.tierCount,
|
||||
),
|
||||
design_flow_m3s: null,
|
||||
}));
|
||||
@@ -309,7 +296,6 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
hit.chainage_m,
|
||||
deps.wallHeight,
|
||||
deps.wallForm,
|
||||
deps.tierCount,
|
||||
);
|
||||
section.setPipeFacilities(pipeFacilities);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user