From f93cc56284c2642a93e9196108295be3ca0bb587 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 6 Oct 2026 13:47:56 +0900 Subject: [PATCH] =?UTF-8?q?fix(B05=C2=B7B06):=2054-30=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20=ED=9D=99=EB=A7=89=EC=9D=B4=20=EA=B0=95=EC=A1=B0=20=EC=9C=A0?= =?UTF-8?q?=EC=A7=80=20=C2=B7=20=EA=B4=80=20=EB=B3=B4=ED=98=B8=EA=B3=B5=20?= =?UTF-8?q?[=EC=97=B0=EB=8F=99]=20[=EA=B2=BD=EC=82=AC]=20=C2=B7=20?= =?UTF-8?q?=EC=B9=B8=20=EA=B3=A0=EC=B9=A8=20=EA=B0=95=EC=A1=B0=20=C2=B7=20?= =?UTF-8?q?=ED=9D=99=EB=A7=89=EC=9D=B4=20=EC=B9=B8=20=EC=9D=BC=EC=9B=90?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 폼이 상자를 새로 세울 때마다(단 수 · 유입 형식 · 벽 조작 · 저장 · 넘김) 고른 상자 강조 다시(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 Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ --- A00_Common/design/design_structure_form.ts | 3 + A00_Common/design/design_structure_pipe.ts | 11 +--- .../design/design_structure_pipe_calc.ts | 37 +++++++++++++ A00_Common/design/design_view_cross.ts | 5 ++ .../B05_Profile_UI_Drainage_Facility.ts | 19 ++++++- .../B05_Profile_UI_Drainage_Revet_Choice.ts | 55 +++++++++++++++++++ .../B05_Profile_UI_Style_Structures.css | 7 +++ B06_Section/B06_Section_UI_Adjust_Dock.ts | 10 ++-- .../B06_Section_UI_Page_Link_Session.ts | 37 +++++++++++++ .../B06_Section_UI_Page_Structures_Panel.ts | 32 +++++++++-- resources/master_template/구조물정의.json | 2 - .../tester/helper_54_27_soil_tier_field.cjs | 9 ++- .../tester/test_52_structure_spec_pipe.py | 5 +- resources/tester/test_54_14_balloon_lines.py | 4 +- .../tester/test_54_27_soil_tier_field.py | 5 +- .../tester/test_54_29_pick_same_panel.py | 30 +++++++++- 16 files changed, 237 insertions(+), 34 deletions(-) create mode 100644 B05_Profile/B05_Profile_UI_Drainage_Revet_Choice.ts diff --git a/A00_Common/design/design_structure_form.ts b/A00_Common/design/design_structure_form.ts index b9656a297..764d17b64 100644 --- a/A00_Common/design/design_structure_form.ts +++ b/A00_Common/design/design_structure_form.ts @@ -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(); @@ -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 { diff --git a/A00_Common/design/design_structure_pipe.ts b/A00_Common/design/design_structure_pipe.ts index 37061d3e9..1915eda99 100644 --- a/A00_Common/design/design_structure_pipe.ts +++ b/A00_Common/design/design_structure_pipe.ts @@ -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", diff --git a/A00_Common/design/design_structure_pipe_calc.ts b/A00_Common/design/design_structure_pipe_calc.ts index ddbd00eb4..a7caceac2 100644 --- a/A00_Common/design/design_structure_pipe_calc.ts +++ b/A00_Common/design/design_structure_pipe_calc.ts @@ -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 { + if (!revetChoice) await crossOpener?.(); + const found = ownerSection(chainageM); + if (found) revetChoice?.write(found.owner, found.sections, kind, on); +} diff --git a/A00_Common/design/design_view_cross.ts b/A00_Common/design/design_view_cross.ts index b260c25b4..8a0fda6d9 100644 --- a/A00_Common/design/design_view_cross.ts +++ b/A00_Common/design/design_view_cross.ts @@ -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 사용자). // 연동으로 옆에서 넘어온 카드는 **소유 측점** 시설을 보여 준다. diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts index 45a65767a..601174bac 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts @@ -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(); diff --git a/B05_Profile/B05_Profile_UI_Drainage_Revet_Choice.ts b/B05_Profile/B05_Profile_UI_Drainage_Revet_Choice.ts new file mode 100644 index 000000000..58effae3d --- /dev/null +++ b/B05_Profile/B05_Profile_UI_Drainage_Revet_Choice.ts @@ -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 }; +} diff --git a/B05_Profile/B05_Profile_UI_Style_Structures.css b/B05_Profile/B05_Profile_UI_Style_Structures.css index 78c34e1c3..c51e9da27 100644 --- a/B05_Profile/B05_Profile_UI_Style_Structures.css +++ b/B05_Profile/B05_Profile_UI_Style_Structures.css @@ -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; +} diff --git a/B06_Section/B06_Section_UI_Adjust_Dock.ts b/B06_Section/B06_Section_UI_Adjust_Dock.ts index 91c2b61df..9c1ef254b 100644 --- a/B06_Section/B06_Section_UI_Adjust_Dock.ts +++ b/B06_Section/B06_Section_UI_Adjust_Dock.ts @@ -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); } /** 이 사본이 슬롯에 서 있으면 내린다 — 다른 사본이 이미 대신 서 있으면 손대지 않는다. */ diff --git a/B06_Section/B06_Section_UI_Page_Link_Session.ts b/B06_Section/B06_Section_UI_Page_Link_Session.ts index a014cc93d..c12715271 100644 --- a/B06_Section/B06_Section_UI_Page_Link_Session.ts +++ b/B06_Section/B06_Section_UI_Page_Link_Session.ts @@ -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 { values: Map; @@ -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)); + }, + }; +} diff --git a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts index db4d5a2c8..dfc668894 100644 --- a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts +++ b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts @@ -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("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; diff --git a/resources/master_template/구조물정의.json b/resources/master_template/구조물정의.json index f62eaddb2..8a304220d 100644 --- a/resources/master_template/구조물정의.json +++ b/resources/master_template/구조물정의.json @@ -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=돌쌓기(메)": "돌 쌓기(메)", diff --git a/resources/tester/helper_54_27_soil_tier_field.cjs b/resources/tester/helper_54_27_soil_tier_field.cjs index d32dc24b0..2f76d7a9b 100644 --- a/resources/tester/helper_54_27_soil_tier_field.cjs +++ b/resources/tester/helper_54_27_soil_tier_field.cjs @@ -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() { diff --git a/resources/tester/test_52_structure_spec_pipe.py b/resources/tester/test_52_structure_spec_pipe.py index 229b448b2..88277e3d1 100644 --- a/resources/tester/test_52_structure_spec_pipe.py +++ b/resources/tester/test_52_structure_spec_pipe.py @@ -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"] == "도수로" diff --git a/resources/tester/test_54_14_balloon_lines.py b/resources/tester/test_54_14_balloon_lines.py index 6374f7903..aa9b92539 100644 --- a/resources/tester/test_54_14_balloon_lines.py +++ b/resources/tester/test_54_14_balloon_lines.py @@ -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 diff --git a/resources/tester/test_54_27_soil_tier_field.py b/resources/tester/test_54_27_soil_tier_field.py index 6ee475570..4c55e96a6 100644 --- a/resources/tester/test_54_27_soil_tier_field.py +++ b/resources/tester/test_54_27_soil_tier_field.py @@ -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: diff --git a/resources/tester/test_54_29_pick_same_panel.py b/resources/tester/test_54_29_pick_same_panel.py index 5d6cd6073..2849d2862 100644 --- a/resources/tester/test_54_29_pick_same_panel.py +++ b/resources/tester/test_54_29_pick_same_panel.py @@ -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