fix(B05·B06): 54-30 추가 흙막이 강조 유지 · 관 보호공 [연동] [경사] · 칸 고침 강조 · 흙막이 칸 일원화

- 폼이 상자를 새로 세울 때마다(단 수 · 유입 형식 · 벽 조작 · 저장 · 넘김) 고른 상자 강조 다시(specpaint 한 곳) · 54-29 의 두 군데 호출 대체
- 관 보호공(유입 · 유출) 상자 안 [연동] [경사] 반 폭 두 단추 — 옛 연동 제어 값(revet_link_detached · revet_follow_grade) 그대로 · 쓸 수 있을 때만 활성 · 경사 끄면 연동 잠김 · 누르면 횡단 그림 바로
- 상자 칸을 고치거나 고르면 그 벽을 고름(횡단에서 누른 것과 같은 길 · 카드 다시 그림) · 단 수 칸은 고른 단을 지킴
- 추가 흙막이 상자 = 사면안정 흙막이와 같은 칸(형태 · 길이 · 높이 · 전 · 후) · 구조 고르기 칸과 정의 이름 두 줄 뺌 · 저장 키 그대로
- 시험 54-29 · 54-27 · 52 명세 · 54-14 말풍선 기대값 맞춤

화면 확인(sub5_검증 12+18): 흙막이 1 강조 = 단 2→1 · 넘김 · 저장 뒤 유지 · 형태 고침 → 상자 · 벽 강조 · 경사 끔 → 13+0 벽 이동 · 두 줄 함께 · 목록 대 횡단 같은 글(칸 54) · 단 0 저장으로 되돌림

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ
This commit is contained in:
2026-10-06 13:47:56 +09:00
co-authored by Claude Opus 5.5
parent c8bd95b1aa
commit f93cc56284
16 changed files with 237 additions and 34 deletions
@@ -201,6 +201,7 @@ export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = {
const title = containerTitle(c, names, groupNames[c.link]);
const box = group(c.required ? title : `${title} ${index + 1}`);
box.root.dataset.specContainer = c.id;
box.root.dataset.specIndex = String(index); // 칸을 고친 뒤 다시 그려도 몇째 상자인지(54-30)
// 보이는 값 = 고른 값 + 그림이 쓰는 값(54-13) · 위 칸이 다른 갈래를 보이면 아래 칸은 빈 채로
const view: Instance = { ...values };
const tags = new Map<string, string>();
@@ -309,6 +310,8 @@ export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = {
root.replaceChildren(...boxes);
// 개수가 밖에서 바뀌었으면 값 변경으로 알림(불러오기 때는 안 함)
if (!quiet) fitted.forEach((c) => hooks.onChange?.(c, c.items[0]));
// 상자를 새로 세움 — 고른 상자 강조를 다시 얹을 곳(B06 · 54-30 · 시험 흉내 DOM 은 없음)
root.dispatchEvent?.(new Event("specpaint", { bubbles: true }));
}
return {
+2 -9
View File
@@ -152,15 +152,8 @@ export const PIPE_SPEC: StructureSpec = {
required: false,
prefix: "soil_add",
link: "gsoiladd",
items: [
{
id: "structure",
label: "구조",
kind: "select",
choices: [{ value: SOIL, label: "흙막이" }],
},
...guardItems(),
],
// 사면안정 흙막이(표 09 · 54-26)와 같은 칸 — 형태 6 · 길이 · 높이 · 전 · 후 · 구조 고르기 없음(54-30)
items: guardItems(),
},
{
id: "chute_add",
@@ -122,3 +122,40 @@ export function guardHeightHint(diameterMm: string): string {
const d = Number(diameterMm || "1000") / 1000;
return `비우면 관경 기준 ${pipeWallMinHeightM(d)}`;
}
/** 관 보호공 [연동] · [경사](54-30) — null = 그 쪽을 쓸 수 없음(벽이 서는 옆 측점 없음 · 구간 없음). */
export interface PipeRevetChoice {
linked: boolean | null;
grade: boolean | null;
}
/** 읽고 쓰는 곳 — 횡단 보기의 연동 제어(`revetLink` · 옛 저장 자리)가 걸어 둠. */
interface RevetChoiceBridge {
read: (owner: CrossSection, sections: readonly CrossSection[]) => PipeRevetChoice;
write: (
owner: CrossSection,
sections: readonly CrossSection[],
kind: "link" | "grade",
on: boolean,
) => void;
}
let revetChoice: RevetChoiceBridge | null = null;
export function setPipeRevetChoiceBridge(bridge: RevetChoiceBridge): void {
revetChoice = bridge;
}
/** 그 관의 [연동] · [경사] 지금 값 — 횡단 자료가 없으면 null(모름). */
export function pipeRevetChoice(chainageM: number | null): PipeRevetChoice | null {
const found = ownerSection(chainageM);
return found && revetChoice ? revetChoice.read(found.owner, found.sections) : null;
}
/** 좌측 [연동] · [경사] → 횡단 연동 제어(종단 보기는 횡단 보기를 숨긴 채 세워 씀 · 54-27 과 같은 길). */
export async function setPipeRevetChoice(
chainageM: number | null,
kind: "link" | "grade",
on: boolean,
): Promise<void> {
if (!revetChoice) await crossOpener?.();
const found = ownerSection(chainageM);
if (found) revetChoice?.write(found.owner, found.sections, kind, on);
}
+5
View File
@@ -78,8 +78,10 @@ import {
setPipeExtraCountSink,
setPipeExtraCountSource,
setPipeInletSource,
setPipeRevetChoiceBridge,
setPipeSectionsSource,
} from "./design_structure_pipe_calc";
import { pipeRevetChoiceBridge } from "../../B06_Section/B06_Section_UI_Page_Link_Session";
import {
bindDesignView,
bindPipeOptions,
@@ -313,6 +315,8 @@ export async function mountCrossView(
});
// 폼이 보이는 유입 그림 값(54-13) — 세션 형식 우선
setPipeInletSource((section) => stationControls.inletStructure.valueFor(section));
// 좌측 관 보호공 [연동] · [경사] = 옛 연동 제어 그대로(54-30)
setPipeRevetChoiceBridge(pipeRevetChoiceBridge(stationControls.revetLink));
const sectionView = createSectionView(
(chainageM, change) => {
@@ -372,6 +376,7 @@ export async function mountCrossView(
partKey: key,
from: "b06",
}),
(chainageM) => sectionView.refreshCard(chainageM),
);
// 횡단도(카드) 자체를 골라도 그 측점 구조물 정보를 폼에 올린다(2026-08-29 사용자).
// 연동으로 옆에서 넘어온 카드는 **소유 측점** 시설을 보여 준다.
@@ -47,6 +47,7 @@ import {
WING_IN_KEYS,
WING_OUT_KEYS,
} from "./B05_Profile_UI_Drainage_Facility_Fields";
import { revetChoiceRow } from "./B05_Profile_UI_Drainage_Revet_Choice";
import { createSpecForm, type SpecLinks } from "../A00_Common/design/design_structure_form";
import { branchOf } from "../A00_Common/design/design_structure_spec";
import { PIPE_SPEC } from "../A00_Common/design/design_structure_pipe";
@@ -321,8 +322,22 @@ export function createFacilityOptionsForm(
// 지형이 못 세운 단은 횡단 기하가 자름 — 끝난 뒤 세운 만큼으로 다시 그림
void setPipeExtraCount(chainage, count).then(() => pipeForm.recount());
});
const inletChoice = revetChoiceRow(
() => chainage,
() => pipeForm.refresh(),
);
const outletChoice = revetChoiceRow(
() => chainage,
() => pipeForm.refresh(),
);
const pipeForm = createSpecForm(specLinks, {
fields: (c, values) => (c.id === "outlet" ? [paintTiers(values)] : []),
// 관 보호공 [연동] · [경사](54-30) — 유입 · 유출 상자 각각 한 줄(같은 값) · 집수정이면 숨김
fields: (c, values) =>
c.id === "outlet"
? [paintTiers(values), outletChoice.paint(false)]
: c.id === "inlet"
? [inletChoice.paint(values.structure === "집수정")]
: [],
shown: (c, item, values, index) => {
if (c.id === "inlet" && item.id === "structure" && (!values.structure || inletIsAuto())) {
const drawn = drawnNow();
@@ -331,8 +346,6 @@ export function createFacilityOptionsForm(
: { value: "", tag: "자동(지형 규칙)" };
}
if (values[item.id]) return null;
// 흙막이 상자 구조 = 표 01 흙막이 하나(54-27)
if (c.id === "soil_add" && item.id === "structure") return { value: "기슭막이", tag: "기본" };
if (item.id !== "form") return byDefault(branchOf(item, values)?.key);
const basin = c.id === "inlet" && values.structure === "집수정";
const drawn = drawnNow();
@@ -0,0 +1,55 @@
/* =============================================================================
* B05_Profile_UI_Drainage_Revet_Choice.ts
* 관 보호공 상자 안 [연동] · [경사] 한 줄(54-30) — 반 폭 단추 둘 · 쓸 수 있는 쪽만 누를 수 있음.
* 값 = 횡단 연동 제어 옛 자리(`revet_link_detached` · `revet_follow_grade` · `design_structure_pipe_calc`) ·
* 고르면 횡단 그림이 바로 따라감. 경사를 끄면(수평) 연동은 풀 수 없음(옛 규칙 그대로).
* ========================================================================== */
import { toggleButton } from "./B05_Profile_UI_Panel_Fields";
import {
pipeRevetChoice,
setPipeRevetChoice,
} from "../A00_Common/design/design_structure_pipe_calc";
const LINK_TITLE = "옆 측점 흙막이와 횡단 자리를 함께 씀 — 끄면 측점마다 따로 잡음";
const LINK_LOCKED = "경사를 끈(수평) 동안은 연동을 풀 수 없음";
const GRADE_TITLE = "종단경사 반영 — 끄면 연장 구간 내내 같은 높이(수평)";
/** 한 줄 — `paint` 가 지금 값 · 쓸 수 있는지를 다시 칠하고 줄을 돌려줌(폼 다시 그림마다). */
export function revetChoiceRow(
chainageOf: () => number | null,
/** 쓴 뒤 — 폼을 다시 그려 유입 · 유출 두 줄을 함께 칠함. */
written: () => void,
): {
paint: (hidden: boolean) => HTMLElement;
} {
const row = document.createElement("div"); // label 이 아님 — 이름표를 눌러 첫 단추가 눌리지 않게
row.className = "b05-route__field";
const caption = document.createElement("span");
caption.textContent = "옵션";
const pair = document.createElement("div");
pair.className = "b05-structure__half-pair";
const write = (kind: "link" | "grade", on: boolean): void =>
void setPipeRevetChoice(chainageOf(), kind, on).then(written);
const link = toggleButton("연동", true, (on) => write("link", on));
const grade = toggleButton("경사", true, (on) => write("grade", on));
grade.title = GRADE_TITLE;
pair.append(link, grade);
row.append(caption, pair);
const set = (button: HTMLButtonElement, value: boolean | null, locked: boolean): void => {
button.disabled = value === null || locked;
button.classList.toggle("is-active", value === true);
button.setAttribute("aria-pressed", String(value === true));
};
function paint(hidden: boolean): HTMLElement {
const now = pipeRevetChoice(chainageOf());
const locked = now?.grade === false;
set(link, now?.linked ?? null, locked);
link.title = locked ? LINK_LOCKED : LINK_TITLE;
set(grade, now?.grade ?? null, false);
row.hidden = hidden;
return row;
}
return { paint };
}
@@ -547,3 +547,10 @@
.b05-structure__spec-add {
align-self: flex-start;
}
/* 반 폭 단추 둘 한 줄 — 관 보호공 [연동] · [경사](54-30). */
.b05-structure__half-pair {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px;
}
+5 -5
View File
@@ -148,7 +148,7 @@ function moveRows(panelRoot: HTMLElement, selector: string, target: HTMLElement)
* 조정창 사본의 단 수·옵션 행을 고른 벽 쪽(유입/유출) 그룹으로 옮긴다. 행 DOM을
* 그대로 옮기므로 배선·값 동기는 손대지 않는다. 자리가 없으면 사본에 그대로 둔다.
*/
function relocateSideRows(panelRoot: HTMLElement): void {
function relocateSideRows(panelRoot: HTMLElement, scroll = true): void {
clearSideSlots();
const side = panelRoot.dataset.side;
if (!sideSlots || (side !== "inlet" && side !== "outlet" && side !== "extra")) return;
@@ -173,7 +173,7 @@ function relocateSideRows(panelRoot: HTMLElement): void {
moveRows(panelRoot, PIPE_OUTLET_ROWS, ownerSlot);
setGroupLabel(soil, "");
soil.classList.add("is-active");
soil.scrollIntoView({ block: "nearest" });
if (scroll) soil.scrollIntoView({ block: "nearest" });
return;
}
const owner = ownerSlot.closest("fieldset");
@@ -197,7 +197,7 @@ function relocateSideRows(panelRoot: HTMLElement): void {
const pipe = target === sideSlots.inlet || target === sideSlots.outlet;
setGroupLabel(group, pipe ? "" : (panelRoot.dataset.panelTitle ?? ""));
// 고른 칸이 화면 밖일 수 있다 — 그 자리로 스크롤한다(지시 2 · 유입 · 유출도 53-1).
group.scrollIntoView({ block: "nearest" });
if (scroll) group.scrollIntoView({ block: "nearest" });
}
function ensureDock(): void {
@@ -248,9 +248,9 @@ export function adoptAdjustPanel(panelRoot: HTMLElement): void {
* 칸(배관 유입구·유출구 / 독립 좌·우)이 아직 이전 시설 것이다(2026-08-30 사용자:
* 다른 기슭막이를 고른 뒤 배관 유출을 고르면 강조가 없다).
*/
export function refreshAdjustSlots(): void {
export function refreshAdjustSlots(scroll = true): void {
const panel = slot?.firstElementChild as HTMLElement | null;
if (panel) relocateSideRows(panel);
if (panel) relocateSideRows(panel, scroll);
}
/** 이 사본이 슬롯에 서 있으면 내린다 — 다른 사본이 이미 대신 서 있으면 손대지 않는다. */
@@ -9,6 +9,15 @@
* 검증 → 담기 / 쓰기 → 통째 직렬화)이라 제어기마다 짝을 두던 것을 하나로 모았다.
* ========================================================================== */
import type { CrossSection } from "./B06_Section_Api_Fetch";
import {
culvertOwnerFor,
culvertReach,
culvertWallsStandAt,
type RevetLinkControl,
} from "./B06_Section_UI_Cross_Culvert_Wire";
import type { PipeRevetChoice } from "../A00_Common/design/design_structure_pipe_calc";
/** 세션에 담기는 측점별 조작값 한 겹 맵. */
export interface SessionMap<V> {
values: Map<string, V>;
@@ -101,3 +110,31 @@ export function createLinkFlagSession(sessionKey: () => string | null): LinkFlag
},
};
}
/**
* 좌측 관 보호공 [연동] · [경사](54-30) — 옛 연동 제어 그대로 읽고 씀. 연동 = 벽이 서는 옆 측점이
* 있을 때만 · 켬 = 그 측점이 모두 연동 · 끄면 모두 따로. 경사 = 보호공 구간이 있을 때만.
* ponytail: 연동을 풀 때 카드 단추가 하던 높이 굳히기는 안 함 — 필요하면 카드 높이 셈을 여기로.
*/
export function pipeRevetChoiceBridge(revetLink: RevetLinkControl) {
const linkCards = (owner: CrossSection, sections: readonly CrossSection[]) =>
sections.filter((s) => culvertOwnerFor(s, sections) === owner && culvertWallsStandAt(owner, s));
return {
read: (owner: CrossSection, sections: readonly CrossSection[]): PipeRevetChoice => {
const cards = linkCards(owner, sections);
return {
linked: cards.length ? cards.every((s) => revetLink.linkedFor(s)) : null,
grade: culvertReach(owner) ? revetLink.followGradeFor(owner) : null,
};
},
write: (
owner: CrossSection,
sections: readonly CrossSection[],
kind: "link" | "grade",
on: boolean,
): void => {
if (kind === "grade") revetLink.setFollowGrade(owner, on);
else linkCards(owner, sections).forEach((s) => revetLink.setLinked(s.chainage_m, on));
},
};
}
@@ -55,6 +55,7 @@ import {
} from "./B06_Section_UI_Adjust_Dock";
import type { SectionDetailResponse } from "./B06_Section_Api_Fetch";
import type { StationControls } from "./B06_Section_UI_Page_Station_Controls";
import type { RevetKey } from "./B06_Section_UI_Cross_Culvert";
import { withSpecDefaults } from "./B06_Section_UI_Page_Structures_Spec";
const PIPE_ADD_GUIDE = "계곡 통과 시설을 넣고 빼려면 프로젝트를 먼저 여세요.";
@@ -317,6 +318,8 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
resetAdjustDock();
section.root.querySelector(".b05-route__panel-body")?.append(adjustDockRoot());
// 단 수·옵션 행은 유입구·유출구 그룹으로 옮겨 붙는다(2026-08-29 지시 4).
// 상자를 새로 세우면(단 수 · 유입 형식 · 벽 조작 · 저장 · 넘김) 고른 상자 강조를 다시 얹음(54-30)
section.facility.root.addEventListener("specpaint", () => refreshAdjustSlots(false));
setAdjustSideSlots({
inlet: section.facility.inletSlot,
outlet: section.facility.outletSlot,
@@ -496,8 +499,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
facility: {
setInletStructure: (value) => section.facility.setInletStructure(value),
onInletStructureChange: (handler) => section.facility.onInletStructureChange(handler),
// 다시 그린 상자에 강조를 다시 얹음(흙막이 상자 · 54-29)
recount: () => (section.facility.recount(), refreshAdjustSlots()),
recount: () => section.facility.recount(),
},
overrideOptions: (chainageM, patch) => {
const hit = pipeFacilities.find(
@@ -567,6 +569,8 @@ export function wireStructureSelection(
/** 고른 부재를 세션에 남긴다(2026-09-04). 해제(null)는 부르지 않는다 — 조정창 닫기가
* 카드(측점) 선택까지 지우면 안 된다. 측점 해제는 카드 선택 리스너가 따로 적는다. */
remember?: (chainageM: number, key: string) => void,
/** 폼에서 고른 벽을 카드 조정창에도 세움(54-30 · 목록 넘김 `applyStructurePick` 과 같은 차례). */
refreshCard?: (chainageM: number) => void,
): { syncInletStructure: () => void } {
const ownerChainageOf = (chainageM: number): number => {
const sectionAt = detail()?.cross_sections.find(
@@ -588,9 +592,7 @@ export function wireStructureSelection(
const syncInletStructure = (): void => {
const chainageM = panel.currentChainage();
const target = chainageM === null ? undefined : sectionAt(chainageM);
if (!target) return;
panel.facility.setInletStructure(stationControls.inletStructure.valueFor(target));
refreshAdjustSlots(); // 폼을 다시 그림 — 고른 상자 강조를 다시 얹음(54-29)
if (target) panel.facility.setInletStructure(stationControls.inletStructure.valueFor(target));
};
const origRevet = stationControls.revetOffset.select;
@@ -611,6 +613,26 @@ export function wireStructureSelection(
refreshAdjustSlots();
};
// 관 상자 칸을 고치거나 고르면 그 부분을 고름 — 횡단에서 누른 것과 같은 길(54-30)
panel.root.addEventListener("change", (event) => {
// 단 수 칸은 고른 단을 지킴(단 수 바꿈 뒤 강조 유지)
if ((event.target as HTMLElement).closest('[data-spec-item="outlet.tiers"]')) return;
const box = (event.target as HTMLElement).closest<HTMLElement>("fieldset[data-spec-container]");
const chainageM = panel.currentChainage();
const target = chainageM === null ? undefined : sectionAt(chainageM);
if (!box || !target?.culvert) return;
const id = box.dataset.specContainer;
const key: RevetKey | null =
id === "inlet" || id === "outlet"
? id
: id === "soil_add"
? `extra${Number(box.dataset.specIndex)}`
: null;
if (!key || stationControls.revetOffset.highlightFor(target) === key) return;
stationControls.revetOffset.select(target.chainage_m, key);
refreshCard?.(target.chainage_m);
});
const origFord = stationControls.ford.select;
stationControls.ford.select = (chainageM, role) => {
const nested = wallSelecting;
@@ -3331,8 +3331,6 @@
"outlet.before": "기준측점 전 (m)",
"outlet.after": "기준측점 후 (m)",
"soil_add": "흙막이 추가",
"soil_add.structure": "구조",
"soil_add.structure=기슭막이": "흙막이",
"soil_add.form": "형태",
"soil_add.form=돌쌓기(찰)": "돌 쌓기(찰)",
"soil_add.form=돌쌓기(메)": "돌 쌓기(메)",
@@ -127,6 +127,10 @@ const PIPE_CALC = {
const STUBS = {
"@config/config_frontend": { FORD_MANNING_N: 0.03, FORD_SLOPE: 0.02 },
"../A00_Common/design/design_structure_pipe_calc": PIPE_CALC,
// 관 보호공 [연동] · [경사] 줄(54-30) — 여기서는 자리만
"./B05_Profile_UI_Drainage_Revet_Choice": {
revetChoiceRow: () => ({ paint: () => new FakeNode("div") }),
},
"../../B05_Profile/B05_Profile_UI_Structures_Summary_Row": {
buildSummaryRow: () => {
const root = new FakeNode("div");
@@ -174,7 +178,10 @@ const tierInput = () => tierRow()?.querySelector("input");
const snap = () => ({
value: tierInput()?.value ?? null,
boxes: boxesOf("soil_add").length,
structures: boxesOf("soil_add").map((b) => b.querySelector("select")?.children[0]?.textContent),
// 상자 첫 칸 이름 — 구조 고르기 없이 형태부터(54-30 · 사면안정 흙막이와 같은 칸)
structures: boxesOf("soil_add").map(
(b) => b.querySelectorAll("label")[0]?.children[0]?.textContent,
),
});
async function main() {
@@ -66,9 +66,10 @@ def test_옛_키로_읽고_같은_키로_씀_옛_칸은_안_씀(out):
def test_선택_컨테이너는_인스턴스마다_차례대로_다시_매김(out):
assert [i["structure"] for i in out["state"]["soil_add"]] == ["기슭막이", ""]
# 흙막이 상자 = 구조 고르기 없음(54-30) — 옛 구조 키만 든 상자도 자리는 지킴
assert [i["form"] for i in out["state"]["soil_add"]] == ["바자", ""]
assert out["back"]["soil_add_1_form"] == "바자"
assert "soil_add_2_form" not in out["back"]
assert out["back"]["soil_add_2_form"] == "" # 빈 상자 자리 표식 = 첫 칸(형태) 빈 글
assert out["back"]["chute_add_1_structure"] == "도수로"
+2 -2
View File
@@ -40,7 +40,7 @@ def test_배관_컨테이너마다_첫_값_한_줄(out):
"관: 관경 1200",
"관 보호공(유입): 구조 집수정",
"관 보호공(유출): 구조 흙막이",
"흙막이 1: 구조 흙막이",
"흙막이 1: 형태 돌망태", # 구조 칸 없음(54-30)
"도수로/돌붙임 1: 구조 도수로",
]
@@ -56,7 +56,7 @@ def test_빈_값은_등록부_기본값에_기본_표시(out):
def test_흙막이_상자_수는_밖에서_정한_단_수(out):
assert out["pipeCount"][-2:] == ["흙막이 1: 구조 흙막이", "흙막이 2: 구조 —"]
assert out["pipeCount"][-2:] == ["흙막이 1: 형태 —", "흙막이 2: 형태 —"]
assert len(out["pipeCount"]) == 2 + 3 + 2
@@ -52,8 +52,9 @@ def test_횡단에서_바꾸면_좌측_칸이_따라감(out) -> None:
assert (out["fromCross"]["value"], out["fromCross"]["boxes"]) == ("2", 2)
def test_늘어난_흙막이_상자_구조는_흙막이_기본(out) -> None:
assert out["set2"]["structures"] == ["흙막이 (기본)", "흙막이 (기본)"]
def test_늘어난_흙막이_상자는_구조_칸_없이_형태부터(out) -> None:
"""54-30 — 사면안정 흙막이와 같은 칸(형태 · 길이 · 높이 · 전 · 후) · 구조 고르기 없음."""
assert out["set2"]["structures"] == ["형태", "형태"]
def test_흙막이_아닌_구조면_칸을_가림(out) -> None:
+27 -3
View File
@@ -40,7 +40,31 @@ def test_배관_유입_단_수_줄은_선_단이_있을_때만() -> None:
def test_폼을_다시_그리면_강조_다시() -> None:
"""54-30 — 상자를 새로 세울 때마다(단 수 · 유입 형식 · 벽 조작 · 저장 · 넘김) 한 곳에서 강조를 다시 얹음."""
form = _src("A00_Common/design/design_structure_form.ts")
assert 'root.dispatchEvent?.(new Event("specpaint", { bubbles: true }));' in form
page = _src("B06_Section/B06_Section_UI_Page_Structures_Panel.ts")
assert "recount: () => (section.facility.recount(), refreshAdjustSlots())" in page
sync = page[page.index("const syncInletStructure") :]
assert sync.index("refreshAdjustSlots()") < sync.index("};")
assert 'addEventListener("specpaint", () => refreshAdjustSlots(false))' in page
def test_상자_칸을_고치면_그_부분을_고름() -> None:
"""54-30 — 칸 고침 = 횡단에서 그 벽을 누른 것과 같은 길 · 단 수 칸은 고른 단을 지킴."""
page = _src("B06_Section/B06_Section_UI_Page_Structures_Panel.ts")
assert '[data-spec-item="outlet.tiers"]' in page
assert "`extra${Number(box.dataset.specIndex)}`" in page
assert "refreshCard?.(target.chainage_m);" in page
def test_흙막이_상자는_사면안정_흙막이와_같은_칸() -> None:
pipe = _src("A00_Common/design/design_structure_pipe.ts")
soil = pipe[pipe.index('id: "soil_add"') :]
assert soil[: soil.index("},")].count("items: guardItems(),") == 1
def test_관_보호공_연동_경사_단추() -> None:
row = _src("B05_Profile/B05_Profile_UI_Drainage_Revet_Choice.ts")
assert 'toggleButton("연동"' in row and 'toggleButton("경사"' in row
assert "b05-structure__half-pair" in row
session = _src("B06_Section/B06_Section_UI_Page_Link_Session.ts")
assert "revetLink.setFollowGrade(owner, on)" in session
assert "revetLink.setLinked(s.chainage_m, on)" in session