Merge remote-tracking branch 'origin/dev' into main_desktop_1
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"판": 12,
|
||||
"판": 13,
|
||||
"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`가 새 키로 옮긴다.",
|
||||
"구조물군": {
|
||||
@@ -3517,6 +3517,145 @@
|
||||
"옵션": [],
|
||||
"형상": {}
|
||||
}
|
||||
],
|
||||
"retaining_wall": [
|
||||
{
|
||||
"id": "gretaindetail",
|
||||
"이름": "상세 내용",
|
||||
"옵션": [
|
||||
"height_m",
|
||||
"length_m",
|
||||
"before_m",
|
||||
"after_m",
|
||||
"form",
|
||||
"side",
|
||||
"weep_hole_diameter_mm",
|
||||
"weep_hole_area_m2"
|
||||
],
|
||||
"형상": {}
|
||||
}
|
||||
],
|
||||
"masonry_wet": [
|
||||
{
|
||||
"id": "gmasonwetdetail",
|
||||
"이름": "상세 내용",
|
||||
"옵션": [
|
||||
"height_m",
|
||||
"length_m",
|
||||
"before_m",
|
||||
"after_m",
|
||||
"thickness_top_m",
|
||||
"thickness_bottom_m",
|
||||
"back_len_cm",
|
||||
"stone_kind",
|
||||
"stone_supply",
|
||||
"stone_coeff_basis",
|
||||
"fill_concrete_mpa",
|
||||
"face_slope_ratio",
|
||||
"face_role",
|
||||
"foundation",
|
||||
"side",
|
||||
"weep_hole_diameter_mm",
|
||||
"weep_hole_area_m2",
|
||||
"blinding_concrete"
|
||||
],
|
||||
"형상": {}
|
||||
}
|
||||
],
|
||||
"masonry_dry": [
|
||||
{
|
||||
"id": "gmasondrydetail",
|
||||
"이름": "상세 내용",
|
||||
"옵션": [
|
||||
"height_m",
|
||||
"length_m",
|
||||
"before_m",
|
||||
"after_m",
|
||||
"thickness_top_m",
|
||||
"thickness_bottom_m",
|
||||
"back_len_cm",
|
||||
"stone_kind",
|
||||
"stone_supply",
|
||||
"stone_coeff_basis",
|
||||
"fill_concrete_mpa",
|
||||
"face_slope_ratio",
|
||||
"face_role",
|
||||
"foundation",
|
||||
"side",
|
||||
"weep_hole_diameter_mm",
|
||||
"weep_hole_area_m2",
|
||||
"blinding_concrete"
|
||||
],
|
||||
"형상": {}
|
||||
}
|
||||
],
|
||||
"soil_guard": [
|
||||
{
|
||||
"id": "gsoilgdetail",
|
||||
"이름": "상세 내용",
|
||||
"옵션": [
|
||||
"height_m",
|
||||
"length_m",
|
||||
"before_m",
|
||||
"after_m",
|
||||
"form",
|
||||
"side",
|
||||
"tiers",
|
||||
"lift_m",
|
||||
"shift_m",
|
||||
"thickness_top_m",
|
||||
"thickness_bottom_m",
|
||||
"back_len_cm",
|
||||
"stone_kind",
|
||||
"stone_supply",
|
||||
"stone_coeff_basis",
|
||||
"fill_concrete_mpa",
|
||||
"face_slope_ratio",
|
||||
"foundation",
|
||||
"weep_hole_diameter_mm",
|
||||
"weep_hole_area_m2"
|
||||
],
|
||||
"형상": {}
|
||||
}
|
||||
],
|
||||
"boulder_masonry": [
|
||||
{
|
||||
"id": "gboulderdetail",
|
||||
"이름": "상세 내용",
|
||||
"옵션": [
|
||||
"height_m",
|
||||
"length_m",
|
||||
"before_m",
|
||||
"after_m",
|
||||
"stone_cm",
|
||||
"bond",
|
||||
"stone_supply",
|
||||
"stone_coeff_basis",
|
||||
"fill_concrete_mpa",
|
||||
"face_slope_ratio",
|
||||
"foundation",
|
||||
"side",
|
||||
"weep_hole_diameter_mm",
|
||||
"weep_hole_area_m2",
|
||||
"blinding_concrete"
|
||||
],
|
||||
"형상": {}
|
||||
}
|
||||
],
|
||||
"berm": [
|
||||
{
|
||||
"id": "gbermdetail",
|
||||
"이름": "상세 내용",
|
||||
"옵션": [
|
||||
"length_m",
|
||||
"before_m",
|
||||
"after_m",
|
||||
"width_m",
|
||||
"interval_m",
|
||||
"slope_deg"
|
||||
],
|
||||
"형상": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"배치": {},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
"""PLAN 54-6 — 사면안정(C군) 종류마다 컨테이너 「상세 내용」(정의 그룹) 하나.
|
||||
|
||||
· 기준 측점(측점 범위) 밖 옵션 전부를 담음 · 옵션 키 · 차례 그대로
|
||||
· 잇기 = 그룹 id(`group_items`) — 종류 단위 잇기 줄은 가림(hidesTypeSummary = 그룹 있음)
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from B05_Profile.B05_Profile_Structures_Schema import DEFINITION_PATH, load_structure_types
|
||||
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 = {
|
||||
"retaining_wall": "gretaindetail",
|
||||
"masonry_wet": "gmasonwetdetail",
|
||||
"masonry_dry": "gmasondrydetail",
|
||||
"soil_guard": "gsoilgdetail",
|
||||
"boulder_masonry": "gboulderdetail",
|
||||
"berm": "gbermdetail",
|
||||
}
|
||||
|
||||
|
||||
def test_C군_종류마다_컨테이너_하나_옵션_전부():
|
||||
types = [t for t in load_structure_types() if t.group == "C"]
|
||||
assert sorted(t.type_id for t in types) == sorted(KINDS)
|
||||
groups = groups_of()
|
||||
for item in types:
|
||||
assert groups[item.type_id] == [
|
||||
{
|
||||
"id": KINDS[item.type_id],
|
||||
"name": "상세 내용",
|
||||
"options": [o.key for o in item.options],
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
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 = {"layer": "system", "owner": None, "key": "SS000001", "name": "옹벽"}
|
||||
rows = _with_group_items(
|
||||
[
|
||||
{
|
||||
"type_id": "retaining_wall",
|
||||
"복사본": "s1",
|
||||
"summary_item": None,
|
||||
"group_items": {"gretaindetail": link},
|
||||
}
|
||||
]
|
||||
)
|
||||
assert [r["복사본"] for r in rows] == ["s1", "s1__gretaindetail"]
|
||||
assert rows[1]["summary_item"] == link and rows[1]["그룹"] == "상세 내용"
|
||||
@@ -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"]
|
||||
|
||||
@@ -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);
|
||||
})();
|
||||
@@ -0,0 +1,198 @@
|
||||
/* 54-6 — B05 · B06 좌측 「구조물 배치」 사면안정(C군) 종류마다 컨테이너 「상세 내용」 하나
|
||||
* 시작 화면: B05 종단설계(#/b05-profile) · 검증 프로젝트(랩탑_보조)
|
||||
* 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m546 = "단계";` 를 붙여
|
||||
* seed → 원래 구조물 목록을 sessionStorage 에 두고 흙막이 · 소단을 심고 옹벽에 옛 종류 단위 잇기를 얹음(PUT) · 그 뒤 orca reload
|
||||
* check → C군 종류마다 컨테이너 1 · 그 안 칸 수 = 그리는 칸 수 · 밖 칸 0 · 잇기 1 · 종류 단위 줄 가림 ·
|
||||
* 옹벽 컨테이너 잇기 = 옛 값 — B05 에서 한 번(값을 sessionStorage 에 둠) · B06(#/b06-section) 에서 한 번(B05 = B06)
|
||||
* save → 옹벽 [수정] · [저장] — 옛 값이 컨테이너 잇기(group_items)로 옮겨 저장 · 그 뒤 orca reload
|
||||
* verify → 다시 연 옹벽 컨테이너 잇기 = 옛 값 · 파일 = group_items 만
|
||||
* restore → 원래 구조물 목록으로 되돌림(PUT) · 초안 비움
|
||||
* 쓰는 것: 검증 프로젝트 structures.json(되돌림 단계가 원래 값으로 다시 씀)
|
||||
*/
|
||||
(async () => {
|
||||
const MODE = window.__m546 || "check";
|
||||
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 OLD = { layer: "system", owner: null, key: "SS000001", name: "54-6 옛 잇기" };
|
||||
const KINDS = {
|
||||
retaining_wall: ["gretaindetail", /^10\+0~10\+10옹벽/],
|
||||
masonry_wet: ["gmasonwetdetail", /^4\+0~4\+10돌쌓기\(찰\)/],
|
||||
masonry_dry: ["gmasondrydetail", /^7\+0~7\+10돌쌓기\(메\)/],
|
||||
soil_guard: ["gsoilgdetail", /흙막이/],
|
||||
boulder_masonry: ["gboulderdetail", /^16\+0~16\+10큰돌쌓기/],
|
||||
berm: ["gbermdetail", /소단/],
|
||||
};
|
||||
const section = () => document.querySelector(".b05-structure-section");
|
||||
const selects = () => section().querySelectorAll(".b05-structure__grid select");
|
||||
const visible = (n) => n.offsetParent !== null;
|
||||
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 reset = async () => {
|
||||
section().querySelector(".b05-route__irregular-actions button:last-child").click();
|
||||
await w(400);
|
||||
};
|
||||
const b06 = location.hash.includes("b06");
|
||||
const types = Object.fromEntries((await api.fetchStructureTypes()).map((t) => [t.type_id, t]));
|
||||
/** 그리는 칸 = 정의 옵션 중 이 화면 몫(B05 = b05 · B06 = 상세까지) · 켬 */
|
||||
const expected = (type) =>
|
||||
types[type].options
|
||||
.filter((o) => o.visible !== false && o.enabled !== false)
|
||||
.filter((o) => b06 || api.isB05Option(o))
|
||||
.map((o) => o.key);
|
||||
const shape = (type) => {
|
||||
const boxes = [...section().querySelectorAll("[data-b05-group]")].filter(visible);
|
||||
const box = boxes[0];
|
||||
const inside = box ? [...box.querySelectorAll("[data-option-keys]")] : [];
|
||||
return {
|
||||
boxes: boxes.map((b) => `${b.dataset.b05Group}:${b.querySelector("legend")?.textContent}`),
|
||||
fields: inside.map((n) => n.dataset.optionKeys),
|
||||
want: expected(type),
|
||||
loose: [...section().querySelectorAll("[data-option-keys]")].filter(
|
||||
(n) => !n.closest("[data-b05-group]") && visible(n),
|
||||
).length,
|
||||
links: box ? box.querySelectorAll(".b05-structure__summary").length : 0,
|
||||
typeRows: [...section().querySelectorAll(".b05-structure__summary")].filter(
|
||||
(n) => !n.closest("[data-b05-group]") && visible(n),
|
||||
).length,
|
||||
link: box?.querySelector(".b05-structure__summary-name")?.textContent.trim(),
|
||||
};
|
||||
};
|
||||
const ok = (type, s) =>
|
||||
s.boxes.length === 1 &&
|
||||
s.boxes[0] === `${KINDS[type][0]}:상세 내용` &&
|
||||
JSON.stringify(s.fields) === JSON.stringify(s.want) &&
|
||||
s.loose === 0 &&
|
||||
s.links === 1 &&
|
||||
s.typeRows === 0;
|
||||
const save = async () => {
|
||||
document.querySelector('[data-dock-action="save"]').click();
|
||||
await w(500);
|
||||
return until(() => !document.querySelector(".ui-overlay.is-active"), 60000);
|
||||
};
|
||||
const out = { mode: MODE, page: location.hash };
|
||||
|
||||
if (MODE === "seed") {
|
||||
const now = await api.fetchStructures(projectId);
|
||||
sessionStorage.setItem("__546_original", JSON.stringify(now.structures));
|
||||
const add = (type_id, at, options) => ({
|
||||
structure_id: null,
|
||||
type_id,
|
||||
placement: "interval",
|
||||
chainage_m: at,
|
||||
start_m: at,
|
||||
end_m: at + 10,
|
||||
options: { length_m: 10, before_m: 0, after_m: 10, ...options },
|
||||
memo: "54-6",
|
||||
});
|
||||
const next = now.structures.map((s) =>
|
||||
s.type_id === "retaining_wall" ? { ...s, summary_item: OLD } : s,
|
||||
);
|
||||
next.push(add("soil_guard", 500, { height_m: 1.5 }), add("berm", 560, { width_m: 1 }));
|
||||
await api.saveStructures(projectId, now.revision, next);
|
||||
for (const k of Object.keys(sessionStorage))
|
||||
if (k.includes("draft:structures")) sessionStorage.removeItem(k);
|
||||
const after = (await api.fetchStructures(projectId)).structures;
|
||||
out.seeded = after.filter((s) => s.type_id in KINDS).map((s) => [s.type_id, !!s.summary_item]);
|
||||
}
|
||||
|
||||
if (MODE === "check") {
|
||||
await until(() => document.querySelectorAll(".b05-route__irregular-list li").length > 0);
|
||||
const r = {};
|
||||
for (const [type, [, re]] of Object.entries(KINDS)) {
|
||||
await open(re);
|
||||
r[type] = shape(type);
|
||||
r[type].ok = ok(type, r[type]);
|
||||
}
|
||||
await reset();
|
||||
out.allOk = Object.values(r).every((s) => s.ok);
|
||||
out.oldLink = r.retaining_wall.link;
|
||||
out.read = r;
|
||||
if (!b06) sessionStorage.setItem("__546_b05", JSON.stringify(r));
|
||||
else {
|
||||
// B06 은 상세 칸까지 그림 — 같은 그리기 판정은 컨테이너 · 잇기 · 밖 칸 · 옛 값
|
||||
const strip = (x) =>
|
||||
JSON.stringify(
|
||||
Object.fromEntries(
|
||||
Object.entries(x).map(([k, s]) => [
|
||||
k,
|
||||
[s.boxes, s.loose, s.links, s.typeRows, s.link, s.ok],
|
||||
]),
|
||||
),
|
||||
);
|
||||
out.sameDrawing = strip(JSON.parse(sessionStorage.getItem("__546_b05") || "{}")) === strip(r);
|
||||
}
|
||||
}
|
||||
|
||||
if (MODE === "save") {
|
||||
await open(KINDS.retaining_wall[1]);
|
||||
out.before = shape("retaining_wall").link;
|
||||
section().querySelector(".b05-route__irregular-actions .is-primary").click(); // [수정]
|
||||
await w(800);
|
||||
out.saved = await save();
|
||||
const wall = (await api.fetchStructures(projectId)).structures.find(
|
||||
(s) => s.type_id === "retaining_wall",
|
||||
);
|
||||
out.file = { summary_item: wall.summary_item ?? null, group_items: wall.group_items ?? null };
|
||||
}
|
||||
|
||||
if (MODE === "verify") {
|
||||
await open(KINDS.retaining_wall[1]);
|
||||
out.link = shape("retaining_wall").link;
|
||||
await reset();
|
||||
const wall = (await api.fetchStructures(projectId)).structures.find(
|
||||
(s) => s.type_id === "retaining_wall",
|
||||
);
|
||||
out.fileOk =
|
||||
!wall.summary_item &&
|
||||
JSON.stringify(wall.group_items) === JSON.stringify({ gretaindetail: OLD });
|
||||
}
|
||||
|
||||
if (MODE === "restore") {
|
||||
const original = JSON.parse(sessionStorage.getItem("__546_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("__546_")) 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.summary_item ?? null,
|
||||
s.group_items ?? null,
|
||||
]),
|
||||
);
|
||||
out.restored = key(after.structures) === key(original);
|
||||
}
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
Reference in New Issue
Block a user