Merge remote-tracking branch 'origin/dev' into sub_laptop_8
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
|
||||
import type { SpecChoice, SpecItem, StructureSpec } from "./design_structure_spec";
|
||||
import { FORD_SPEC } from "./design_structure_ford";
|
||||
import { PAVEMENT_SPEC } from "./design_structure_road";
|
||||
|
||||
const SOIL = "기슭막이"; // 저장 값(옛 키 그대로) — 화면 이름은 흙막이
|
||||
const BASIN = "집수정";
|
||||
@@ -188,4 +189,5 @@ export const PIPE_SPEC: StructureSpec = {
|
||||
export const STRUCTURE_SPECS: Readonly<Record<string, StructureSpec>> = {
|
||||
pipe: PIPE_SPEC,
|
||||
ford_bridge: FORD_SPEC,
|
||||
pavement_concrete: PAVEMENT_SPEC,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/* =============================================================================
|
||||
* design_structure_road.ts
|
||||
* 노면공 › 콘크리트 포장 명세 — 사용자 구조물 표 08(`docs/raw/구조물정의_표/08_사토장_토취장_노면공_기타.md`).
|
||||
*
|
||||
* 기준 측점(패널 위치 칸) + 필수 컨테이너 「상세 내용」 한 벌 = 포장사양 고르기 · 높이(m) 손 입력.
|
||||
* 구간 길이 · 전 · 후는 정의 옵션 칸 그대로(구간 배치) · 표에 없는 옛 상세 칸은 화면에서 빼고 값은 남김.
|
||||
* 저장 = structures.json `options` — 새 키 `pave_spec` · `height_m`.
|
||||
* import 없음 — 시험이 이 파일만 옮겨 돌림.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { SpecChoice, StructureSpec } from "./design_structure_spec";
|
||||
|
||||
/** 포장사양 선택지 — 표의 나머지 값은 사용자 답 뒤 여기에만 더함. */
|
||||
export const PAVE_SPEC_CHOICES: SpecChoice[] = [{ value: "콘크리트", label: "콘크리트" }];
|
||||
|
||||
export const PAVEMENT_SPEC: StructureSpec = {
|
||||
typeId: "pavement_concrete",
|
||||
containers: [
|
||||
{
|
||||
id: "detail",
|
||||
title: "상세 내용",
|
||||
required: true,
|
||||
link: "gpavedetail",
|
||||
items: [
|
||||
{
|
||||
id: "spec",
|
||||
label: "포장사양",
|
||||
kind: "select",
|
||||
key: "pave_spec",
|
||||
choices: PAVE_SPEC_CHOICES,
|
||||
},
|
||||
{ id: "height", label: "높이 (m)", kind: "number", key: "height_m", step: 0.05 },
|
||||
],
|
||||
},
|
||||
],
|
||||
// 표 08 에 없는 옛 상세 칸 — 화면에서 빼고 값은 남김(지난 정의 값)
|
||||
retired: [
|
||||
"thickness_cm",
|
||||
"width_m",
|
||||
"widening_area_m2",
|
||||
"joint_spacing_m",
|
||||
"separation_sheet",
|
||||
"wire_mesh",
|
||||
],
|
||||
};
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
createFacilityDefinition,
|
||||
type FacilityDefinition,
|
||||
} from "./B05_Profile_UI_Structures_Define_Form";
|
||||
import { createStructureSpecForm } from "./B05_Profile_UI_Structures_Spec";
|
||||
|
||||
export interface StructuresFormElements {
|
||||
root: HTMLElement;
|
||||
@@ -164,6 +165,13 @@ export function buildStructuresForm(options: {
|
||||
set: groupBoxes.setOne,
|
||||
});
|
||||
groupBoxes.watch(facilityOptions.paintLinks);
|
||||
// 일반 구조물 명세 컨테이너(PLAN 52-10 · 노면공 등) — 잇기 · 반영 길은 배관 명세와 같음
|
||||
const structureSpec = createStructureSpecForm(
|
||||
optionRow,
|
||||
{ get: groupBoxes.get, pick: groupBoxes.pick, clear: groupBoxes.clear, set: groupBoxes.setOne },
|
||||
options.onFacilityChange,
|
||||
);
|
||||
groupBoxes.watch(structureSpec.paintLinks);
|
||||
const staleBox = document.createElement("fieldset");
|
||||
staleBox.className = "b05-structure__group b05-structure__stale";
|
||||
staleBox.hidden = true;
|
||||
@@ -175,6 +183,7 @@ export function buildStructuresForm(options: {
|
||||
summary.root, // 종류 단위 「구조물 조합」 잇기 — 기준 측점 바로 아래(PLAN 52-1 · 컨테이너 없는 종류)
|
||||
positionDivider,
|
||||
optionRow,
|
||||
structureSpec.root,
|
||||
facilityOptions.root,
|
||||
staleBox,
|
||||
groupBoxes.root,
|
||||
|
||||
@@ -56,6 +56,12 @@ export function numberDuplicateNames(names: ReadonlyArray<string>): string[] {
|
||||
});
|
||||
}
|
||||
|
||||
/** 기타 = 사용자가 적은 상세 종류 이름(표 08 · 52-10) — 비었으면 null(종류 이름). */
|
||||
function etcName(structure: StructureInstance): string | null {
|
||||
const name = structure.type_id === "etc" ? String(structure.options?.name ?? "").trim() : "";
|
||||
return name || null;
|
||||
}
|
||||
|
||||
export function renderStructureList(params: StructureListParams): void {
|
||||
const { list, typeMap, intervalM, editingId, selectedPipeChainage } = params;
|
||||
list.replaceChildren();
|
||||
@@ -89,7 +95,9 @@ export function renderStructureList(params: StructureListParams): void {
|
||||
const displayNames = numberDuplicateNames(
|
||||
rows.map((row) =>
|
||||
row.kind === "structure"
|
||||
? (typeMap.get(row.structure.type_id)?.name ?? row.structure.type_id)
|
||||
? (etcName(row.structure) ??
|
||||
typeMap.get(row.structure.type_id)?.name ??
|
||||
row.structure.type_id)
|
||||
: (typeMap.get(row.pipe.facility)?.name ?? row.pipe.facility),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -18,6 +18,7 @@ import type { FacilityAttributes, FacilityOptionsForm } from "./B05_Profile_UI_D
|
||||
import type { StationFields } from "./B05_Profile_UI_Structures_Fields";
|
||||
import { splitPavementRange } from "./B05_Profile_UI_Structures_Pavement";
|
||||
import { mergeStructureOptions } from "./B05_Profile_UI_Structures_Summary";
|
||||
import { readStructureSpec, specRenderedKeys } from "./B05_Profile_UI_Structures_Spec";
|
||||
import type { GroupBoxes } from "./B05_Profile_UI_Structures_Groups";
|
||||
import {
|
||||
readGroupItems,
|
||||
@@ -40,6 +41,8 @@ export interface StructuresCommitContext {
|
||||
startFields: StationFields;
|
||||
endFields: StationFields;
|
||||
facilityOptions: FacilityOptionsForm;
|
||||
/** 옵션 칸 — 짝 명세 폼(52-10)을 찾는 열쇠. */
|
||||
optionRow: HTMLElement;
|
||||
optionInputs: Array<{
|
||||
key: string;
|
||||
required: boolean;
|
||||
@@ -205,7 +208,7 @@ export function readOptions(ctx: StructuresCommitContext): Record<string, string
|
||||
if (input.isEmpty()) return;
|
||||
values[input.key] = input.read();
|
||||
});
|
||||
return values;
|
||||
return { ...values, ...readStructureSpec(ctx.optionRow, ctx.currentType()) };
|
||||
}
|
||||
|
||||
export function emit(ctx: StructuresCommitContext): void {
|
||||
@@ -369,7 +372,11 @@ export async function commit(ctx: StructuresCommitContext, live = false): Promis
|
||||
options: mergeStructureOptions(
|
||||
previous,
|
||||
type.type_id,
|
||||
[...ctx.optionInputs.map((entry) => entry.key), ...ctx.groupBoxes.shapeKeys()],
|
||||
[
|
||||
...ctx.optionInputs.map((entry) => entry.key),
|
||||
...ctx.groupBoxes.shapeKeys(),
|
||||
...specRenderedKeys(type, previous.options), // 명세 폼 키(52-10) — 비운 칸 · 뺀 컨테이너는 지움
|
||||
],
|
||||
records[0].options,
|
||||
),
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import { FORM_OPTION_KEYS } from "./B05_Profile_UI_Drainage_Facility_Merge";
|
||||
import { STRUCTURE_SPECS } from "../A00_Common/design/design_structure_pipe";
|
||||
import { specKeys } from "../A00_Common/design/design_structure_spec";
|
||||
import { paintStructureSpec, specOwnedKeys } from "./B05_Profile_UI_Structures_Spec";
|
||||
|
||||
/** 종류 단위 「구조물 조합」 줄을 가릴 종류인지 — 구조물 명세 컨테이너 · 정의 그룹 상자(그룹마다 잇기 줄)로
|
||||
* 그리는 종류(PLAN 52-1 · 52-6b) · 종단배수(B군 · 조합 잇기 없음 · 표 07). */
|
||||
@@ -62,10 +63,15 @@ export function renderOptionFields(
|
||||
optionRow.replaceChildren();
|
||||
// 서브폼이 담당하는 타입(계곡 통과 시설·독립 기슭막이)은 여기서 그리지 않는다.
|
||||
// 차례 · 안 씀 · 숨김은 정의대로(48-3 · `_Structures_Define`).
|
||||
// 명세 종류(52-10)는 명세 키 · 명세가 뺀 옛 키를 칸으로 안 그림 — 명세 폼이 옵션 칸 아래에 그림
|
||||
const owned = specOwnedKeys(type);
|
||||
const shown =
|
||||
type && !ctx.facilityFormKind(type)
|
||||
? orderedOptions(type).filter((o) => ctx.includeDetail || isB05Option(o))
|
||||
? orderedOptions(type).filter(
|
||||
(o) => (ctx.includeDetail || isB05Option(o)) && !owned.has(o.key),
|
||||
)
|
||||
: [];
|
||||
paintStructureSpec(optionRow, type, values);
|
||||
optionRow.style.gridTemplateColumns = "";
|
||||
if (!type || !shown.length) {
|
||||
optionRow.hidden = true;
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile_UI_Structures_Spec.ts
|
||||
* 일반 구조물(structures.json) 명세 컨테이너 폼(PLAN 52-10) — 계곡 통과 시설 밖 종류가 명세
|
||||
* (`STRUCTURE_SPECS`)를 가지면 옵션 칸 아래에 같은 공용 폼(`design_structure_form`)을 그림.
|
||||
*
|
||||
* · 폼은 옵션 칸(`optionRow`) 짝으로 하나 — 조립기가 세워 걸고, 옵션 그리기 · 저장이 꺼내 씀
|
||||
* · 명세 키 · 명세가 뺀 옛 키(retired)는 정의 옵션 칸으로 그리지 않음(옛 값은 병합이 지킴)
|
||||
* · 잠금은 옵션 칸을 따름(측점 확정 전 흐림 · 조작 막음)
|
||||
* ========================================================================== */
|
||||
|
||||
import type { StructureType } from "./B05_Profile_Api_Structures";
|
||||
import {
|
||||
createSpecForm,
|
||||
type SpecForm,
|
||||
type SpecLinks,
|
||||
} from "../A00_Common/design/design_structure_form";
|
||||
import { STRUCTURE_SPECS } from "../A00_Common/design/design_structure_pipe";
|
||||
import { specKeys, type StructureSpec } from "../A00_Common/design/design_structure_spec";
|
||||
|
||||
const forms = new WeakMap<HTMLElement, SpecForm>();
|
||||
|
||||
/** 일반 구조물 명세 — 계곡 통과 시설(관 지점 정본)은 시설 폼 몫이라 뺌. */
|
||||
export const structureSpecOf = (type: StructureType | null): StructureSpec | null =>
|
||||
(type && !type.managed_by && STRUCTURE_SPECS[type.type_id]) || null;
|
||||
|
||||
/** 옵션 칸 짝 폼을 세움 — 값이 바뀌면 `onChange`(패널 즉시 반영). */
|
||||
export function createStructureSpecForm(
|
||||
optionRow: HTMLElement,
|
||||
links: SpecLinks,
|
||||
onChange: () => void,
|
||||
): SpecForm {
|
||||
const form = createSpecForm(links, { onChange });
|
||||
form.root.hidden = true;
|
||||
forms.set(optionRow, form);
|
||||
const lock = (): void => {
|
||||
const locked = optionRow.classList.contains("is-locked");
|
||||
form.root.inert = locked;
|
||||
form.root.classList.toggle("is-locked", locked);
|
||||
};
|
||||
if (typeof MutationObserver !== "undefined")
|
||||
new MutationObserver(lock).observe(optionRow, {
|
||||
attributes: true,
|
||||
attributeFilter: ["class"],
|
||||
});
|
||||
return form;
|
||||
}
|
||||
|
||||
/** 정의 옵션 칸으로 안 그리는 키 — 명세 키 · 명세가 뺀 옛 키. */
|
||||
export function specOwnedKeys(type: StructureType | null): Set<string> {
|
||||
const spec = structureSpecOf(type);
|
||||
return new Set(spec ? [...specKeys(spec).keys, ...spec.retired] : []);
|
||||
}
|
||||
|
||||
/** 종류 · 값으로 명세 폼을 다시 그림(명세 없는 종류면 가림). */
|
||||
export function paintStructureSpec(
|
||||
optionRow: HTMLElement,
|
||||
type: StructureType | null,
|
||||
values: Record<string, string | number>,
|
||||
): void {
|
||||
const form = forms.get(optionRow);
|
||||
const spec = structureSpecOf(type);
|
||||
if (!form) return;
|
||||
form.root.hidden = !spec;
|
||||
if (!spec) return;
|
||||
const groups = Object.fromEntries((type?.groups ?? []).map((g) => [g.id, g.name]));
|
||||
form.write(spec, values, type?.spec_names ?? {}, groups);
|
||||
}
|
||||
|
||||
/** 명세 폼 값(저장 키 그대로) — 명세 없는 종류면 빈 묶음. */
|
||||
export function readStructureSpec(
|
||||
optionRow: HTMLElement,
|
||||
type: StructureType | null,
|
||||
): Record<string, string | number> {
|
||||
const form = forms.get(optionRow);
|
||||
return form && structureSpecOf(type) ? form.read() : {};
|
||||
}
|
||||
|
||||
/** [수정] 병합에서 폼이 다시 쓰는 키 — 명세 키 + 옛 값 중 선택 컨테이너 머리 키(빼면 지워져야 함). */
|
||||
export function specRenderedKeys(
|
||||
type: StructureType | null,
|
||||
previous: Record<string, string | number> | undefined,
|
||||
): string[] {
|
||||
const spec = structureSpecOf(type);
|
||||
if (!spec) return [];
|
||||
const { keys, prefixes } = specKeys(spec);
|
||||
const own = Object.keys(previous ?? {}).filter((key) => prefixes.some((p) => key.startsWith(p)));
|
||||
return [...keys, ...own];
|
||||
}
|
||||
@@ -190,6 +190,7 @@
|
||||
/* 측점 미입력 상태의 상세 옵션 잠금 — 입력 순서 가이드(2026-08-18 사용자 지시).
|
||||
조작 차단은 disabled·inert가 맡고 여기서는 흐림만 그린다. */
|
||||
.b05-structure__grid.is-locked,
|
||||
.b05-structure__spec.is-locked,
|
||||
.b05-drainage__facility.is-locked {
|
||||
opacity: 0.5;
|
||||
}
|
||||
@@ -501,6 +502,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
min-width: 0; /* 긴 집계 항목 이름이 상자를 패널 밖으로 밀지 않게(fieldset 최소 폭 · 52-9) */
|
||||
}
|
||||
|
||||
.b05-structure__optgroup > legend {
|
||||
|
||||
@@ -138,6 +138,7 @@ def _with_group_items(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
복사본 `{구조물 id}__{그룹 id}`. 정의된 그룹만(미지정 그룹 = 「미지정」 알림에 셈) · 정의가 사라진
|
||||
그룹 연결은 기록에만 남고 표에 안 들어감."""
|
||||
from common_util.common_util_summary_item import GROUP_JOIN
|
||||
from M02_MasterTemplete.M02_Table_Fill_Spec import container_rows
|
||||
from M02_MasterTemplete.StructOptions.M02_SO_Groups import groups_of
|
||||
|
||||
defined = groups_of()
|
||||
@@ -154,6 +155,7 @@ def _with_group_items(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"그룹": group["name"],
|
||||
}
|
||||
)
|
||||
out.extend(container_rows(item, links)) # 선택 컨테이너마다 한 건(PLAN 52-1b)
|
||||
return out
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
"""집계표 채움 — 구조물 명세 선택 컨테이너마다 한 건 더(PLAN 52-1b).
|
||||
|
||||
선택 컨테이너 인스턴스(옵션 `{머리}_{n}_{항목}`)마다 같은 구조물 · 자리에 한 건 —
|
||||
집계 항목 = 잇기 `group_items[{잇기}{n}]` · 복사본 `{구조물 id}__{잇기}{n}` · 옵션 = 그 컨테이너 값.
|
||||
열 바인딩이 읽는 칸이 갈래마다 달라(독립 기슭막이 `form` · `length_m` … · 관 `{유입|유출}_revet_*`)
|
||||
컨테이너 값을 그 칸들에 함께 얹음 — 어느 출처로 읽어도 그 컨테이너 값.
|
||||
짝: `A00_Common/design/design_structure_pipe.ts` 선택 컨테이너(머리 · 잇기 · 제목) — 시험이 대조.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from common_util.common_util_summary_item import GROUP_JOIN
|
||||
|
||||
#: 종류 → 선택 컨테이너 (머리, 잇기, 제목)
|
||||
SPEC_CONTAINERS: dict[str, tuple[tuple[str, str, str], ...]] = {
|
||||
"pipe": (
|
||||
("soil_add", "gsoiladd", "흙막이 추가"),
|
||||
("chute_add", "gchuteadd", "도수로/돌붙임"),
|
||||
),
|
||||
}
|
||||
_TEXT = ("structure", "form")
|
||||
|
||||
|
||||
def _as_options(values: dict[str, Any]) -> dict[str, Any]:
|
||||
"""컨테이너 값 → 열이 읽는 칸 — 구조 = `structure` · `{쪽}_type` ·
|
||||
나머지 = 맨 칸 + `{쪽}_revet_*`."""
|
||||
out: dict[str, Any] = {}
|
||||
for item, value in values.items():
|
||||
if item == "structure":
|
||||
out.update(structure=value, inlet_type=value, outlet_type=value)
|
||||
continue
|
||||
name = item if item in _TEXT else f"{item}_m"
|
||||
out[name] = value
|
||||
for side in ("inlet", "outlet"):
|
||||
out[f"{side}_revet_{name}"] = value
|
||||
return out
|
||||
|
||||
|
||||
def container_rows(item: dict[str, Any], links: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"""한 구조물의 선택 컨테이너 인스턴스마다 한 건(n 차례) — 컨테이너 없는 종류는 빈 목록."""
|
||||
rows: list[dict[str, Any]] = []
|
||||
for prefix, link, title in SPEC_CONTAINERS.get(item["type_id"], ()):
|
||||
head = re.compile(rf"^{prefix}_(\d+)_(.+)$")
|
||||
found: dict[int, dict[str, Any]] = {}
|
||||
for key, value in item["options"].items():
|
||||
hit = head.match(key)
|
||||
if hit:
|
||||
found.setdefault(int(hit.group(1)), {})[hit.group(2)] = value
|
||||
for n in sorted(found):
|
||||
gid = f"{link}{n}"
|
||||
rows.append(
|
||||
{
|
||||
**item,
|
||||
"options": {**item["options"], **_as_options(found[n])},
|
||||
"summary_item": links.get(gid),
|
||||
"복사본": f"{item['복사본']}{GROUP_JOIN}{gid}",
|
||||
"그룹": f"{title} {n}",
|
||||
}
|
||||
)
|
||||
return rows
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"판": 8,
|
||||
"판": 9,
|
||||
"schema_version": 1,
|
||||
"comment": "B05 구조물 타입 레지스트리 정본. 근거·옵션 상세는 PLAN.md 구조물 리스트와 resources/knowledge/technical_info/01_임도 참조. 2026-08-17 선택지 재편: B군(종단배수)·F군(생태/녹화)·G군(노면공)은 enabled:false 보존 — B05 선택지에서 빠지고 B06 개별 횡단도 옵션으로 재사용한다. 바닥막이(bed_sill)·생태연못(eco_pond)은 임도 미사용 확정으로 삭제. 배관·BOX암거·물넘이·세월교는 pipe_points.json 정본 관리(managed_by) — structures.json에 저장하지 않으며, 배관 부속(유형/집수정/기슭막이/돌붙임)·날개벽 옵션 정의는 B06/B07 필수 승격의 선언 근거다. ★기본값 원칙(2026-08-16 크로스체크 2차): default를 두는 것은 ①법정이 등급과 무관하게 못박은 단일 수치 ②사용자가 개발 단계에서 확정한 값 ③도메인 수치가 아닌 표시용 문자열, 이 셋뿐이다. 재료·형식 같은 설계자 판단 선택지와 미확정 제원은 default 없이 required로 두어 사용자가 직접 고르게 한다 (pipe_diameter_mm 1000 = 별표2 원칙값+사용자 확정 — 화이트리스트 등재). BOX암거 본체 2.0×2.0·날개벽 유입/유출 있음·짧은쪽 높이 1m·길이 2m·각도 45°는 2026-08-17 사용자 확정값이다 — 본체 규격은 실무 관측 규격(2.0×2.0·3.0×3.0)을 폼에서 프리셋 select로 받고 필요 시 사용자 지정으로 자유 입력하되, 저장 키는 body_width_m·body_height_m 숫자 그대로다(프리셋 전용 키를 만들지 않는다). 이 값들은 B05 서브폼이 실제로 받으므로 phase를 detail로 두지 않는다. ★보호공 개편(2026-08-17 사용자 지시): 구 `*_pitching`(있음/없음) + `*_pitching_finish`(찰/메) 두 축을 `*_protection` 한 축(돌붙임(찰)·돌붙임(메)·도수로)으로 합쳤다 — 도수로·산비탈수로(B4)는 유출부에서 돌붙임 대신 쓰는 공종이라 별도 구조물이 아니라 보호공 선택지로 받는다. \"없음\"은 삭제(물이 흐르는 자리라 보호공 필수). 치수는 배타 — 돌붙임이면 `*_protection_area_m2`(10㎡), 도수로면 `*_protection_width_m`(1m, 지식DB 수치 근거 없음·사용자 확정, 실무 확인 후 개선). 구 저장분은 `common_util_drainage_pipes.parse_pipe_points`가 새 키로 옮긴다.",
|
||||
"구조물군": {
|
||||
@@ -2838,7 +2838,7 @@
|
||||
{
|
||||
"type_id": "spoil_bank",
|
||||
"group": "E",
|
||||
"name": "유용토운반작업장(구 사토장)",
|
||||
"name": "유용토운반작업장(사토장)",
|
||||
"placement": "interval",
|
||||
"style": {
|
||||
"color": "#8c9a2e",
|
||||
@@ -2850,56 +2850,15 @@
|
||||
"quantity"
|
||||
],
|
||||
"options": [
|
||||
{
|
||||
"key": "side",
|
||||
"label": "쌓는 쪽",
|
||||
"input": "select",
|
||||
"choices": [
|
||||
"자동(성토 쪽)",
|
||||
"좌",
|
||||
"우"
|
||||
],
|
||||
"default": "자동(성토 쪽)",
|
||||
"required": false,
|
||||
"phase": "b05",
|
||||
"default_basis": "「자동(성토 쪽)」 — 흙은 성토 쪽에 쌓는다는 지금까지의 동작을 이어받는 표식. 측점마다 좌·우로 바꿈"
|
||||
},
|
||||
{
|
||||
"key": "fill_slope_ratio",
|
||||
"label": "비탈 기울기 (1:n 의 n)",
|
||||
"input": "number",
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 그 측점의 노선 성토 기울기를 그대로 씀 — 지식DB 가 「기본 비탈기울기는 근거에 없다」고 못 박은 자리"
|
||||
},
|
||||
{
|
||||
"key": "extra_distance_m",
|
||||
"label": "추가 운반거리",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": 0,
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"default_basis": "0 = 「추가 없음」 — 노선 안 사토장은 측점 누가거리로 거리가 나오므로 이 칸은 외부 사토장 대비 여벌"
|
||||
},
|
||||
{
|
||||
"key": "area_m2",
|
||||
"label": "면적",
|
||||
"input": "number",
|
||||
"unit": "㎡",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "capacity_m3",
|
||||
"label": "용량",
|
||||
"label": "사토장 부피",
|
||||
"input": "number",
|
||||
"unit": "㎥",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
"required": false,
|
||||
"phase": "b05",
|
||||
"empty_means": "비우면 B06 사토 배분이 이 사토장을 건너뜀"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -2917,23 +2876,15 @@
|
||||
"quantity"
|
||||
],
|
||||
"options": [
|
||||
{
|
||||
"key": "area_m2",
|
||||
"label": "면적",
|
||||
"input": "number",
|
||||
"unit": "㎡",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "capacity_m3",
|
||||
"label": "용량",
|
||||
"label": "토취장 부피",
|
||||
"input": "number",
|
||||
"unit": "㎥",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
"required": false,
|
||||
"phase": "b05",
|
||||
"empty_means": "비우면 자리만 잡음 — 부피는 설계자가 현장 조사로 손 입력"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -3232,7 +3183,7 @@
|
||||
"options": [
|
||||
{
|
||||
"key": "name",
|
||||
"label": "이름",
|
||||
"label": "상세 종류 이름",
|
||||
"input": "text",
|
||||
"default": "기타 구조물"
|
||||
}
|
||||
@@ -3394,6 +3345,26 @@
|
||||
],
|
||||
"형상": {}
|
||||
}
|
||||
],
|
||||
"spoil_bank": [
|
||||
{
|
||||
"id": "gspoildetail",
|
||||
"이름": "상세 내용",
|
||||
"옵션": [
|
||||
"capacity_m3"
|
||||
],
|
||||
"형상": {}
|
||||
}
|
||||
],
|
||||
"borrow_pit": [
|
||||
{
|
||||
"id": "gborrowdetail",
|
||||
"이름": "상세 내용",
|
||||
"옵션": [
|
||||
"capacity_m3"
|
||||
],
|
||||
"형상": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"배치": {},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,43 @@ const load = (file, deps = {}) => {
|
||||
return box.exports;
|
||||
};
|
||||
const s = load("design_structure_spec.ts");
|
||||
const { PIPE_SPEC } = load("design_structure_pipe.ts");
|
||||
const road = load("design_structure_road.ts");
|
||||
const { PIPE_SPEC, STRUCTURE_SPECS } = load("design_structure_pipe.ts", {
|
||||
"./design_structure_road": road,
|
||||
});
|
||||
|
||||
// 노면공 › 콘크리트 포장(PLAN 52-10) — 옛 상세 칸은 남고 명세 키만 오감
|
||||
if (process.argv[2] === "road") {
|
||||
const spec = STRUCTURE_SPECS.pavement_concrete;
|
||||
const old = {
|
||||
length_m: 12,
|
||||
before_m: 4,
|
||||
after_m: 8,
|
||||
thickness_cm: 20,
|
||||
joint_spacing_m: 6,
|
||||
pave_spec: "콘크리트",
|
||||
height_m: 0.2,
|
||||
};
|
||||
const state = s.readState(spec, old);
|
||||
const edited = JSON.parse(JSON.stringify(state));
|
||||
edited.detail[0].height = "0.35";
|
||||
const cleared = JSON.parse(JSON.stringify(state));
|
||||
cleared.detail[0].height = "";
|
||||
process.stdout.write(
|
||||
JSON.stringify({
|
||||
registered: Object.keys(STRUCTURE_SPECS),
|
||||
same: spec === road.PAVEMENT_SPEC,
|
||||
containers: spec.containers.map((c) => [c.id, c.title, c.required, c.link]),
|
||||
choices: road.PAVE_SPEC_CHOICES.map((c) => c.value),
|
||||
state,
|
||||
edited: s.writeOptions(spec, edited),
|
||||
cleared: s.writeOptions(spec, cleared),
|
||||
keys: s.specKeys(spec),
|
||||
retired: spec.retired,
|
||||
}),
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (process.argv[2] === "names") {
|
||||
process.stdout.write(JSON.stringify(s.nameTable(PIPE_SPEC)));
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""노면공 › 콘크리트 포장 명세 · 기타 이름(사용자 구조물 표 08) — PLAN 52-10.
|
||||
|
||||
순수 규칙은 Node 헬퍼(`helper_52_structure_spec.cjs road`)가 돌림 ·
|
||||
화면은 ORCA `ui_checks/52-10_노면공_기타.js`.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(os.path.dirname(HERE))
|
||||
DEFINITION = os.path.join(ROOT, "resources", "master_template", "구조물정의.json")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out():
|
||||
proc = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_52_structure_spec.cjs"), "road"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_명세_등록_필수_컨테이너_하나(out):
|
||||
assert {"pipe", "pavement_concrete"} <= set(out["registered"]) # 다른 창 명세도 함께 등록됨
|
||||
assert out["same"] is True
|
||||
assert out["containers"] == [["detail", "상세 내용", True, "gpavedetail"]]
|
||||
|
||||
|
||||
def test_포장사양_선택지는_명세_한_곳(out):
|
||||
assert out["choices"] == ["콘크리트"]
|
||||
|
||||
|
||||
def test_저장_키_왕복_옛_칸은_명세_밖(out):
|
||||
assert out["state"] == {"detail": [{"spec": "콘크리트", "height": "0.2"}]}
|
||||
assert out["edited"] == {"pave_spec": "콘크리트", "height_m": 0.35}
|
||||
assert out["cleared"] == {"pave_spec": "콘크리트"}
|
||||
assert out["keys"] == {"keys": ["pave_spec", "height_m"], "prefixes": []}
|
||||
assert "length_m" not in out["retired"] # 구간 길이 · 전 · 후는 정의 칸 그대로
|
||||
assert set(out["retired"]) == {
|
||||
"thickness_cm",
|
||||
"width_m",
|
||||
"widening_area_m2",
|
||||
"joint_spacing_m",
|
||||
"separation_sheet",
|
||||
"wire_mesh",
|
||||
}
|
||||
|
||||
|
||||
def test_기타_상세_종류_이름_글_칸():
|
||||
with open(DEFINITION, encoding="utf-8") as f:
|
||||
doc = json.load(f)
|
||||
etc = next(t for t in doc["types"] if t["type_id"] == "etc")
|
||||
name = next(o for o in etc["options"] if o["key"] == "name")
|
||||
assert name["label"] == "상세 종류 이름" and name["input"] == "text"
|
||||
pave = next(t for t in doc["types"] if t["type_id"] == "pavement_concrete")
|
||||
assert [o["key"] for o in pave["options"]][:3] == ["length_m", "before_m", "after_m"]
|
||||
@@ -0,0 +1,84 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""배관 남은 연동 — PLAN 52-1b.
|
||||
|
||||
못박는 것
|
||||
- 집계표: 선택 컨테이너(흙막이 추가 · 도수로/돌붙임)마다 한 건 — 잇기 `{link}{n}` 항목 ·
|
||||
그 컨테이너 값.
|
||||
- 짝: 서버 컨테이너 표(머리 · 잇기) = 배관 명세 TS.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
||||
from M02_MasterTemplete import M02_Table_Fill as fill
|
||||
from M02_MasterTemplete.M02_Table_Fill_Spec import SPEC_CONTAINERS, container_rows
|
||||
from resources.tester.test_structures_summary_snapshot import ( # noqa: F401 — world 고정물
|
||||
MS,
|
||||
PIPE,
|
||||
RV,
|
||||
_pipe,
|
||||
_save,
|
||||
world,
|
||||
)
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(os.path.dirname(HERE))
|
||||
SOIL = {
|
||||
"soil_add_1_structure": "기슭막이",
|
||||
"soil_add_1_form": "바자",
|
||||
"soil_add_1_length": 6,
|
||||
"soil_add_1_height": 2,
|
||||
"soil_add_2_structure": "기슭막이",
|
||||
"chute_add_1_structure": "돌붙임",
|
||||
}
|
||||
|
||||
|
||||
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] == [
|
||||
"pipe@40.0__gsoiladd1",
|
||||
"pipe@40.0__gsoiladd2",
|
||||
"pipe@40.0__gchuteadd1",
|
||||
]
|
||||
assert [r["summary_item"] for r in rows] == [RV, None, MS]
|
||||
first = rows[0]["options"]
|
||||
assert first["length_m"] == first["inlet_revet_length_m"] == first["outlet_revet_length_m"] == 6
|
||||
assert (
|
||||
first["form"] == first["outlet_revet_form"] == "바자" and first["outlet_type"] == "기슭막이"
|
||||
)
|
||||
assert first["pipe_diameter_mm"] == 1000 # 구조물 옵션은 그대로
|
||||
assert container_rows({"type_id": "masonry_dry", "options": SOIL, "복사본": "st0"}, {}) == []
|
||||
# 옛 값 — 컨테이너 없는 옛 관 · 인스턴스 없는 잇기는 줄 없음
|
||||
old = {"type_id": "pipe", "options": {"inlet_revet_length_m": 5}, "복사본": "pipe@9.0"}
|
||||
assert container_rows(old, {"gsoiladd9": RV}) == []
|
||||
|
||||
|
||||
def test_집계표_채움에_컨테이너_줄(world):
|
||||
"""흙막이 추가 1 → 돌기슭막이 열(rv) 6 m · 관 본체 · 미지정 컨테이너는 표에 안 듦."""
|
||||
root, system = world["root"], world["system"]
|
||||
point = {**_pipe(PIPE), "group_items": {"gsoiladd1": RV}}
|
||||
point["options"] = {**point["options"], **SOIL}
|
||||
_save(root, [], [point])
|
||||
table = json.loads((system / "table/구조물집계표.json").read_text("utf-8"))
|
||||
doc = fill.fill_table(root, table)
|
||||
values = [row["값"] for row in doc["줄"] if not row.get("고정")]
|
||||
rv = [v for row in values for k, v in row.items() if k.split("|")[0] == "rv"]
|
||||
assert rv == [6]
|
||||
copies = [i["복사본"] for i in fill.collect_items(root)]
|
||||
assert "pipe@40.0__gsoiladd1" in copies and "pipe@40.0__gchuteadd1" in copies
|
||||
|
||||
|
||||
def test_서버_컨테이너_표는_배관_명세와_짝():
|
||||
with open(
|
||||
os.path.join(ROOT, "A00_Common/design/design_structure_pipe.ts"), encoding="utf-8"
|
||||
) as f:
|
||||
text = f.read()
|
||||
found = re.findall(
|
||||
r'title: "([^"]+)",\s*required: false,\s*prefix: "(\w+)",\s*link: "(\w+)"', text
|
||||
)
|
||||
assert [(p, link, t) for t, p, link in found] == list(SPEC_CONTAINERS["pipe"])
|
||||
@@ -0,0 +1,74 @@
|
||||
"""PLAN 52-9 — 안전부대용지 › 사토장 · 토취장 = 사용자 구조물 표 08.
|
||||
|
||||
· 있는 종류 키 그대로(spoil_bank · borrow_pit) · 기준 측점 + 컨테이너 「상세 내용」(정의 그룹) 하나
|
||||
· 부피 손 입력 = 옛 용량 키 `capacity_m3`(B06 사토 배분이 읽는 자리)
|
||||
· 옛 칸은 정의에서 빼 지난 정의 값
|
||||
· 잇기 = 그룹 id(`group_items`) — 집계 표가 그룹마다 한 줄
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from B05_Profile.B05_Profile_Structures_Schema import DEFINITION_PATH, load_structure_types
|
||||
from B06_Section.B06_Section_Engine_SpoilFill import spoil_sites
|
||||
from M02_MasterTemplete.M02_Table_Fill import _with_group_items
|
||||
from M02_MasterTemplete.StructOptions import M02_SO_Definition
|
||||
from M02_MasterTemplete.StructOptions.M02_SO_Groups import groups_of
|
||||
|
||||
KINDS = {
|
||||
"spoil_bank": ("유용토운반작업장(사토장)", "사토장 부피", "gspoildetail"),
|
||||
"borrow_pit": ("토사채취장(토취장)", "토취장 부피", "gborrowdetail"),
|
||||
}
|
||||
|
||||
|
||||
def test_옵션은_부피_하나_손_입력():
|
||||
by_id = {t.type_id: t for t in load_structure_types()}
|
||||
for type_id, (name, label, _) in KINDS.items():
|
||||
item = by_id[type_id]
|
||||
assert item.group == "E" and item.name == name and item.managed_by is None
|
||||
assert [o.key for o in item.options] == ["capacity_m3"]
|
||||
volume = item.options[0]
|
||||
assert volume.label == label and volume.input == "number" and volume.unit == "㎥"
|
||||
assert volume.phase == "b05" and not volume.required and volume.default is None
|
||||
|
||||
|
||||
def test_컨테이너_상세_내용_하나_부피를_담음():
|
||||
groups = groups_of()
|
||||
for type_id, (_, _, gid) in KINDS.items():
|
||||
assert groups[type_id] == [{"id": gid, "name": "상세 내용", "options": ["capacity_m3"]}]
|
||||
|
||||
|
||||
def test_정의_검사_통과_판_이력():
|
||||
doc = json.loads(Path(DEFINITION_PATH).read_text("utf-8"))
|
||||
assert M02_SO_Definition.check(doc) == []
|
||||
assert M02_SO_Definition.history_path(doc["판"] - 1).is_file()
|
||||
|
||||
|
||||
def test_잇기는_그룹마다_집계_한_줄():
|
||||
link = {"source": "SS", "code": "SS-000001", "name": "사토장"}
|
||||
rows = _with_group_items(
|
||||
[
|
||||
{
|
||||
"type_id": "spoil_bank",
|
||||
"복사본": "s1",
|
||||
"summary_item": None,
|
||||
"group_items": {"gspoildetail": link},
|
||||
}
|
||||
]
|
||||
)
|
||||
assert [r["복사본"] for r in rows] == ["s1", "s1__gspoildetail"]
|
||||
assert rows[1]["summary_item"] == link and rows[1]["그룹"] == "상세 내용"
|
||||
|
||||
|
||||
def test_B06_사토_배분은_부피를_그대로_읽음_옛_값도():
|
||||
new = {"type_id": "spoil_bank", "start_m": 20, "end_m": 40, "options": {"capacity_m3": 150}}
|
||||
old = {
|
||||
"type_id": "spoil_bank",
|
||||
"start_m": 60,
|
||||
"end_m": 80,
|
||||
"options": {"capacity_m3": 90, "side": "좌", "area_m2": 30, "extra_distance_m": 5},
|
||||
}
|
||||
sites = spoil_sites([new, old])
|
||||
assert [s["capacity_m3"] for s in sites] == [150.0, 90.0]
|
||||
assert sites[0]["side_option"] is None and sites[1]["side_option"] == "좌"
|
||||
assert sites[1]["extra_distance_m"] == 5
|
||||
@@ -0,0 +1,215 @@
|
||||
/* 52-10 — B05 · B06 좌측 「구조물 배치」 노면공 › 콘크리트 포장 · 기타 = 사용자 구조물 표 08
|
||||
* 시작 화면: B05 종단설계(#/b05-profile) · 검증 프로젝트(데스크탑_보조) · 폼 비어 있음 · 로그인 상태
|
||||
* 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m5210 = "단계";` 를 붙여
|
||||
* orca eval --expression "window.__m5210='edit'; $(cat 이 파일)" → 두 종류 모양 · 값 · 잇기 · [추가] · [저장]
|
||||
* (orca reload 로 다시 열기)
|
||||
* orca eval --expression "window.__m5210='verify'; $(cat 이 파일)" → 저장 값 그대로 · 같은 모양
|
||||
* (B06 으로 옮겨 같은 단계 — 같은 값 · 같은 모양)
|
||||
* orca eval --expression "window.__m5210='restore'; $(cat 이 파일)" → 추가한 둘 삭제 · [저장]
|
||||
* 쓰는 것: 검증 프로젝트 structures.json(되돌림 단계가 지움)
|
||||
* 기대 수치:
|
||||
* 포장 — containers ["detail"] · 상세 내용 · 포장사양 선택지 ["콘크리트"] · 높이 0.35 · 잇기 지정 ·
|
||||
* 종류 단위 잇기 줄 가림 · 옛 상세 칸(두께 · 줄눈 …) 안 보임 · 길이 · 전 · 후 칸 보임
|
||||
* 기타 — 「상세 종류 이름」 글 칸 = 「52-10 시험 기타」 · 목록 이름도 그 글 · 종류 단위 잇기 줄 보임 · 지정
|
||||
* verify — 같은 값 · 같은 모양(B05 = B06)
|
||||
*/
|
||||
(async () => {
|
||||
const MODE = window.__m5210 || "edit";
|
||||
const NAME = "52-10 시험 기타";
|
||||
const w = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const until = async (fn, ms = 15000) => {
|
||||
for (let t = 0; t < ms; t += 200) {
|
||||
if (fn()) return true;
|
||||
await w(200);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const vis = (n) => !!n && n.offsetParent !== null;
|
||||
const sec = () => document.querySelector(".b05-structure-section");
|
||||
const body = () => sec().querySelector(".b05-route__panel-body");
|
||||
const spec = () => sec().querySelector(".b05-structure__spec");
|
||||
const selects = () => sec().querySelectorAll(".b05-structure__grid select");
|
||||
const fire = (el, value) => {
|
||||
el.value = value;
|
||||
el.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
};
|
||||
const listItems = () => [...document.querySelectorAll(".b05-route__irregular-list li")];
|
||||
const ITEMS = [
|
||||
{ typeId: "pavement_concrete", group: "G", at: [4, 6], list: /콘크리트 포장/ },
|
||||
{ typeId: "etc", group: "호환", at: [5, 2], list: new RegExp(NAME) },
|
||||
];
|
||||
const typeSummary = () =>
|
||||
[...body().children].find((c) => c.classList.contains("b05-structure__summary"));
|
||||
const optionRow = () =>
|
||||
[...body().children].filter((c) => c.classList.contains("b05-structure__grid"))[1];
|
||||
const optionLabels = () =>
|
||||
vis(optionRow())
|
||||
? [...optionRow().querySelectorAll(":scope > label > span:first-child")].map((s) =>
|
||||
s.textContent.trim(),
|
||||
)
|
||||
: [];
|
||||
const specCtl = (item) =>
|
||||
spec()?.querySelector(`[data-spec-item="detail.${item}"]`)?.querySelector("select, input");
|
||||
|
||||
const shape = (typeId) => {
|
||||
const position = [...body().children].find((c) =>
|
||||
c.classList.contains("b05-structure__position-row"),
|
||||
);
|
||||
const after = [...body().children]
|
||||
.slice([...body().children].indexOf(position) + 1)
|
||||
.find((c) => vis(c));
|
||||
const nameInput = [...optionRow().querySelectorAll("label")]
|
||||
.find((l) => l.textContent.includes("상세 종류 이름"))
|
||||
?.querySelector("input");
|
||||
const boxes = vis(spec())
|
||||
? [...spec().querySelectorAll("[data-spec-container]")].filter(vis)
|
||||
: [];
|
||||
return {
|
||||
typeId,
|
||||
options: optionLabels(),
|
||||
containers: boxes.map((b) => b.dataset.specContainer),
|
||||
legend: boxes.map((b) => b.querySelector("legend").firstChild.textContent),
|
||||
paveChoices: specCtl("spec")
|
||||
? [...specCtl("spec").options].filter((o) => o.value).map((o) => o.value)
|
||||
: null,
|
||||
paveSpec: specCtl("spec")?.value ?? null,
|
||||
height: specCtl("height")?.value ?? null,
|
||||
containerLink: !!boxes[0]?.querySelector(".b05-structure__summary-name.is-assigned"),
|
||||
typeSummaryVisible: vis(typeSummary()),
|
||||
typeSummaryAfterPosition: after === typeSummary(),
|
||||
typeLink: !!typeSummary().querySelector(".b05-structure__summary-name.is-assigned"),
|
||||
name: nameInput?.value ?? null,
|
||||
stale: vis(sec().querySelector(".b05-structure__stale")),
|
||||
locked: !!spec()?.inert,
|
||||
};
|
||||
};
|
||||
const pickType = async (group, typeId) => {
|
||||
const [g, t] = selects();
|
||||
fire(g, group);
|
||||
await w(300);
|
||||
fire(t, typeId);
|
||||
await w(500);
|
||||
};
|
||||
const linkFirstLeaf = async (button) => {
|
||||
button.click();
|
||||
await until(() => document.querySelector(".ui-item-picker"));
|
||||
await w(600);
|
||||
const modal = document.querySelector(".ui-item-picker");
|
||||
const ctl = ["닫기", "개인용으로 복사", "관리 화면에서 고치기", "해제", "적용"];
|
||||
const leaf = [...modal.querySelectorAll("button")].find((b) => {
|
||||
const t = b.textContent.trim();
|
||||
return t && !/^[▸▾]/.test(t) && !ctl.includes(t);
|
||||
});
|
||||
leaf?.click();
|
||||
await w(300);
|
||||
[...modal.querySelectorAll("button")].find((b) => b.textContent.trim() === "적용")?.click();
|
||||
await w(500);
|
||||
return !!leaf;
|
||||
};
|
||||
const clickBtn = async (text) => {
|
||||
[...document.querySelectorAll("button")]
|
||||
.find((b) => b.textContent.trim() === text && vis(b))
|
||||
?.click();
|
||||
await w(1500);
|
||||
};
|
||||
// B06 은 첫 눌림이 그 측점의 다른 항목(관 등)을 고르기도 함 — 목록이 다시 그려지므로 새로 찾아 한 번 더
|
||||
const open = async (item) => {
|
||||
for (let k = 0; k < 3 && selects()[1].value !== item.typeId; k++) {
|
||||
const li = listItems().find((l) => item.list.test(l.textContent));
|
||||
if (!li) return false;
|
||||
(li.querySelector("button") ?? li).click();
|
||||
await w(800);
|
||||
}
|
||||
return selects()[1].value === item.typeId;
|
||||
};
|
||||
const out = { mode: MODE, items: [] };
|
||||
|
||||
if (MODE === "edit") {
|
||||
await until(() => listItems().length > 0);
|
||||
out.before = listItems().length;
|
||||
for (const item of ITEMS) {
|
||||
await pickType(item.group, item.typeId);
|
||||
const row = { lockedBeforePosition: shape(item.typeId).locked };
|
||||
const [no, dist] = sec()
|
||||
.querySelectorAll(".b05-structure__position-row > label")[2]
|
||||
.querySelectorAll("input");
|
||||
fire(no, String(item.at[0]));
|
||||
fire(dist, String(item.at[1]));
|
||||
await w(300);
|
||||
if (item.typeId === "pavement_concrete") {
|
||||
fire(specCtl("spec"), "콘크리트");
|
||||
await w(200);
|
||||
fire(specCtl("height"), "0.35");
|
||||
await w(200);
|
||||
row.picked = await linkFirstLeaf(
|
||||
spec().querySelector('[data-spec-link] [data-b05-summary="pick"]'),
|
||||
);
|
||||
} else {
|
||||
const input = [...optionRow().querySelectorAll("label")]
|
||||
.find((l) => l.textContent.includes("상세 종류 이름"))
|
||||
.querySelector("input");
|
||||
input.value = NAME;
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
input.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await w(200);
|
||||
row.picked = await linkFirstLeaf(typeSummary().querySelector('[data-b05-summary="pick"]'));
|
||||
}
|
||||
Object.assign(row, shape(item.typeId), { phase: "edit" });
|
||||
sec().querySelector(".is-primary").click(); // 추가
|
||||
await w(800);
|
||||
out.items.push(row);
|
||||
}
|
||||
out.after = listItems().length;
|
||||
await clickBtn("저장");
|
||||
out.listed = listItems()
|
||||
.map((l) => l.textContent.trim())
|
||||
.filter((t) => /콘크리트 포장|52-10/.test(t));
|
||||
} else if (MODE === "verify") {
|
||||
await until(() => listItems().length > 0);
|
||||
for (const item of ITEMS) {
|
||||
if (!(await open(item))) {
|
||||
out.items.push({ typeId: item.typeId, missing: true });
|
||||
continue;
|
||||
}
|
||||
await w(300);
|
||||
out.items.push({ ...shape(item.typeId), phase: "verify" });
|
||||
const li = listItems().find((l) => item.list.test(l.textContent));
|
||||
(li.querySelector("button") ?? li).click(); // 재선택 = 해제
|
||||
await w(300);
|
||||
}
|
||||
out.listed = listItems()
|
||||
.map((l) => l.textContent.trim())
|
||||
.filter((t) => /콘크리트 포장|52-10/.test(t));
|
||||
} else if (MODE === "restore") {
|
||||
await until(() => listItems().length > 0);
|
||||
for (const item of ITEMS) {
|
||||
if (!(await open(item))) continue;
|
||||
await clickBtn("삭제");
|
||||
}
|
||||
await clickBtn("저장");
|
||||
out.left = listItems().length;
|
||||
out.leftover = listItems().filter((l) => /52-10/.test(l.textContent)).length;
|
||||
}
|
||||
const [pave, etc] = out.items;
|
||||
out.paveOk =
|
||||
!!pave &&
|
||||
!pave.missing &&
|
||||
JSON.stringify(pave.containers) === '["detail"]' &&
|
||||
pave.legend[0] === "상세 내용" &&
|
||||
JSON.stringify(pave.paveChoices) === '["콘크리트"]' &&
|
||||
pave.paveSpec === "콘크리트" &&
|
||||
Number(pave.height) === 0.35 &&
|
||||
pave.containerLink &&
|
||||
!pave.typeSummaryVisible &&
|
||||
!pave.options.some((l) => /두께|줄눈|철망|비닐|포장 폭|확폭/.test(l)) &&
|
||||
["길이", "기준측점 전", "기준측점 후"].every((l) => pave.options.some((o) => o.startsWith(l)));
|
||||
out.etcOk =
|
||||
!!etc &&
|
||||
!etc.missing &&
|
||||
etc.name === NAME &&
|
||||
etc.containers.length === 0 &&
|
||||
etc.typeSummaryVisible &&
|
||||
etc.typeSummaryAfterPosition &&
|
||||
etc.typeLink;
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
@@ -9,6 +9,8 @@
|
||||
* 기대 수치(edit): kinds 5 · required 3 늘 보임 · formBasin I형/L형/ㄷ형 · formSoil 6 · split 길이 7 ·
|
||||
* soilAdd 둘 → 하나 빼면 하나(콘크리트) · chute 하나(돌붙임) · 잇기 유입 지정 · 유출 지정 뒤 풂
|
||||
* 기대 수치(verify): 저장 값 그대로 · 같은 그리기(B05 = B06) · 관 길이 수(B06)
|
||||
* 52-1b: edit 가 흙막이 추가 · 도수로/돌붙임 컨테이너도 이음 → verify 집계표 줄 = 처음 + 3
|
||||
* (유입 잇기 1 · 컨테이너마다 1)
|
||||
*/
|
||||
(async () => {
|
||||
const MODE = window.__m521 || "edit";
|
||||
@@ -28,6 +30,15 @@
|
||||
.filter((n) => n.includes(part))
|
||||
.pop();
|
||||
const fetchApi = await import(modUrl("/B04_PreProcess/B04_PreProcess_Api_Fetch.ts"));
|
||||
const m02Api = await import(
|
||||
modUrl("/B04_PreProcess/B04_PreProcess_Api_Fetch.ts").replace(
|
||||
/B04_PreProcess\/B04_PreProcess_Api_Fetch\.ts.*$/,
|
||||
"M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts",
|
||||
)
|
||||
);
|
||||
/** 구조물집계표 채움 줄 수(전구간 고정 줄 뺌) — 52-1b. */
|
||||
const filledRows = async () =>
|
||||
(await m02Api.readFilled(projectId, "구조물집계표")).문서.줄.filter((r) => !r.고정).length;
|
||||
const spec = () => document.querySelector(".b05-structure__spec");
|
||||
const box = (c, i = 0) => spec().querySelectorAll(`[data-spec-container="${c}"]`)[i];
|
||||
const ctl = (c, item, i = 0) =>
|
||||
@@ -86,6 +97,8 @@
|
||||
if (MODE === "edit") {
|
||||
const before = await fetchApi.fetchDetailPipePoints(projectId);
|
||||
sessionStorage.setItem("__521_original", JSON.stringify(before.pipe_points));
|
||||
out.baseRows = await filledRows();
|
||||
sessionStorage.setItem("__521_baseRows", String(out.baseRows));
|
||||
await pickPipe();
|
||||
const s0 = shape();
|
||||
out.kinds =
|
||||
@@ -128,6 +141,10 @@
|
||||
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")];
|
||||
// 구조물 조합 잇기 — 유입 지정 · 유출 지정 뒤 풂
|
||||
await pickLink("inlet");
|
||||
out.linkInlet = linkName("inlet");
|
||||
@@ -169,6 +186,8 @@
|
||||
out.sameDrawing = JSON.stringify(b05) === JSON.stringify(b06);
|
||||
if (!out.sameDrawing) out.diff = { b05, b06 };
|
||||
out.b06Length = ctl("pipe", "length").value;
|
||||
out.baseRows = Number(sessionStorage.getItem("__521_baseRows"));
|
||||
out.rows = await filledRows();
|
||||
}
|
||||
|
||||
if (MODE === "restore") {
|
||||
@@ -191,7 +210,7 @@
|
||||
);
|
||||
await fetchApi.saveDetailPipePoints(projectId, points);
|
||||
for (const k of Object.keys(sessionStorage))
|
||||
if (k.startsWith("aislo:draft:pipes:") || k === "__521_original")
|
||||
if (k.startsWith("aislo:draft:pipes:") || k.startsWith("__521_"))
|
||||
sessionStorage.removeItem(k);
|
||||
const now = await fetchApi.fetchDetailPipePoints(projectId);
|
||||
const pick = (list) =>
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
/* 52-9 — B05 · B06 좌측 「구조물 배치」 사토장 · 토취장 = 사용자 구조물 표 08
|
||||
* 시작 화면: B05 종단설계(#/b05-profile) · 검증 프로젝트(데스크탑_메인)
|
||||
* 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m529 = "단계";` 를 붙여
|
||||
* seed → 원래 구조물 목록을 sessionStorage 에 두고 옛 꼴 사토장 하나를 심음(PUT) · 그 뒤 orca reload
|
||||
* edit → 옛 사토장 = 지난 정의 값 · 새 사토장 · 토취장 추가(부피 · 잇기) · [저장] · 그 뒤 orca reload
|
||||
* verify → 다시 연 값 — B05 에서 한 번 · B06(#/b06-section) 에서 한 번(B05 = B06 그리기)
|
||||
* restore → 원래 구조물 목록으로 되돌림(PUT) · 초안 비움
|
||||
* 쓰는 것: 검증 프로젝트 structures.json(되돌림 단계가 원래 값으로 다시 씀)
|
||||
* 기대 수치(edit): 컨테이너 1(상세 내용) · 칸 = 사토장 부피 / 토취장 부피 · 종류 단위 잇기 줄 가림 ·
|
||||
* 옛 사토장 지난 정의 값 = side · fill_slope_ratio · extra_distance_m · area_m2 · 부피 90
|
||||
* 기대 수치(verify): 부피 150 · 80 · 잇기 지정 · 옛 키 그대로 · B05 = B06
|
||||
*/
|
||||
(async () => {
|
||||
const MODE = window.__m529 || "edit";
|
||||
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 projectId = localStorage.getItem("frd_current_project_id");
|
||||
const modUrl = (part) =>
|
||||
performance
|
||||
.getEntriesByType("resource")
|
||||
.map((e) => e.name)
|
||||
.filter((n) => n.includes(part))
|
||||
.pop();
|
||||
const api = await import(modUrl("/B05_Profile/B05_Profile_Api_Structures.ts"));
|
||||
const GID = { spoil_bank: "gspoildetail", borrow_pit: "gborrowdetail" };
|
||||
const section = () => document.querySelector(".b05-structure-section");
|
||||
const selects = () => section().querySelectorAll(".b05-structure__grid select");
|
||||
const change = async (el, v) => {
|
||||
el.value = v;
|
||||
el.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await w(250);
|
||||
};
|
||||
const station = async (index, no, plus) => {
|
||||
const wrap = section().querySelector(".b05-structure__position-row").children[index];
|
||||
const [s, r] = wrap.querySelectorAll("input");
|
||||
s.value = String(no);
|
||||
s.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
r.value = String(plus);
|
||||
r.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await w(250);
|
||||
};
|
||||
const box = (type) => section().querySelector(`[data-b05-group="${GID[type]}"]`);
|
||||
const volume = (type) => box(type)?.querySelector('[data-option-keys="capacity_m3"] input');
|
||||
const linked = (type) =>
|
||||
!!box(type)?.querySelector(".b05-structure__summary-name")?.classList.contains("is-assigned");
|
||||
const shape = (type) => ({
|
||||
boxes: [...section().querySelectorAll("[data-b05-group]")].map((b) => ({
|
||||
id: b.dataset.b05Group,
|
||||
legend: b.querySelector("legend").textContent,
|
||||
fields: [...b.querySelectorAll("[data-option-keys]")].map(
|
||||
(n) => `${n.dataset.optionKeys}=${n.querySelector("span")?.textContent}`,
|
||||
),
|
||||
linked: linked(type),
|
||||
})),
|
||||
typeRowHidden: section().querySelector(".b05-structure__summary").hidden,
|
||||
// 컨테이너 밖에 보이는 옵션 칸(0 이어야 함 — 부피는 컨테이너 안)
|
||||
looseFields: [...section().querySelectorAll("[data-option-keys]")].filter(
|
||||
(n) => !n.closest("[data-b05-group]") && n.offsetParent !== null,
|
||||
).length,
|
||||
stale: [...section().querySelectorAll(".b05-structure__stale input")].map((i) =>
|
||||
[i.closest(".b05-route__field")?.querySelector("span")?.textContent, i.value].join("="),
|
||||
),
|
||||
volume: volume(type)?.value,
|
||||
});
|
||||
const listItem = (re) =>
|
||||
[...document.querySelectorAll(".b05-route__irregular-list li")].find((l) =>
|
||||
re.test(l.textContent.trim()),
|
||||
);
|
||||
const open = async (re) => {
|
||||
await until(() => listItem(re));
|
||||
const li = listItem(re);
|
||||
(li.querySelector("button") ?? li).click();
|
||||
await w(800);
|
||||
// 이미 골라져 있었으면 풀림 — 한 번 더(재선택 = 해제)
|
||||
if (!selects()[1].value) (li.querySelector("button") ?? li).click();
|
||||
await until(() => !!selects()[1].value);
|
||||
await w(400);
|
||||
};
|
||||
const pickLink = async (type) => {
|
||||
box(type).querySelector('[data-b05-summary="pick"]').click();
|
||||
await until(() => document.querySelector(".ui-item-picker__tree .ui-tree__row"));
|
||||
for (let k = 0; k < 8; k++) {
|
||||
const rows = [...document.querySelectorAll(".ui-item-picker__tree .ui-tree__row")];
|
||||
const leaf = rows.find((r) => !r.classList.contains("is-branch"));
|
||||
if (leaf) {
|
||||
leaf.click();
|
||||
break;
|
||||
}
|
||||
rows.find((r) => r.querySelector(".ui-tree__caret")?.textContent === "▸")?.click();
|
||||
await w(300);
|
||||
}
|
||||
await w(300);
|
||||
[...document.querySelectorAll(".ui-item-picker__actions button")]
|
||||
.find((b) => b.textContent.includes("적용"))
|
||||
.click();
|
||||
await w(600);
|
||||
};
|
||||
const save = async () => {
|
||||
document.querySelector('[data-dock-action="save"]').click();
|
||||
await w(500);
|
||||
return until(() => !document.querySelector(".ui-overlay.is-active"), 60000);
|
||||
};
|
||||
const mine = (list) =>
|
||||
list
|
||||
.filter((s) => s.type_id in GID)
|
||||
.map((s) => ({
|
||||
type: s.type_id,
|
||||
at: [s.start_m, s.chainage_m, s.end_m],
|
||||
options: s.options,
|
||||
group_items: Object.keys(s.group_items ?? {}),
|
||||
}));
|
||||
const out = { mode: MODE };
|
||||
|
||||
if (MODE === "seed") {
|
||||
const now = await api.fetchStructures(projectId);
|
||||
sessionStorage.setItem("__529_original", JSON.stringify(now.structures));
|
||||
const old = {
|
||||
structure_id: null,
|
||||
type_id: "spoil_bank",
|
||||
placement: "interval",
|
||||
chainage_m: 370,
|
||||
start_m: 360,
|
||||
end_m: 380,
|
||||
options: {
|
||||
side: "좌",
|
||||
fill_slope_ratio: 1.5,
|
||||
extra_distance_m: 5,
|
||||
area_m2: 30,
|
||||
capacity_m3: 90,
|
||||
},
|
||||
memo: "52-9 옛 꼴",
|
||||
};
|
||||
const saved = await api.saveStructures(projectId, now.revision, [...now.structures, old]);
|
||||
for (const k of Object.keys(sessionStorage))
|
||||
if (k.includes("draft:structures")) sessionStorage.removeItem(k);
|
||||
out.seeded = mine(saved.structures ?? (await api.fetchStructures(projectId)).structures);
|
||||
}
|
||||
|
||||
if (MODE === "edit") {
|
||||
// ① 옛 꼴 사토장 — 컨테이너 하나 · 옛 칸은 지난 정의 값 · 부피 = 옛 용량 값
|
||||
await open(/^18\+0~19\+0.*사토장/);
|
||||
out.old = shape("spoil_bank");
|
||||
// ② 새 사토장 25+0 ~ 26+0(기준 25+10) · 부피 150
|
||||
section().querySelector(".b05-route__irregular-actions button:last-child").click(); // 리셋
|
||||
await w(400);
|
||||
await change(selects()[0], "E");
|
||||
await change(selects()[1], "spoil_bank");
|
||||
await station(1, 25, 0);
|
||||
await station(3, 26, 0);
|
||||
await station(2, 25, 10);
|
||||
out.newSpoilForm = shape("spoil_bank");
|
||||
await change(volume("spoil_bank"), "150");
|
||||
section().querySelector(".b05-route__irregular-actions .is-primary").click();
|
||||
await w(800);
|
||||
// ③ 새 토취장 28+0 · 부피 80
|
||||
section().querySelector(".b05-route__irregular-actions button:last-child").click();
|
||||
await w(400);
|
||||
await change(selects()[0], "E");
|
||||
await change(selects()[1], "borrow_pit");
|
||||
await station(2, 28, 0);
|
||||
out.newBorrowForm = shape("borrow_pit");
|
||||
await change(volume("borrow_pit"), "80");
|
||||
section().querySelector(".b05-route__irregular-actions .is-primary").click();
|
||||
await w(800);
|
||||
// ④ 잇기 — 새 사토장 · 토취장 컨테이너마다
|
||||
await open(/^25\+0~26\+0.*사토장/);
|
||||
await pickLink("spoil_bank");
|
||||
out.linkSpoil = linked("spoil_bank");
|
||||
await open(/^28\+0.*토취장/);
|
||||
await pickLink("borrow_pit");
|
||||
out.linkBorrow = linked("borrow_pit");
|
||||
out.saved = await save();
|
||||
out.file = mine((await api.fetchStructures(projectId)).structures);
|
||||
}
|
||||
|
||||
// 다시 연 값 — B05 에서 한 번(값을 sessionStorage 에 둠) · B06(#/b06-section)에서 한 번(같은지 견줌)
|
||||
if (MODE === "verify") {
|
||||
await until(() => document.querySelectorAll(".b05-route__irregular-list li").length > 0);
|
||||
const r = {};
|
||||
await open(/^18\+0~19\+0.*사토장/);
|
||||
r.old = shape("spoil_bank");
|
||||
await open(/^25\+0~26\+0.*사토장/);
|
||||
r.spoil = shape("spoil_bank");
|
||||
await open(/^28\+0.*토취장/);
|
||||
r.borrow = shape("borrow_pit");
|
||||
section().querySelector(".b05-route__irregular-actions button:last-child").click(); // 리셋
|
||||
out.page = location.hash;
|
||||
out.read = r;
|
||||
if (location.hash.includes("b05")) sessionStorage.setItem("__529_b05", JSON.stringify(r));
|
||||
else out.sameDrawing = sessionStorage.getItem("__529_b05") === JSON.stringify(r);
|
||||
}
|
||||
|
||||
if (MODE === "restore") {
|
||||
const original = JSON.parse(sessionStorage.getItem("__529_original") || "null");
|
||||
if (!original) return JSON.stringify({ error: "원래 값 없음" });
|
||||
const now = await api.fetchStructures(projectId);
|
||||
await api.saveStructures(projectId, now.revision, original);
|
||||
for (const k of Object.keys(sessionStorage))
|
||||
if (k.includes("draft:structures") || k.startsWith("__529_")) sessionStorage.removeItem(k);
|
||||
const after = await api.fetchStructures(projectId);
|
||||
const key = (list) =>
|
||||
JSON.stringify(
|
||||
list.map((s) => [s.type_id, s.start_m, s.chainage_m, s.end_m, s.options, s.group_items]),
|
||||
);
|
||||
out.restored = key(after.structures) === key(original);
|
||||
out.left = mine(after.structures).length;
|
||||
}
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
Reference in New Issue
Block a user