feat(B05): 사토장 기준 측점 = 토취장과 같은 꼴(54-7) — 기준 측점 한 점 · B06 사토 배분 이어 씀
- 정의 사토장(spoil_bank) 배치 구간 → 부지 · 정의 판 14(이력 판_13) · 화면은 토취장 길 그대로(기준 측점 No + 거리) - 옛 구간 저장분 = 읽을 때 기준 측점(옛 기준 → 시작) · 시작 · 종료 뺌 · 파일은 다음 [저장]까지 그대로 - B06 사토 배분 = 기준 측점에 가장 가까운 측점 하나(가운데면 앞 측점) × 대표 길이(앞뒤 절반) - 바뀌는 값: 담은 양 같음 · 쌓는 측점 구간 여럿 → 하나 · 그 측점 폭 · 면적 커짐 - 시험 test_b06_spoil_fill_allocation(기준 측점 꼴 · 옛 꼴 읽기 · 가까운 측점) · test_52_9 · 화면 ui_checks/54-7 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7
This commit is contained in:
@@ -201,6 +201,24 @@ class StructureInstance(BaseModel):
|
||||
def validate_group_items(cls, value: Any) -> dict[str, Any] | None:
|
||||
return parse_group_items(value)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def follow_site_definition(cls, data: Any) -> Any:
|
||||
"""정의가 부지형인데 옛 구간형으로 적힌 저장분(사토장 · PLAN 54-7) — 기준 측점 하나로 읽음.
|
||||
|
||||
기준 측점이 없으면 옛 기준 → 시작 차례. 파일의 옛 값은 [저장] 전까지 그대로.
|
||||
"""
|
||||
if not isinstance(data, dict) or data.get("placement") != "interval":
|
||||
return data
|
||||
definition = structure_type_map().get(str(data.get("type_id")))
|
||||
if definition is None or definition.placement != "site":
|
||||
return data
|
||||
chainage = data.get("chainage_m")
|
||||
if chainage is None:
|
||||
chainage = data.get("start_m")
|
||||
rest = {key: value for key, value in data.items() if key not in ("start_m", "end_m")}
|
||||
return {**rest, "placement": "site", "chainage_m": chainage}
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_placement_fields(self) -> "StructureInstance":
|
||||
if self.placement == "interval":
|
||||
|
||||
@@ -37,32 +37,35 @@ _SIDE_AUTO = "자동(성토 쪽)"
|
||||
_SIDE_WORDS = {"좌": "left", "우": "right"}
|
||||
|
||||
|
||||
def _sections_in(cross_sections: list[dict[str, Any]], start_m: float, end_m: float) -> list[dict]:
|
||||
"""구간 안에 든 측점만. **새 측점을 만들지 않는다**(2026-09-09 사용자 확정 ③)."""
|
||||
picked = []
|
||||
for section in cross_sections:
|
||||
chainage = section.get("chainage_m")
|
||||
if chainage is None:
|
||||
continue
|
||||
value = float(chainage)
|
||||
if start_m - 1e-6 <= value <= end_m + 1e-6:
|
||||
picked.append(section)
|
||||
return sorted(picked, key=lambda item: float(item["chainage_m"]))
|
||||
def _section_at(cross_sections: list[dict[str, Any]], chainage_m: float) -> list[dict]:
|
||||
"""기준 측점에 가장 가까운 측점 하나(PLAN 54-7 · 가운데면 앞 측점).
|
||||
|
||||
|
||||
def _spans(sections: list[dict[str, Any]], start_m: float, end_m: float) -> list[float]:
|
||||
"""측점마다 대표 길이(m) — 앞뒤 측점과의 절반씩. 구간 끝은 경계까지만."""
|
||||
spans: list[float] = []
|
||||
for index, section in enumerate(sections):
|
||||
chainage = float(section["chainage_m"])
|
||||
left = float(sections[index - 1]["chainage_m"]) if index else max(start_m, chainage)
|
||||
right = (
|
||||
float(sections[index + 1]["chainage_m"])
|
||||
if index + 1 < len(sections)
|
||||
else min(end_m, chainage)
|
||||
**새 측점을 만들지 않는다**(확정 ③).
|
||||
"""
|
||||
placed = [section for section in cross_sections if section.get("chainage_m") is not None]
|
||||
if not placed:
|
||||
return []
|
||||
return [
|
||||
min(
|
||||
placed,
|
||||
key=lambda item: (
|
||||
abs(float(item["chainage_m"]) - chainage_m),
|
||||
float(item["chainage_m"]),
|
||||
),
|
||||
)
|
||||
spans.append(max((chainage - left) / 2 + (right - chainage) / 2, 0.0))
|
||||
return spans
|
||||
]
|
||||
|
||||
|
||||
def _span_of(cross_sections: list[dict[str, Any]], section: dict[str, Any]) -> float:
|
||||
"""그 측점의 대표 길이(m) — 앞뒤 측점과의 절반씩. 끝 측점은 한쪽만."""
|
||||
chainages = sorted(
|
||||
{float(item["chainage_m"]) for item in cross_sections if item.get("chainage_m") is not None}
|
||||
)
|
||||
here = float(section["chainage_m"])
|
||||
index = chainages.index(here)
|
||||
left = chainages[index - 1] if index else here
|
||||
right = chainages[index + 1] if index + 1 < len(chainages) else here
|
||||
return (right - left) / 2
|
||||
|
||||
|
||||
def _has_spoil_value(design: dict[str, Any]) -> bool:
|
||||
@@ -92,7 +95,10 @@ def _side_of(design: dict[str, Any], option: Any) -> str | None:
|
||||
|
||||
|
||||
def spoil_sites(structures: list[Any]) -> list[dict[str, Any]]:
|
||||
"""배치된 사토장만 골라 쓰기 좋은 모양으로. 구간·용량이 없으면 뺀다."""
|
||||
"""배치된 사토장만 골라 쓰기 좋은 모양으로. 기준 측점·용량이 없으면 뺀다.
|
||||
|
||||
기준 측점이 없는 옛 구간 저장분은 시작 측점으로 읽음(PLAN 54-7).
|
||||
"""
|
||||
sites: list[dict[str, Any]] = []
|
||||
for item in structures:
|
||||
type_id = getattr(item, "type_id", None) or (
|
||||
@@ -104,13 +110,13 @@ def spoil_sites(structures: list[Any]) -> list[dict[str, Any]]:
|
||||
if options is None and isinstance(item, dict):
|
||||
options = item.get("options")
|
||||
options = options or {}
|
||||
start = getattr(item, "start_m", None)
|
||||
end = getattr(item, "end_m", None)
|
||||
chainage = getattr(item, "chainage_m", None)
|
||||
if isinstance(item, dict):
|
||||
start = item.get("start_m")
|
||||
end = item.get("end_m")
|
||||
chainage = item.get("chainage_m")
|
||||
if chainage is None:
|
||||
chainage = item.get("start_m")
|
||||
capacity = options.get("capacity_m3")
|
||||
if start is None or end is None or capacity in (None, ""):
|
||||
if chainage is None or capacity in (None, ""):
|
||||
continue
|
||||
try:
|
||||
capacity_value = float(capacity)
|
||||
@@ -122,8 +128,7 @@ def spoil_sites(structures: list[Any]) -> list[dict[str, Any]]:
|
||||
{
|
||||
"structure_id": getattr(item, "structure_id", None)
|
||||
or (item.get("structure_id") if isinstance(item, dict) else None),
|
||||
"start_m": min(float(start), float(end)),
|
||||
"end_m": max(float(start), float(end)),
|
||||
"chainage_m": float(chainage),
|
||||
"capacity_m3": capacity_value,
|
||||
"side_option": options.get("side"),
|
||||
"slope_ratio_n": options.get("fill_slope_ratio"),
|
||||
@@ -223,7 +228,7 @@ def enforce_spoil_fills(
|
||||
# (노선 성토도 그때 원래 값으로 돌아온다 — 사토장 몫을 빼 두었기 때문이다.)
|
||||
covered: set[float] = set()
|
||||
for site in sites:
|
||||
for section in _sections_in(cross_sections, site["start_m"], site["end_m"]):
|
||||
for section in _section_at(cross_sections, site["chainage_m"]):
|
||||
covered.add(float(section["chainage_m"]))
|
||||
for section in cross_sections:
|
||||
design = section.get("design")
|
||||
@@ -256,10 +261,10 @@ def enforce_spoil_fills(
|
||||
if not sites:
|
||||
return changed
|
||||
for site in sites:
|
||||
sections = _sections_in(cross_sections, site["start_m"], site["end_m"])
|
||||
sections = _section_at(cross_sections, site["chainage_m"])
|
||||
if not sections:
|
||||
continue
|
||||
spans = _spans(sections, site["start_m"], site["end_m"])
|
||||
spans = [_span_of(cross_sections, section) for section in sections]
|
||||
sides = [_side_of(section.get("design") or {}, site["side_option"]) for section in sections]
|
||||
ratio = site["slope_ratio_n"]
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"판": 13,
|
||||
"판": 14,
|
||||
"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`가 새 키로 옮긴다.",
|
||||
"구조물군": {
|
||||
@@ -2917,7 +2917,7 @@
|
||||
"type_id": "spoil_bank",
|
||||
"group": "E",
|
||||
"name": "유용토운반작업장(사토장)",
|
||||
"placement": "interval",
|
||||
"placement": "site",
|
||||
"style": {
|
||||
"color": "#8c9a2e",
|
||||
"abbr": "사토장"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -72,3 +72,19 @@ def test_B06_사토_배분은_부피를_그대로_읽음_옛_값도():
|
||||
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
|
||||
|
||||
|
||||
def test_54_7_사토장_기준_측점_꼴은_토취장과_같음_옛_구간은_기준_측점으로_읽음():
|
||||
from B05_Profile.B05_Profile_Structures_Schema import StructureInstance
|
||||
|
||||
by_id = {t.type_id: t for t in load_structure_types()}
|
||||
assert by_id["spoil_bank"].placement == by_id["borrow_pit"].placement == "site"
|
||||
old = {"type_id": "spoil_bank", "placement": "interval", "start_m": 60, "end_m": 80}
|
||||
for extra, station in (({"chainage_m": 70}, 70.0), ({}, 60.0)):
|
||||
item = StructureInstance.model_validate({**old, **extra})
|
||||
assert (item.placement, item.chainage_m, item.start_m, item.end_m) == (
|
||||
"site",
|
||||
station,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
① 용량이 커질수록 폭이 넓어지고, 상한에서 멈추면 못 담은 몫이 값으로 남는다.
|
||||
② 사토장이 선 측점은 **노선 성토가 줄고** 그만큼이 사토장 몫으로 간다(두 번 세지 않기).
|
||||
③ 「자동」이면 그 측점의 **성토 쪽**에 선다.
|
||||
④ 구간 밖 측점은 안 건드린다 — **새 측점을 만들지 않는다**.
|
||||
④ 기준 측점 밖 측점은 안 건드린다 — **새 측점을 만들지 않는다**.
|
||||
⑤ 사토장 = 기준 측점 한 점(PLAN 54-7) — 가장 가까운 측점 하나 · 옛 구간 저장분은 시작 측점.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -65,9 +66,8 @@ def _write_site(root: Path, capacity_m3: float, **options) -> None:
|
||||
{
|
||||
"structure_id": "sp-1",
|
||||
"type_id": "spoil_bank",
|
||||
"placement": "interval",
|
||||
"start_m": 100.0,
|
||||
"end_m": 160.0,
|
||||
"placement": "site",
|
||||
"chainage_m": 120.0,
|
||||
"options": {"capacity_m3": capacity_m3, **options},
|
||||
}
|
||||
],
|
||||
@@ -86,15 +86,15 @@ def _run(root: Path, capacity_m3: float, **options) -> list[dict]:
|
||||
|
||||
|
||||
def _placed(sections: list[dict]) -> float:
|
||||
inside = [s for s in sections if 100.0 <= s["chainage_m"] <= 160.0]
|
||||
inside = [s for s in sections if s["chainage_m"] == 120.0]
|
||||
return float(inside[0]["design"].get("spoil_fill_placed_m3") or 0.0)
|
||||
|
||||
|
||||
def test_용량이_클수록_폭이_넓어진다(tmp_path: Path) -> None:
|
||||
small = _run(tmp_path, 200.0)
|
||||
large = _run(tmp_path, 800.0)
|
||||
narrow = small[1]["design"]["spoil_fill_width_m"]
|
||||
wide = large[1]["design"]["spoil_fill_width_m"]
|
||||
narrow = small[2]["design"]["spoil_fill_width_m"]
|
||||
wide = large[2]["design"]["spoil_fill_width_m"]
|
||||
assert 0 < narrow < wide
|
||||
assert _placed(small) == pytest.approx(200.0, rel=0.02)
|
||||
assert _placed(large) == pytest.approx(800.0, rel=0.02)
|
||||
@@ -103,7 +103,7 @@ def test_용량이_클수록_폭이_넓어진다(tmp_path: Path) -> None:
|
||||
def test_담을_수_없으면_남은_몫이_값으로_남는다(tmp_path: Path) -> None:
|
||||
"""상한에서 멈춘다 — 임의로 더 넓히지 않는다."""
|
||||
sections = _run(tmp_path, 1_000_000.0)
|
||||
design = sections[1]["design"]
|
||||
design = sections[2]["design"]
|
||||
assert design["spoil_fill_unplaced_m3"] > 0
|
||||
assert design["spoil_fill_width_m"] == pytest.approx(design["spoil_fill_max_width_m"])
|
||||
|
||||
@@ -123,16 +123,16 @@ def test_노선_성토가_그만큼_줄어든다(tmp_path: Path) -> None:
|
||||
def test_자동이면_성토_쪽에_선다(tmp_path: Path) -> None:
|
||||
sections = _run(tmp_path, 400.0, side="자동(성토 쪽)")
|
||||
# right_cut = 우측 절토 ⇒ 성토는 좌측(+offset).
|
||||
assert sections[1]["design"]["spoil_fill_side"] == "left"
|
||||
assert sections[2]["design"]["spoil_fill_side"] == "left"
|
||||
|
||||
|
||||
def test_구간_밖_측점은_안_건드린다(tmp_path: Path) -> None:
|
||||
"""새 측점을 만들지 않고, 구간 밖은 종전 그대로다(확정 ③)."""
|
||||
def test_기준_측점_밖_측점은_안_건드린다(tmp_path: Path) -> None:
|
||||
"""새 측점을 만들지 않고, 기준 측점 밖은 종전 그대로다(확정 ③)."""
|
||||
sections = _run(tmp_path, 400.0)
|
||||
assert len(sections) == 6
|
||||
for section in sections:
|
||||
design = section["design"]
|
||||
if 100.0 <= section["chainage_m"] <= 160.0:
|
||||
if section["chainage_m"] == 120.0:
|
||||
assert design["spoil_fill_area_m2"] > 0
|
||||
else:
|
||||
assert design.get("spoil_fill_area_m2", 0) == 0
|
||||
@@ -186,3 +186,62 @@ def test_사토장을_지우면_값도_사라진다(tmp_path: Path) -> None:
|
||||
assert design.get("spoil_fill_capacity_m3") in (None, 0), section["chainage_m"]
|
||||
# 노선 성토도 원래 값으로 돌아온다 — 사토장 몫을 빼 두었던 것이 복구된다.
|
||||
assert sections[2]["design"]["fill_area_m2"] == pytest.approx(plain_fill, abs=1e-6)
|
||||
|
||||
|
||||
def _write_raw(root: Path, item: dict) -> None:
|
||||
(root / "B05_Profile" / "route").mkdir(parents=True, exist_ok=True)
|
||||
(root / "B05_Profile" / "route" / "structures.json").write_text(
|
||||
json.dumps({"revision": 1, "structures": [item]}, ensure_ascii=False), encoding="utf-8"
|
||||
)
|
||||
|
||||
|
||||
def _spoiled(sections: list[dict]) -> list[float]:
|
||||
return [
|
||||
s["chainage_m"] for s in sections if float(s["design"].get("spoil_fill_area_m2") or 0) > 0
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("legacy", "station"),
|
||||
[
|
||||
({"chainage_m": 140.0}, 140.0), # 옛 기준이 있으면 옛 기준
|
||||
({}, 100.0), # 없으면 시작
|
||||
],
|
||||
)
|
||||
def test_옛_구간_저장분은_옛_기준_시작_차례로_읽는다(
|
||||
tmp_path: Path, legacy: dict, station: float
|
||||
) -> None:
|
||||
_write_raw(
|
||||
tmp_path,
|
||||
{
|
||||
"structure_id": "sp-1",
|
||||
"type_id": "spoil_bank",
|
||||
"placement": "interval",
|
||||
"start_m": 100.0,
|
||||
"end_m": 160.0,
|
||||
"options": {"capacity_m3": 200.0},
|
||||
**legacy,
|
||||
},
|
||||
)
|
||||
sections = _sections([80.0, 100.0, 120.0, 140.0, 160.0, 180.0])
|
||||
enforce_spoil_fills(_LONGITUDINAL, sections, tmp_path)
|
||||
assert _spoiled(sections) == [station]
|
||||
|
||||
|
||||
def test_기준_측점_플러스_거리는_가장_가까운_측점에_선다(tmp_path: Path) -> None:
|
||||
_write_raw(
|
||||
tmp_path,
|
||||
{
|
||||
"structure_id": "sp-1",
|
||||
"type_id": "spoil_bank",
|
||||
"placement": "site",
|
||||
"chainage_m": 127.0,
|
||||
"options": {"capacity_m3": 200.0},
|
||||
},
|
||||
)
|
||||
sections = _sections([80.0, 100.0, 120.0, 140.0, 160.0, 180.0])
|
||||
enforce_spoil_fills(_LONGITUDINAL, sections, tmp_path)
|
||||
assert _spoiled(sections) == [120.0]
|
||||
# 대표 길이 = 앞뒤 측점 절반씩 20 m — 부피 = 면적 × 20
|
||||
design = sections[2]["design"]
|
||||
assert design["spoil_fill_placed_m3"] == pytest.approx(design["spoil_fill_area_m2"] * 20.0)
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
/* 54-7 — B05 · B06 좌측 「구조물 배치」 사토장 기준 측점 = 토취장과 같은 꼴(52-9 시나리오 고침)
|
||||
* 시작 화면: B05 종단설계(#/b05-profile) · 검증 프로젝트(sub7_검증)
|
||||
* 앞 준비: 검증 프로젝트 structures.json 에 옛 꼴(구간 18+0~19+0 · 기준 18+10 · 부피 90) 사토장을
|
||||
* 파일로 직접 심음(API 로 보내면 서버가 새 꼴로 바꿔 적음) · 구조물 초안 비움 · orca reload
|
||||
* 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m547 = "단계";` 를 붙여
|
||||
* edit → 옛 사토장 · 새 사토장(25+10 · 150) · 토취장(28+0 · 80) 위치 줄 꼴 · 잇기 · [저장] · 그 뒤 orca reload
|
||||
* verify → 다시 연 값 — B05 에서 한 번 · B06(#/b06-section) 에서 한 번(B05 = B06)
|
||||
* 되돌림: 앞 준비 때 떠 둔 structures.json 을 다시 씀 · 초안 비움
|
||||
* 기대 수치: 위치 줄 = 「기준 측점」 칸 하나(No + 거리 입력 둘) — 사토장 = 토취장 · 시작 · 종료 칸 숨김 ·
|
||||
* 옛 사토장 = 18+10(옛 기준) · 부피 90 · 저장 뒤 정본 = site · chainage 370 / 510 / 560
|
||||
*/
|
||||
(async () => {
|
||||
const MODE = window.__m547 || "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 row = () => section().querySelector(".b05-structure__position-row");
|
||||
const station = async (index, no, plus) => {
|
||||
const [s, r] = row().children[index].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 position = () =>
|
||||
[...row().children]
|
||||
.filter((c) => !c.hidden && c.offsetParent !== null)
|
||||
.map((c) => ({
|
||||
label: c.querySelector("span")?.textContent ?? "",
|
||||
inputs: c.querySelectorAll("input").length,
|
||||
value: [...c.querySelectorAll("input")].map((i) => i.value).join("+"),
|
||||
}));
|
||||
const shape = (type) => ({
|
||||
position: position(),
|
||||
boxes: [...section().querySelectorAll("[data-b05-group]")].map((b) => b.dataset.b05Group),
|
||||
volume: volume(type)?.value,
|
||||
linked: linked(type),
|
||||
});
|
||||
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);
|
||||
if (!li) return false;
|
||||
(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);
|
||||
return true;
|
||||
};
|
||||
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,
|
||||
placement: s.placement,
|
||||
at: [s.start_m, s.chainage_m, s.end_m],
|
||||
capacity: s.options?.capacity_m3,
|
||||
group_items: Object.keys(s.group_items ?? {}),
|
||||
}));
|
||||
const out = { mode: MODE };
|
||||
|
||||
if (MODE === "edit") {
|
||||
out.listOld = listItem(/사토장/)?.textContent.trim();
|
||||
// ① 옛 꼴 사토장 — 기준 측점 하나(옛 기준 18+10)
|
||||
out.openedOld = await open(/^18\+10.*사토장/);
|
||||
out.old = shape("spoil_bank");
|
||||
// ② 새 사토장 기준 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");
|
||||
out.newSpoilForm = shape("spoil_bank");
|
||||
const anchor = [...row().children].findIndex((c) => !c.hidden && c.offsetParent !== null);
|
||||
await station(anchor, 25, 10);
|
||||
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");
|
||||
out.newBorrowForm = shape("borrow_pit");
|
||||
await station(anchor, 28, 0);
|
||||
await change(volume("borrow_pit"), "80");
|
||||
section().querySelector(".b05-route__irregular-actions .is-primary").click();
|
||||
await w(800);
|
||||
out.sameShape =
|
||||
JSON.stringify(out.newSpoilForm.position.map((p) => [p.label, p.inputs])) ===
|
||||
JSON.stringify(out.newBorrowForm.position.map((p) => [p.label, p.inputs]));
|
||||
// ④ 잇기
|
||||
await open(/^25\+10.*사토장/);
|
||||
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);
|
||||
}
|
||||
|
||||
if (MODE === "verify") {
|
||||
await until(() => document.querySelectorAll(".b05-route__irregular-list li").length > 0);
|
||||
const r = {};
|
||||
await open(/^18\+10.*사토장/);
|
||||
r.old = shape("spoil_bank");
|
||||
await open(/^25\+10.*사토장/);
|
||||
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("__547_b05", JSON.stringify(r));
|
||||
else out.sameDrawing = sessionStorage.getItem("__547_b05") === JSON.stringify(r);
|
||||
}
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
Reference in New Issue
Block a user