Merge remote-tracking branch 'origin/sub_laptop_5' into main_laptop_1

This commit is contained in:
2026-10-06 10:33:46 +09:00
20 changed files with 279 additions and 81 deletions
+30 -9
View File
@@ -55,6 +55,9 @@ export interface SpecFormHooks {
/** 숫자 칸 빈 칸 안내. */
hint?: (c: SpecContainer, item: SpecItem, state: SpecState) => string;
onChange?: (c: SpecContainer, item: SpecItem) => void;
/** 선택 컨테이너 개수를 밖에서 정함(54-1 배관 흙막이 = 추가 기슭막이(단)) — 수면 그만큼 늘 보임 ·
* [추가] · [빼기] 없음 · 줄면 끝에서부터 빠짐 · null = 모름(저장된 것 그대로) · undefined = 손 [추가]. */
count?: (c: SpecContainer) => number | null | undefined;
}
export interface SpecForm {
@@ -75,6 +78,8 @@ export interface SpecForm {
/** 잇기 값이 바뀌었을 때 줄을 다시 그림. */
paintLinks: () => void;
refresh: () => void;
/** 밖에서 정한 개수(`hooks.count`)가 바뀌었을 때 — 맞춰 그리고 바뀌었으면 change 알림. */
recount: () => void;
}
export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = {}): SpecForm {
@@ -122,11 +127,12 @@ export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = {
}
const input = numberInput(String(item.step ?? 0.1), "0", hooks.hint?.(c, item, state) ?? "");
if (item.kind === "auto") {
input.type = "text";
input.readOnly = true;
input.value = hooks.auto?.(c, item, state) || "—";
input.title = "자동 계산 — 고치지 않음";
return input;
// 보여 주는 칸(54-1) — 손 입력 칸 아님
const shown = document.createElement("output");
shown.className = "b05-structure__auto";
shown.textContent = hooks.auto?.(c, item, state) || "—";
shown.title = "자동 계산";
return shown;
}
input.value = values[item.id] ?? "";
return stepper(input, item.step ?? 0.1);
@@ -198,7 +204,7 @@ export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = {
rows.push({ key, set: row.set });
box.body.append(row.root);
}
if (!c.required) {
if (!c.required && hooks.count?.(c) === undefined) {
const remove = document.createElement("button");
remove.type = "button";
remove.className = "b05-route__irregular-btn";
@@ -222,13 +228,25 @@ export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = {
return box.root;
};
function paint(): void {
/** 밖에서 정한 개수로 맞춤 — 줄면 끝 인스턴스 · 잇기를 뺌. 바뀌었으면 true. */
const fit = (c: SpecContainer, list: Instance[]): boolean => {
const n = hooks.count?.(c);
if (c.required || n === undefined || n === null || n === list.length) return false;
for (let k = n + 1; k <= list.length; k++) links?.set(linkOf(c, k), null);
list.splice(n);
while (list.length < n) list.push({});
return true;
};
function paint(quiet = true): void {
rows.length = 0;
const boxes: HTMLElement[] = [];
const fitted: SpecContainer[] = [];
for (const c of spec?.containers ?? []) {
const list = (state[c.id] ??= c.required ? [{}] : []);
if (fit(c, list)) fitted.push(c);
list.forEach((values, index) => boxes.push(instanceBox(c, values, index)));
if (c.required || (c.max && list.length >= c.max)) continue;
if (c.required || (c.max && list.length >= c.max) || hooks.count?.(c) !== undefined) continue;
const add = document.createElement("button");
add.type = "button";
add.className = "b05-route__irregular-btn b05-structure__spec-add";
@@ -243,6 +261,8 @@ export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = {
boxes.push(add);
}
root.replaceChildren(...boxes);
// 개수가 밖에서 바뀌었으면 값 변경으로 알림(불러오기 때는 안 함)
if (!quiet) fitted.forEach((c) => hooks.onChange?.(c, c.items[0]));
}
return {
@@ -268,6 +288,7 @@ export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = {
const now = links?.get() ?? {};
rows.forEach((row) => row.set(now[row.key] ?? null));
},
refresh: paint,
refresh: () => paint(),
recount: () => paint(false),
};
}
+12 -14
View File
@@ -2,8 +2,9 @@
* design_structure_pipe.ts
* 횡단배수 › 배관 명세 — 사용자 구조물 표 01(`docs/raw/구조물정의_표/01_횡단배수_배관.md`) 그대로.
*
* 컨테이너 다섯: 관(필수) · 관 보호공(유입)(필수) · 관 보호공(유출)(필수) · 흙막이 추가(선택 · 여러 개) ·
* 도수로/돌붙임(선택 · 여러 개). 구조 · 정본 = 이 명세 · 이름만 정의(M02)에서 바꿈.
* 컨테이너 다섯: 관(필수) · 관 보호공(유입)(필수) · 관 보호공(유출)(필수) · 흙막이(선택 · 개수 = 유출
* 「추가 기슭막이(단)」 · 54-1) · 도수로/돌붙임(선택 · 여러 개). 집수정 = 길이 · 전 · 후 없음(54-1).
* 구조 · 정본 = 이 명세 · 이름만 정의(M02)에서 바꿈.
* 저장 키 = pipe_points.json 옛 키 그대로(B06 횡단 · B07 · B08 이 읽는 자리) — 흙막이 값은 옛 「기슭막이」
* (`inlet_type` · `*_revet_*`)에 둠. 새 키 = 집수정 높이 · 선택 컨테이너(`soil_add_{n}_*` · `chute_add_{n}_*`).
* 정함(PLAN 52): 유출 형태 = 흙막이 6가지 · 도수로/돌붙임 = 구조만 · 길이 = 전 + 후 맞물림.
@@ -40,10 +41,13 @@ function guardItems(keys?: Record<string, Record<string, string>>): SpecItem[] {
? {
item: "structure",
map: Object.fromEntries(
Object.entries(keys).map(([structure, k]) => [
structure,
{ key: k[field], choices: choices?.[structure] },
]),
// 키 없는 갈래 = 그 구조에 없는 칸(집수정 길이 · 전 · 후) — 가림
Object.entries(keys)
.filter(([, k]) => k[field])
.map(([structure, k]) => [
structure,
{ key: k[field], choices: choices?.[structure] },
]),
),
}
: undefined;
@@ -121,13 +125,7 @@ export const PIPE_SPEC: StructureSpec = {
],
},
...guardItems({
[BASIN]: {
form: "inlet_structure",
length: "inlet_basin_length_m",
height: "inlet_basin_height_m",
before: "inlet_basin_before_m",
after: "inlet_basin_after_m",
},
[BASIN]: { form: "inlet_structure", height: "inlet_basin_height_m" },
[SOIL]: revetKeys("inlet"),
}),
],
@@ -150,7 +148,7 @@ export const PIPE_SPEC: StructureSpec = {
},
{
id: "soil_add",
title: "흙막이 추가",
title: "흙막이",
required: false,
prefix: "soil_add",
link: "gsoiladd",
@@ -5,6 +5,7 @@
* 셈 · 시험 test_b06_pipe_length) 그대로 — 여기서 새로 셈하지 않음 · 화면에 보이기만(저장 안 함).
* 높이 = 손 입력 · 횡단도 조정 높이는 B06 `withSpecDefaults` 가 같은 키(`*_revet_height_m`)에 실음 ·
* 빈 칸 안내 = 관경 기준 최소 높이(`pipeWallMinHeightM` ↔ 파이썬 `pipe_wall_min_height_m` 짝).
* 흙막이 개수 = 주인 측점 유출 「추가 기슭막이(단)」(횡단 설계 patch `extra_wall_counts` · 54-1) — 읽기만.
* ========================================================================== */
import { pipeWallMinHeightM } from "../../common_util/common_util_culvert_sets";
@@ -18,9 +19,16 @@ export function setPipeSectionsSource(source: () => readonly CrossSection[] | nu
sectionsSource = source;
}
/** 관 길이(m) — 그 관의 주인 측점(관 자리에서 가장 가까운 측점) 횡단 기하 값 · 못 세우면 저장된
* `pipe_length_m` · 그것도 없으면 null. */
export function pipeLengthM(chainageM: number | null): number | null {
/** 유출 단 수를 대는 곳 — 횡단 보기의 세션 값 우선(`countFor`) · 없으면 정본 patch. */
let extraCountSource: ((section: CrossSection) => number) | null = null;
export function setPipeExtraCountSource(source: (section: CrossSection) => number): void {
extraCountSource = source;
}
/** 그 관의 주인 측점(관 자리에서 가장 가까운 측점) — 횡단 자료가 없으면 null. */
function ownerSection(
chainageM: number | null,
): { owner: CrossSection; sections: readonly CrossSection[] } | null {
const sections = sectionsSource?.() ?? null;
if (chainageM === null || !sections?.length) return null;
const holders = sections.filter(
@@ -30,11 +38,26 @@ export function pipeLengthM(chainageM: number | null): number | null {
const owner = holders.reduce((a, b) =>
Math.abs(b.chainage_m - chainageM) < Math.abs(a.chainage_m - chainageM) ? b : a,
);
return { owner, sections };
}
/** 관 길이(m) — 주인 측점 횡단 기하 값 · 못 세우면 저장된 `pipe_length_m` · 그것도 없으면 null. */
export function pipeLengthM(chainageM: number | null): number | null {
const found = ownerSection(chainageM);
if (!found) return null;
const { owner, sections } = found;
const drawn = computeStoredLayouts(owner, sections)?.culvert?.pipe?.lengthM;
const length = typeof drawn === "number" && drawn > 0 ? drawn : owner.design?.pipe_length_m;
return typeof length === "number" && length > 0 ? length : null;
}
/** 흙막이 개수 = 주인 측점 유출 「추가 기슭막이(단)」 — 횡단 자료가 없으면 null(모름). */
export function pipeExtraCount(chainageM: number | null): number | null {
const owner = ownerSection(chainageM)?.owner;
if (!owner) return null;
return extraCountSource?.(owner) ?? owner.design?.extra_wall_counts?.outlet ?? 0;
}
/** 흙막이 높이 빈 칸 안내 — 관경 기준 최소 높이(엔진이 빈 칸을 이 값으로 그림). */
export function guardHeightHint(diameterMm: string): string {
const d = Number(diameterMm || "1000") / 1000;
+4 -1
View File
@@ -73,7 +73,7 @@ import {
createStandardSlot,
} from "../../B06_Section/B06_Section_UI_Page_Panel";
import type { DesignPageHost } from "./design_page";
import { setPipeSectionsSource } from "./design_structure_pipe_calc";
import { setPipeExtraCountSource, setPipeSectionsSource } from "./design_structure_pipe_calc";
import {
bindDesignView,
bindPipeOptions,
@@ -285,7 +285,10 @@ export async function mountCrossView(
ensureSampledWidth,
projectId: () => projectId,
onSaveError: (message) => showToast(`배수관 구간값 저장 실패 — ${message}`, "error"),
onExtraCount: () => structuresPanel.facility.recount(),
});
// 배관 흙막이 개수 = 유출 단 수(54-1) — 세션 값 우선
setPipeExtraCountSource((section) => stationControls.extraWalls.countFor(section, "outlet"));
const sectionView = createSectionView(
(chainageM, change) => {
+3
View File
@@ -70,6 +70,7 @@ import {
} from "../../B05_Profile/B05_Profile_UI_Page_Actions";
import { createLatestSource, panelValuesOf } from "../../B05_Profile/B05_Profile_UI_Page_Latest";
import { stationLinesOf } from "../../B05_Profile/B05_Profile_UI_Page_Stations";
import { setPipeSectionsSource } from "./design_structure_pipe_calc";
import type { RouteProfilePanelCallbacks } from "../../B05_Profile/B05_Profile_UI_Profile_Panel_Types";
import "../../B05_Profile/B05_Profile_UI_Style.css";
// 700줄 제한으로 잘라낸 조각 — 본체 **다음에** 불러야 캐스케이드 순서가 같다(2026-09-04).
@@ -194,6 +195,8 @@ export async function mountProfileView(
let latest: RouteLatestResponse | null = null;
let roadWidths = DEFAULT_ROAD_WIDTHS;
let currentSectionDetail: SectionDetailResponse | null = null;
// 배관 관 길이 · 흙막이 개수(54-1) — 종단 보기도 같은 횡단 자료로(B05 = B06)
setPipeSectionsSource(() => currentSectionDetail?.cross_sections ?? null);
/** [예상형상] 표시 상태 — 측점 바를 계획고 위로 올릴지 판단한다(패널 기본 ON). */
let corridorVisible = true;
let routeReady = false;
@@ -55,7 +55,11 @@ import {
} from "../A00_Common/design/design_structure_box";
import { FORD_SPEC, withLegacyGuards } from "../A00_Common/design/design_structure_ford";
import { fordHeightHint, fordHeightM } from "../A00_Common/design/design_structure_ford_calc";
import { guardHeightHint, pipeLengthM } from "../A00_Common/design/design_structure_pipe_calc";
import {
guardHeightHint,
pipeExtraCount,
pipeLengthM,
} from "../A00_Common/design/design_structure_pipe_calc";
import {
hasLinks,
linkFields,
@@ -215,6 +219,8 @@ export interface FacilityOptionsForm {
paintLinks: () => void;
/** 고른 관 측점 — 관 길이(자동) 계산에 씀. */
setChainage: (chainageM: number | null) => void;
/** 횡단 「추가 기슭막이(단)」이 바뀜 — 배관 흙막이 컨테이너 수를 다시 맞춤(54-1 · B06 이 부름). */
recount: () => void;
/** 현재 필드 값을 시설 옵션으로 읽는다(빈 값 생략). 숨긴 칸은 폼이 받은 옛 값 그대로. */
readOptions: () => Record<string, string | number>;
/** 「구조물 옵션 보이기」에서 끈 레지스트리 옵션 키 — 그 칸을 가린다(값은 옛 값 보존).
@@ -265,8 +271,10 @@ export function createFacilityOptionsForm(
auto: (_c, item) => {
if (item.id !== "length") return "";
const length = pipeLengthM(chainage);
return length === null ? "횡단 설계 뒤 셈" : length.toFixed(2);
return length === null ? "횡단 설계 뒤 셈" : String(Math.round(length));
},
// 흙막이 개수 = 유출 「추가 기슭막이(단)」(54-1)
count: (c) => (c.id === "soil_add" ? pipeExtraCount(chainage) : undefined),
hint: (c, item, state) => {
const soil = c.id !== "inlet" || state.inlet?.[0]?.structure === "기슭막이";
return item.id === "height" && soil ? guardHeightHint(state.pipe?.[0]?.diameter ?? "") : "";
@@ -475,6 +483,9 @@ export function createFacilityOptionsForm(
chainage = chainageM;
if (current === "pipe") pipeForm.refresh();
},
recount() {
if (current === "pipe") pipeForm.recount();
},
onInletStructureChange(handler) {
inletStructureHandler = handler;
},
@@ -494,7 +505,11 @@ export function createFacilityOptionsForm(
// 받은 값만 칸에 — 없으면 「안 정함」(새 시설은 부르는 쪽이 등록부 기본값을 실어 줌 · 46-7).
const groups = Object.fromEntries((definition?.groups ?? []).map((g) => [g.id, g.name]));
const names = definition?.spec_names ?? {};
if (facility === "pipe") pipeForm.write(PIPE_SPEC, options, names, groups);
if (facility === "pipe") {
// 정의 파일의 옛 이름 「흙막이 추가」 — 명세 이름 「흙막이」가 앞섬(54-1)
const { soil_add: _old, ...pipeNames } = names;
pipeForm.write(PIPE_SPEC, options, pipeNames, groups);
}
if (facility === "box_culvert") {
// 본체 규격은 저장 안 함 — 폭 · 높이로 되찾음 · 옛 설치 「있음」 = 날개벽
boxForm.write(BOX_SPEC, boxOptionsIn(options), names, groups);
@@ -26,10 +26,8 @@ export const FORM_OPTION_KEYS: Readonly<Record<string, readonly string[]>> = {
"pipe_diameter_mm",
"inlet_type",
"inlet_structure",
"inlet_basin_length_m",
// 집수정 길이 · 전 · 후는 폼에서 빠짐(54-1) — 옛 값 그대로 둠
"inlet_basin_height_m",
"inlet_basin_before_m",
"inlet_basin_after_m",
...revetKeys("inlet"),
"outlet_type",
...revetKeys("outlet"),
@@ -242,7 +242,8 @@ export function paintStale(
? staleValues(values, known).filter(([key]) => !own.prefixes.some((p) => key.startsWith(p)))
: [];
box.replaceChildren();
box.hidden = !stale.length;
// 상자는 모든 종류에서 숨김(54-1) — 값은 파일에 그대로
box.hidden = true;
if (!stale.length) return;
const legend = document.createElement("legend");
legend.textContent = "지난 정의 값";
@@ -34,7 +34,7 @@ export function buildSummaryRow(): SummaryRow {
const pick = document.createElement("button");
pick.type = "button";
pick.className = "b05-route__irregular-btn";
pick.textContent = "고르기";
pick.textContent = "선택";
pick.dataset.b05Summary = "pick";
const clear = document.createElement("button");
clear.type = "button";
+6 -1
View File
@@ -154,7 +154,12 @@ function relocateSideRows(panelRoot: HTMLElement): void {
if (slotUsable(ownerSlot)) moveRows(panelRoot, SIDE_ROWS, ownerSlot);
} else {
if (!slotUsable(target)) return;
moveRows(panelRoot, SIDE_ROWS, target);
// 배관 유출 칸은 읽기 줄(관 길이 · 좌우 · 상하 안내)을 안 받음(54-1)
const rows =
target === sideSlots.outlet
? SIDE_ROWS.replace(".b06-structure-panel__struct--readout, ", "")
: SIDE_ROWS;
moveRows(panelRoot, rows, target);
}
// 고른 구조물이 어느 쪽인지 그룹 테두리로 알린다(2026-08-29 사용자 지시 3).
const group = target.closest<HTMLElement>("fieldset");
@@ -61,6 +61,8 @@ export interface StationControlDeps {
projectId: () => string | null;
/** 구간값 저장 실패 알림 — 화면 토스트는 페이지가 띄운다. */
onSaveError?: (message: string) => void;
/** 다단 단 수가 바뀜 — 좌측 배관 흙막이 컨테이너 수를 맞춤(54-1). */
onExtraCount?: () => void;
}
/** 반폭·기슭막이·유입 구조물 제어 묶음. `load`는 경로가 바뀔 때 세션 값을 다시 읽는다. */
@@ -366,6 +368,7 @@ export function createStationControls(deps: StationControlDeps): StationControls
},
});
refreshWithLinked(chainageM);
deps.onExtraCount?.();
},
syncCount: (chainageM, built, side = "outlet") => {
const key = extraKey(chainageM, side);
@@ -381,6 +384,14 @@ export function createStationControls(deps: StationControlDeps): StationControls
persistRevetShifts();
persistExtraSpans();
persistExtraCounts();
// 캐시 patch 도 세운 만큼으로 — 세션 키를 지우면 `countFor` 가 옛 patch 수로 돌아감(54-1)
patchCachedDesign(chainageM, {
extra_wall_counts: {
outlet: extraCounts.get(extraKey(chainageM, "outlet")) ?? 0,
basin: extraCounts.get(extraKey(chainageM, "basin")) ?? 0,
},
});
deps.onExtraCount?.();
},
};
@@ -139,6 +139,8 @@ export interface B06StructuresPanel {
facility: {
setInletStructure: (value: "auto" | "revet" | "I" | "L" | "U") => void;
onInletStructureChange: (handler: (value: "auto" | "revet" | "I" | "L" | "U") => void) => void;
/** 횡단 단 수가 바뀜 — 배관 흙막이 컨테이너 수를 맞춤(54-1). */
recount: () => void;
};
}
@@ -491,6 +493,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
facility: {
setInletStructure: (value) => section.facility.setInletStructure(value),
onInletStructureChange: (handler) => section.facility.onInletStructureChange(handler),
recount: () => section.facility.recount(),
},
overrideOptions: (chainageM, patch) => {
const hit = pipeFacilities.find(
+1 -1
View File
@@ -19,7 +19,7 @@ from common_util.common_util_summary_item import GROUP_JOIN
#: 종류 → 선택 컨테이너 (머리, 잇기, 제목)
SPEC_CONTAINERS: dict[str, tuple[tuple[str, str, str], ...]] = {
"pipe": (
("soil_add", "gsoiladd", "흙막이 추가"),
("soil_add", "gsoiladd", "흙막이"),
("chute_add", "gchuteadd", "도수로/돌붙임"),
),
"ford_pavement": (("ford_chute", "gfordpavechute", "도수로/돌붙임"),),
+1 -1
View File
@@ -39,7 +39,7 @@ SOIL = {
def test_컨테이너마다_한_건_잇기와_그_값():
item = {"type_id": "pipe", "options": {"pipe_diameter_mm": 1000, **SOIL}, "복사본": "pipe@40.0"}
rows = container_rows(item, {"gsoiladd1": RV, "gchuteadd1": MS})
assert [r["그룹"] for r in rows] == ["흙막이 추가 1", "흙막이 추가 2", "도수로/돌붙임 1"]
assert [r["그룹"] for r in rows] == ["흙막이 1", "흙막이 2", "도수로/돌붙임 1"]
assert [r["복사본"] for r in rows] == [
"pipe@40.0__gsoiladd1",
"pipe@40.0__gsoiladd2",
@@ -36,7 +36,7 @@ def test_컨테이너_다섯_필수_셋_선택_둘(out):
["pipe", "관", True],
["inlet", "관 보호공 (유입)", True],
["outlet", "관 보호공 (유출)", True],
["soil_add", "흙막이 추가", False],
["soil_add", "흙막이", False],
["chute_add", "도수로/돌붙임", False],
]
@@ -58,7 +58,8 @@ def test_형태_선택지는_구조를_따름(out):
def test_옛_키로_읽고_같은_키로_씀_옛_칸은_안_씀(out):
back = out["back"]
assert back["pipe_diameter_mm"] == 1000
assert back["inlet_structure"] == "L" and back["inlet_basin_length_m"] == 2
# 집수정 = 길이 · 전 · 후 칸 없음(54-1) — 폼이 안 적음(병합이 옛 값을 지킴)
assert back["inlet_structure"] == "L" and "inlet_basin_length_m" not in back
assert back["outlet_revet_height_m"] == 2.5
# 표 01 에 없는 옛 칸은 폼이 안 적음(병합이 이어 붙임 = 지난 정의 값)
assert "pipe_kind" not in back and "wing_wall_type" not in back
@@ -94,7 +94,7 @@ def merged(tmp_path_factory) -> dict:
def test_폼이_모르는_칸은_남고_폼이_아는_칸은_폼대로(merged) -> None:
options = merged["out"][0]["options"]
assert options["revet_foundation"] == "기초유" # 집계표 값이 살아남음
assert "inlet_basin_before_m" not in options # 폼 칸(24-4a) — 집수정을 걷으면 함께 지움
assert options["inlet_basin_before_m"] == 1.5 # 집수정 전 — 폼에서 빠짐(54-1) · 옛 값 남음
assert options["pipe_diameter_mm"] == 800 # 폼 값이 이김
# 표 01 명세(PLAN 52-1)가 뺀 옛 칸 = 폼이 모르는 칸 → 남음(지난 정의 값)
assert options["inlet_basin_form"] == "□형(기본형)"
@@ -80,7 +80,7 @@ def test_집계_항목_줄(out):
True,
None,
)
assert (empty["label"], empty["pick"], empty["clear"]) == ("집계 항목", "고르기", "해제")
assert (empty["label"], empty["pick"], empty["clear"]) == ("집계 항목", "선택", "해제")
row = out["rowSet"]
assert row["assigned"] is True and row["clearDisabled"] is False
assert row["name"] == "돌기슭막이 메쌓기 1.2m" and A_KEY in row["title"]
+12 -14
View File
@@ -2,13 +2,13 @@
* 시작 화면: B05 종단설계(#/b05-profile) · 검증 프로젝트 · 목록에 「배관 1」 「세월교 1」 · 로그인 상태
* 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m5213 = "단계";` 를 붙여
* cache → 저장 안 하고 B05 → B06 → B05 오가기(캐시 값 그대로) · 종류 바꾸기(배관 → BOX → 배관)
* pipe → 배관 길이 · 전 · 후 비움 + 흙막이 추가 셋 + 도수로/돌붙임 하나 → [저장]
* (orca reload) pipe_v → 다시 열기 · 흙막이 수 3 · 비운 칸 그대로
* pipe → 배관 길이 · 전 · 후 비움 + 도수로/돌붙임 하나 → [저장](흙막이 = 유출 단 수 · [추가] 없음 · 54-1)
* (orca reload) pipe_v → 다시 열기 · 흙막이 수 = 저장 전 그대로 · 비운 칸 그대로
* bf → 258.12 를 BOX암거로(PUT) · 가로 비움 · 세월교 1 월류 폭 비움 → [저장]
* (orca reload) bf_v → 다시 열기 · 오류 없음
* restore → 원래 관 지점으로 되돌림(PUT) · 초안 비움
* 쓰는 것: 검증 프로젝트 pipe_points.json(되돌림 단계가 원래 값으로 다시 씀)
* 기대 수치: errors 0 · cache.same true · kind.leak 0 · soilCount 3 · inletBlank 빈 칸 셋 · restored true
* 기대 수치: errors 0 · cache.same true · kind.leak 0 · soilCount pipe = pipe_v · soilAdd 0 · inletBlank 빈 칸 셋 · restored true
* (배관 도수로/돌붙임 [추가] 는 하나 뒤에도 보임 — 「하나만」은 물넘이 포장 · 기타만 · 52-3 · 52-10)
*/
(async () => {
@@ -66,7 +66,8 @@
await w(500);
return until(() => !document.querySelector(".ui-overlay.is-active"), 60000);
};
const items = () => [...spec().querySelectorAll("[data-spec-item]")].map((n) => n.dataset.specItem);
const items = () =>
[...spec().querySelectorAll("[data-spec-item]")].map((n) => n.dataset.specItem);
const point = async (lo, hi) =>
(await fetchApi.fetchDetailPipePoints(projectId)).pipe_points.find(
(p) => p.chainage_m > lo && p.chainage_m < hi,
@@ -89,21 +90,17 @@
}
if (MODE === "pipe") {
sessionStorage.setItem("__5213_original", JSON.stringify((await fetchApi.fetchDetailPipePoints(projectId)).pipe_points));
sessionStorage.setItem(
"__5213_original",
JSON.stringify((await fetchApi.fetchDetailPipePoints(projectId)).pipe_points),
);
await pick(/배관 1$/);
await set("inlet", "structure", "기슭막이");
await set("inlet", "form", "돌망태");
await set("inlet", "length", "");
await set("inlet", "before", "");
await set("inlet", "after", "");
for (let i = 0; i < 3; i++) {
spec().querySelector('[data-spec-add="soil_add"]').click();
await w(200);
}
for (let i = 0; i < 3; i++) {
await set("soil_add", "structure", "기슭막이", i);
await set("soil_add", "form", "콘크리트", i);
}
out.soilAdd = spec().querySelectorAll('[data-spec-add="soil_add"]').length;
spec().querySelector('[data-spec-add="chute_add"]').click();
await w(200);
await set("chute_add", "structure", "돌붙임");
@@ -180,7 +177,8 @@
clearDrafts();
sessionStorage.removeItem("__5213_original");
const now = (await fetchApi.fetchDetailPipePoints(projectId)).pipe_points;
out.restored = JSON.stringify(now.map((p) => [p.chainage_m, p.facility ?? null, p.options ?? null])) ===
out.restored =
JSON.stringify(now.map((p) => [p.chainage_m, p.facility ?? null, p.options ?? null])) ===
JSON.stringify(original.map((p) => [p.chainage_m, p.facility ?? null, p.options ?? null]));
}
@@ -6,8 +6,8 @@
* orca eval --expression "window.__m521='verify'; $(cat 이 파일)" → 다시 연 값 · B06 같은 그리기 · 관 길이
* orca eval --expression "window.__m521='restore'; $(cat 이 파일)" → 원래 관 지점으로 되돌림(PUT) · 초안 비움
* 쓰는 것: 검증 프로젝트 pipe_points.json(되돌림 단계가 원래 값으로 다시 씀)
* 기대 수치(edit): kinds 5 · required 3 늘 보임 · formBasin I형/L형/ㄷ형 · formSoil 6 · split 길이 7 ·
* soilAdd 둘 → 하나 빼면 하나(콘크리트) · chute 하나(돌붙임) · 잇기 유입 지정 · 유출 지정 뒤 풂
* 기대 수치(edit): kinds 4(흙막이 = 유출 단 수 · [추가] 없음 · 54-1) · required 3 늘 보임 · formBasin I형/L형/ㄷ형 ·
* formSoil 6 · split 길이 7 · soilAddButtons 0 · chute 하나(돌붙임) · 잇기 유입 지정 · 유출 지정 뒤 풂
* 기대 수치(verify): 저장 값 그대로 · 같은 그리기(B05 = B06) · 관 길이 수(B06)
* 52-1b: edit 가 흙막이 추가 · 도수로/돌붙임 컨테이너도 이음 → verify 집계표 줄 = 처음 + 3
* (유입 잇기 1 · 컨테이너마다 1)
@@ -120,33 +120,16 @@
out.splitAfter = ctl("inlet", "after").value;
await set("inlet", "after", "4");
out.splitLength = ctl("inlet", "length").value;
// 선택 컨테이너 — 흙막이 추가 둘 → 첫째 뺌 · 도수로/돌붙임 하나
spec().querySelector('[data-spec-add="soil_add"]').click();
await w(200);
spec().querySelector('[data-spec-add="soil_add"]').click();
await w(200);
await set("soil_add", "structure", "기슭막이", 0);
await set("soil_add", "form", "바자", 0);
await set("soil_add", "structure", "기슭막이", 1);
await set("soil_add", "form", "콘크리트", 1);
out.soilAddTwo = spec().querySelectorAll('[data-spec-container="soil_add"]').length;
out.soilFormsSoilAdd = labels("soil_add", "form").length;
box("soil_add", 0).querySelector('[data-spec-remove="soil_add"]').click();
await w(300);
out.soilAddAfterRemove = [...spec().querySelectorAll('[data-spec-container="soil_add"]')].map(
(b, i) => [
b.querySelector("legend").firstChild.textContent,
ctl("soil_add", "form", i).value,
],
);
// 흙막이 = 유출 「추가 기슭막이(단)」 수만큼(54-1) — [추가] · [빼기] 없음 · 도수로/돌붙임 하나
out.soilAddButtons = spec().querySelectorAll('[data-spec-add="soil_add"]').length;
out.soilRemoveButtons = spec().querySelectorAll('[data-spec-remove="soil_add"]').length;
spec().querySelector('[data-spec-add="chute_add"]').click();
await w(200);
await set("chute_add", "structure", "돌붙임");
out.chuteChoices = labels("chute_add", "structure");
// 52-1b — 컨테이너마다 구조물 조합 잇기(집계표 한 줄씩)
await pickLink("soil_add");
await pickLink("chute_add");
out.linkContainers = [linkName("soil_add"), linkName("chute_add")];
out.linkContainers = [linkName("chute_add")];
// 구조물 조합 잇기 — 유입 지정 · 유출 지정 뒤 풂
await pickLink("inlet");
out.linkInlet = linkName("inlet");
@@ -179,7 +162,7 @@
linkOutlet: linkName("outlet"),
};
const b05 = shape();
out.b05Length = ctl("pipe", "length").value;
out.b05Length = box("pipe").querySelector("output")?.textContent; // 보여 주는 칸(54-1)
location.hash = "#/b06-section";
await until(() => location.hash.includes("b06"), 5000);
await w(6000);
@@ -187,7 +170,8 @@
const b06 = shape();
out.sameDrawing = JSON.stringify(b05) === JSON.stringify(b06);
if (!out.sameDrawing) out.diff = { b05, b06 };
out.b06Length = ctl("pipe", "length").value;
out.b06Length = box("pipe").querySelector("output")?.textContent;
out.lengthInputs = box("pipe").querySelectorAll('[data-spec-item="pipe.length"] input').length;
out.baseRows = Number(sessionStorage.getItem("__521_baseRows"));
out.rows = await filledRows();
}
@@ -0,0 +1,134 @@
/* 54-1 — B05 · B06 좌측 패널 배관: 「선택」 글 · 지난 정의 값 숨김 · 관 길이 보여 주는 칸(정수) ·
* 유입 집수정 = 길이 · 전 · 후 숨김 · 유출 읽기 줄(관길이 · 좌우 자동 자리) 없음 · [흙막이 추가] 없음 ·
* 흙막이 컨테이너 수 = 유출 「추가 기슭막이(단)」 N
* 시작 화면: B05(#/b05-profile) 또는 B06(#/b06-section) · 검증 프로젝트(sub5_검증) · 목록에 「배관 1」(4+5)
* 부르는 법: orca eval --expression "window.__m541='단계'; $(cat 이 파일)"
* look → 지금 화면 배관 폼 수치(아무것도 안 바꿈)
* basin → 유입 구조 집수정 · 흙막이 번갈아 보이는 칸(되돌림 · 저장 안 함)
* tier+ / tier- → B06 유출 벽을 골라 「추가 기슭막이(단)」 + / - 한 번 · 흙막이 컨테이너 수
* 기대: pick 글 전부 「선택」 · 고르기 0 · stale 보임 0 · 관 길이 input 0 · 값 정수 ·
* 집수정 칸 = structure · form · height · 흙막이 칸 = + length · before · after ·
* soil_add 추가 단추 0 · 유출 읽기 줄 0 · soil_add 수 = 단 수
*/
(async () => {
const MODE = window.__m541 || "look";
// 고를 관(목록 이름) — 기본 「배관 1」 · 단이 서는 지형이 아니면 다른 관(window.__m541pipe)
const PIPE = window.__m541pipe || "배관 1";
let station = "";
const w = (ms) => new Promise((r) => setTimeout(r, ms));
const until = async (fn, ms = 20000) => {
for (let t = 0; t < ms; t += 200) {
if (fn()) return true;
await w(200);
}
return false;
};
const spec = () =>
[...document.querySelectorAll(".b05-structure__spec")].find((n) => n.offsetParent !== null);
const box = (c, i = 0) => spec()?.querySelectorAll(`[data-spec-container="${c}"]`)[i];
const items = (c) =>
[...(box(c)?.querySelectorAll("[data-spec-item]") ?? [])].map(
(n) => n.dataset.specItem.split(".")[1],
);
const set = async (c, item, v) => {
const el = box(c).querySelector(`[data-spec-item="${c}.${item}"]`).querySelector("select");
el.value = v;
el.dispatchEvent(new Event("change", { bubbles: true }));
await w(300);
};
const pickPipe = async () => {
const chosen = document.querySelector(".b05-route__irregular-list li.is-selected");
if (spec() && chosen?.textContent.trim().endsWith(PIPE)) return;
await until(() => document.querySelectorAll(".b05-route__irregular-list li").length > 0);
const li = [...document.querySelectorAll(".b05-route__irregular-list li")].find((l) =>
l.textContent.trim().endsWith(PIPE),
);
(li.querySelector("button") ?? li).click();
await until(() => spec(), 5000);
await w(500);
};
const visible = (n) => n.offsetParent !== null && !n.closest("[hidden]");
const tierRow = () =>
box("outlet")?.querySelector(".b06-structure-panel__struct--tier:not(.is-hidden)");
const look = () => {
const lengthField = box("pipe")?.querySelector('[data-spec-item="pipe.length"]');
const picks = [...document.querySelectorAll('[data-b05-summary="pick"]')].filter(visible);
return {
view: location.hash,
pickTexts: [...new Set(picks.map((b) => b.textContent))],
pickCount: picks.length,
gorugi: [...document.querySelectorAll("button")].filter(
(b) => visible(b) && b.textContent.trim() === "고르기",
).length,
staleShown: [...document.querySelectorAll(".b05-structure__stale")].filter(visible).length,
staleLegend: [...document.querySelectorAll("legend")].filter(
(l) => visible(l) && l.textContent.includes("지난 정의 값"),
).length,
lengthInputs: lengthField?.querySelectorAll("input").length,
lengthText: lengthField?.querySelector("output")?.textContent,
inletStructure: box("inlet")?.querySelector('[data-spec-item="inlet.structure"] select')
?.value,
inletItems: items("inlet"),
soilAddButton: spec()?.querySelectorAll('[data-spec-add="soil_add"]').length,
soilRemove: spec()?.querySelectorAll('[data-spec-remove="soil_add"]').length,
chuteAddButton: spec()?.querySelectorAll('[data-spec-add="chute_add"]').length,
soilBoxes: [...(spec()?.querySelectorAll('[data-spec-container="soil_add"]') ?? [])].map(
(b) => ({
legend: b.querySelector("legend").firstChild.textContent,
items: [...b.querySelectorAll("[data-spec-item]")].map(
(n) => n.dataset.specItem.split(".")[1],
),
link: !!b.querySelector('[data-b05-summary="pick"]'),
}),
),
outletReadout: box("outlet")?.querySelectorAll(".b06-structure-panel__struct--readout")
.length,
outletText: box("outlet")?.textContent.includes("자동 자리"),
tier: tierRow()?.querySelector(".b06-structure-panel__hval")?.textContent ?? null,
extraGroupShown: [...document.querySelectorAll("legend")].filter(
(l) => visible(l) && l.textContent.startsWith("추가 기슭막이"),
).length,
};
};
await pickPipe();
station = [...document.querySelectorAll(".b05-route__irregular-list li")]
.find((l) => l.textContent.trim().endsWith(PIPE))
.textContent.trim()
.split(PIPE)[0];
const out = { mode: MODE, pipe: PIPE, station };
if (MODE === "look") Object.assign(out, look());
if (MODE === "basin") {
const was = box("inlet").querySelector('[data-spec-item="inlet.structure"] select').value;
await set("inlet", "structure", "집수정");
out.basinItems = items("inlet");
await set("inlet", "structure", "기슭막이");
out.soilItems = items("inlet");
await set("inlet", "structure", was);
out.back = items("inlet");
out.was = was;
}
if (MODE === "tier+" || MODE === "tier-") {
// 유출 벽 고르기(이미 골라져 단 수 줄이 서 있으면 그대로)
if (!tierRow()) {
// 판정면(관 위로 올린 것) — 4+5 카드의 유출 벽
const wall = [...document.querySelectorAll(".b06-chart__culvert-revet-hit")].find(
(e) =>
e.closest(".b06-cross-card")?.textContent.startsWith(station) &&
e.querySelector("title")?.textContent.startsWith("유출"),
);
out.wallFound = !!wall;
wall?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
await until(() => tierRow(), 5000);
await w(500);
}
out.before = tierRow()?.querySelector(".b06-structure-panel__hval")?.textContent;
const btns = [...(tierRow()?.querySelectorAll("button") ?? [])];
(MODE === "tier+"
? btns.find((b) => b.textContent === "+")
: btns.find((b) => b.textContent === "-")
)?.click();
await w(1500);
Object.assign(out, look());
}
return JSON.stringify(out);
})();