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

This commit is contained in:
2026-10-06 13:02:22 +09:00
7 changed files with 4128 additions and 79 deletions
@@ -30,8 +30,8 @@ import { solveSplit, specKeys, type SplitRole } from "../A00_Common/design/desig
export const hidesTypeSummary = (type: StructureType): boolean =>
!!STRUCTURE_SPECS[type.type_id]?.containers.length || !!type.groups?.length || type.group === "B";
/** 길이 = 전 + 후 를 배관 맞물림(`solveSplit` · 마지막에 고친 둘을 지킴)으로 푸는 종류(표 07 가드레일). */
const SOLVE_SPLIT = new Set(["guardrail"]);
/** 길이 = 전 + 후 를 배관 맞물림(`solveSplit` · 마지막에 고친 둘을 지킴)으로 푸는 종류(표 07 가드레일 · 표 09 흙막이 · 옹벽). */
const SOLVE_SPLIT = new Set(["guardrail", "soil_guard", "retaining_wall"]);
const SPLIT_ROLE: Record<string, SplitRole> = {
length_m: "length",
before_m: "before",
@@ -159,6 +159,22 @@ export function renderOptionFields(
input.addEventListener("change", () => refresh(true));
}
});
// 숨긴 길이 · 전 · 후(표 09 소단) — 칸은 안 그리고 값만 쥠: 범위 계산 · 저장이 옛 값 · 기본값을 이어 씀(54-26)
for (const option of type.options) {
if (!SPLIT_ROLE[option.key] || option.visible !== false) continue;
const input = document.createElement("input");
input.value = values[option.key] === undefined ? "" : String(values[option.key]);
all.push({
wrap: document.createElement("div"),
entry: {
key: option.key,
required: false,
input,
read: () => Number(input.value) || 0,
isEmpty: () => input.value.trim() === "",
},
});
}
/** 길이 · 전 · 후 맞물림 — 고친 차례(최근이 앞)를 쥐고 나머지 하나를 셈. */
const recent: SplitRole[] = [];
function solveInto(key: string): void {
+111 -75
View File
@@ -1,5 +1,5 @@
{
"판": 14,
"판": 15,
"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`가 새 키로 옮긴다.",
"구조물군": {
@@ -975,15 +975,15 @@
],
"options": [
{
"key": "height_m",
"label": "높이",
"input": "number",
"unit": "m",
"default": 2,
"default_basis": "기본값 · 소광리 도면 H=2.0 · 바꿀 수 있음",
"required": false,
"phase": "b05",
"empty_means": "비워도 놓임 — 줄만 서고 미확정이라 금액에 안 들어감(높이를 적으면 섬 · 2026-09-14 브레인 판정 ①)"
"key": "wall_form",
"label": "형태",
"input": "select",
"choices": [
"콘크리트"
],
"default": "콘크리트",
"default_basis": "표 09 — 선택지 하나(콘크리트)",
"required": false
},
{
"key": "length_m",
@@ -994,6 +994,17 @@
"required": false,
"phase": "b05"
},
{
"key": "height_m",
"label": "높이",
"input": "number",
"unit": "m",
"default": 2,
"default_basis": "기본값 · 소광리 도면 H=2.0 · 바꿀 수 있음",
"required": false,
"phase": "b05",
"empty_means": "비워도 놓임 — 줄만 서고 미확정이라 금액에 안 들어감(높이를 적으면 섬 · 2026-09-14 브레인 판정 ①)"
},
{
"key": "before_m",
"label": "기준측점 전",
@@ -1525,13 +1536,20 @@
],
"options": [
{
"key": "height_m",
"label": "높이",
"input": "number",
"unit": "m",
"default": 2.5,
"key": "form",
"label": "형태",
"input": "select",
"choices": [
"돌쌓기(찰)",
"돌쌓기(메)",
"콘크리트",
"돌망태",
"통나무·목재틀",
"바자"
],
"default": null,
"required": false,
"phase": "b05"
"empty_means": "비우면 통나무·목재틀로 그림(횡단 기본 형태) — B05 로 옮긴 칸이라 필수로 안 막음(옛 흙막이 저장 보호)"
},
{
"key": "length_m",
@@ -1542,6 +1560,15 @@
"required": false,
"phase": "b05"
},
{
"key": "height_m",
"label": "높이",
"input": "number",
"unit": "m",
"default": 2.5,
"required": false,
"phase": "b05"
},
{
"key": "before_m",
"label": "기준측점 전",
@@ -1560,24 +1587,6 @@
"required": false,
"phase": "b05"
},
{
"key": "form",
"label": "형식",
"input": "select",
"choices": [
"콘크리트",
"돌(찰)",
"돌(메)",
"블록쌓기",
"돌망태",
"흙포대",
"통나무쌓기",
"떼"
],
"default": null,
"required": true,
"phase": "detail"
},
{
"key": "side",
"label": "설치 측",
@@ -1969,6 +1978,24 @@
"quantity"
],
"options": [
{
"key": "width_m",
"label": "소단 폭",
"input": "number",
"unit": "m",
"default": 0.5,
"required": false,
"phase": "b05"
},
{
"key": "height_m",
"label": "소단 높이",
"input": "number",
"unit": "m",
"default": null,
"required": false,
"empty_means": "비워도 놓임 — 아직 계산(소단 간격 · 횡단)에 안 쓰임(표 09 새 칸)"
},
{
"key": "length_m",
"label": "길이",
@@ -1996,15 +2023,6 @@
"required": false,
"phase": "b05"
},
{
"key": "width_m",
"label": "폭",
"input": "number",
"unit": "m",
"default": 0.5,
"required": false,
"phase": "b05"
},
{
"key": "interval_m",
"label": "간격(사면길이)",
@@ -3396,11 +3414,51 @@
"동작": [
"포장_물넘이_겹침"
]
},
"soil_guard": {
"이름": {
"form=돌쌓기(찰)": "돌 쌓기(찰)",
"form=돌쌓기(메)": "돌 쌓기(메)",
"form=콘크리트": "콘크리트",
"form=돌망태": "돌망태",
"form=통나무·목재틀": "통나무/목재틀",
"form=바자": "바자"
}
}
},
"표시": {
"pipe": {
"pipe_kind": false
},
"soil_guard": {
"side": false,
"tiers": false,
"lift_m": false,
"shift_m": false,
"thickness_top_m": false,
"thickness_bottom_m": false,
"back_len_cm": false,
"stone_kind": false,
"stone_supply": false,
"stone_coeff_basis": false,
"fill_concrete_mpa": false,
"face_slope_ratio": false,
"foundation": false,
"weep_hole_diameter_mm": false,
"weep_hole_area_m2": false
},
"retaining_wall": {
"form": false,
"side": false,
"weep_hole_diameter_mm": false,
"weep_hole_area_m2": false
},
"berm": {
"length_m": false,
"before_m": false,
"after_m": false,
"interval_m": false,
"slope_deg": false
}
},
"그룹": {
@@ -3521,16 +3579,13 @@
"retaining_wall": [
{
"id": "gretaindetail",
"이름": "상세 내용",
"이름": "옹벽",
"옵션": [
"height_m",
"wall_form",
"length_m",
"height_m",
"before_m",
"after_m",
"form",
"side",
"weep_hole_diameter_mm",
"weep_hole_area_m2"
"after_m"
],
"형상": {}
}
@@ -3592,28 +3647,13 @@
"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"
"length_m",
"height_m",
"before_m",
"after_m"
],
"형상": {}
}
@@ -3647,12 +3687,8 @@
"id": "gbermdetail",
"이름": "상세 내용",
"옵션": [
"length_m",
"before_m",
"after_m",
"width_m",
"interval_m",
"slope_deg"
"height_m"
],
"형상": {}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,87 @@
"""PLAN 54-26 — 사용자 구조물 표 09 사면안정 › 흙막이 · 옹벽 · 소단.
· 컨테이너 = 흙막이 「흙막이」 · 옹벽 「옹벽」 · 소단 「상세 내용」 — 칸은 표대로만 · 표 밖 옛 칸은 숨김(값 · 기본값 그대로)
· 흙막이 형태 = 배관 흙막이 6 · 옹벽 형태 = 콘크리트 하나(새 칸 `wall_form` · 옛 형식 `form` 값은 그대로 통과)
"""
import json
from pathlib import Path
from B05_Profile.B05_Profile_Structures_Repository import _validate_types
from B05_Profile.B05_Profile_Structures_Schema import (
DEFINITION_PATH,
StructureInstance,
load_structure_types,
)
from M02_MasterTemplete.StructOptions import M02_SO_Definition
from M02_MasterTemplete.StructOptions.M02_SO_Groups import groups_of
from M02_MasterTemplete.StructOptions.M02_SO_Options import hidden_keys
TABLE = {
"soil_guard": (
"gsoilgdetail",
"흙막이",
["form", "length_m", "height_m", "before_m", "after_m"],
),
"retaining_wall": (
"gretaindetail",
"옹벽",
["wall_form", "length_m", "height_m", "before_m", "after_m"],
),
"berm": ("gbermdetail", "상세 내용", ["width_m", "height_m"]),
}
SOIL_FORMS = ["돌쌓기(찰)", "돌쌓기(메)", "콘크리트", "돌망태", "통나무·목재틀", "바자"]
def _types():
return {t.type_id: t for t in load_structure_types()}
def test_컨테이너_하나_칸은_표대로_차례대로():
groups = groups_of()
types = _types()
hidden = hidden_keys()
for type_id, (gid, name, keys) in TABLE.items():
assert groups[type_id] == [{"id": gid, "name": name, "options": keys}]
options = [o.key for o in types[type_id].options]
assert options[: len(keys)] == keys # 칸 차례 = 표 차례
assert hidden[type_id] == set(options) - set(keys) # 표 밖 = 숨김(값은 남음)
def test_형태_선택지():
types = _types()
soil = {o.key: o for o in types["soil_guard"].options}
pipe = {o.key: o for o in types["pipe"].options}
assert soil["form"].choices == SOIL_FORMS == pipe["outlet_revet_form"].choices
assert soil["form"].phase == "b05" # B05 = B06
wall = {o.key: o for o in types["retaining_wall"].options}
assert wall["wall_form"].choices == ["콘크리트"]
assert "반중력식" in wall["form"].choices # 옛 형식 값 그대로 통과
doc = json.loads(Path(DEFINITION_PATH).read_text("utf-8"))
assert doc["종류"]["soil_guard"]["이름"]["form=통나무·목재틀"] == "통나무/목재틀"
def test_옛_값_저장_막지_않음():
def item(type_id, **options):
return StructureInstance(
type_id=type_id,
placement="interval",
chainage_m=20,
start_m=20,
end_m=30,
options={"length_m": 10, "before_m": 0, "after_m": 10, **options},
)
_validate_types(
[
item("soil_guard", height_m=2), # 형태 없는 옛 흙막이
item("retaining_wall", form="반중력식", height_m=2),
item("berm", width_m=0.5, interval_m=3, height_m=5),
]
)
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()
@@ -20,6 +20,7 @@ KINDS = {
"boulder_masonry": "gboulderdetail",
"berm": "gbermdetail",
}
TABLE_09 = {"soil_guard", "retaining_wall", "berm"}
def test_C군_종류마다_컨테이너_하나_옵션_전부():
@@ -27,6 +28,8 @@ def test_C군_종류마다_컨테이너_하나_옵션_전부():
assert sorted(t.type_id for t in types) == sorted(KINDS)
groups = groups_of()
for item in types:
if item.type_id in TABLE_09: # 표 09 로 바뀜 — test_54_26_slope_table09
continue
assert groups[item.type_id] == [
{
"id": KINDS[item.type_id],
@@ -55,4 +58,4 @@ def test_잇기는_컨테이너마다_집계_한_줄():
]
)
assert [r["복사본"] for r in rows] == ["s1", "s1__gretaindetail"]
assert rows[1]["summary_item"] == link and rows[1]["그룹"] == "상세 내용"
assert rows[1]["summary_item"] == link and rows[1]["그룹"] == "옹벽" # 표 09 컨테이너 이름
@@ -270,7 +270,9 @@ def test_slope_group_computes_range_from_length():
assert options[key].default == default, f"{type_id}.{key} 기본값"
assert options[key].input == "number" and options[key].unit == "m", key
keys = [option.key for option in item.options if option.phase == "b05"]
assert keys[:3] == ["height_m", "length_m", "before_m"], f"{type_id} 옵션 순서"
# 표 09 흙막이 · 옹벽 = 형태 → 길이 → 높이 → 전 → 후(54-26 · test_54_26_slope_table09)
if type_id not in ("soil_guard", "retaining_wall"):
assert keys[:3] == ["height_m", "length_m", "before_m"], f"{type_id} 옵션 순서"
def test_retaining_wall_renamed_plain():
@@ -0,0 +1,242 @@
/* 54-26 — B05 · B06 좌측 「구조물 배치」 사면안정 표 09(흙막이 · 옹벽 · 소단)
* 시작 화면: B05 종단설계(#/b05-profile) · 검증 프로젝트(랩탑_보조)
* 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m5426 = "단계";` 를 붙여
* seed → 원래 목록을 sessionStorage 에 두고 흙막이(20+0 · 잇기 하나) · 소단을 심음(PUT) · 그 뒤 orca reload
* check → 세 종류 컨테이너 1(이름) · 칸 = 표대로 · 밖 칸 0 · 잇기 1 · 램프 · 형태 선택지 6/1
* B05 에서 한 번(값을 sessionStorage 에 둠) · B06(#/b06-section) 에서 한 번(B05 = B06)
* split → 흙막이 길이 10 · 전 3 → 후 7(B05 · 실시간 반영 · 저장 안 함)
* height → B06 — 좌측 높이 1.8 · 2.3 → 20+0 횡단 벽 그림 높이(H=) 같은 값
* save → [저장] — 파일 = 형태 · 길이 10 · 전 3 · 후 7 · 높이 2.3 · 그 뒤 orca reload
* verify → 다시 연 흙막이 칸 = 파일 값(B05 · B06 각각)
* restore → 원래 목록으로 되돌림(PUT) · 초안 비움
* 쓰는 것: 검증 프로젝트 structures.json(되돌림 단계가 원래 값으로 다시 씀)
*/
(async () => {
const MODE = window.__m5426 || "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 LINK = { layer: "system", owner: null, key: "SS000001", name: "54-26 잇기" };
const KINDS = {
soil_guard: ["gsoilgdetail", "흙막이", /흙막이$/, 6], // 심은 것 하나 · 전 3 이면 19+17~20+7
retaining_wall: ["gretaindetail", "옹벽", /^10\+0~10\+10옹벽/, 1],
berm: ["gbermdetail", "상세 내용", /^28\+0~28\+10소단/, 0],
};
const TABLE = {
soil_guard: ["form", "length_m", "height_m", "before_m", "after_m"],
retaining_wall: ["wall_form", "length_m", "height_m", "before_m", "after_m"],
berm: ["width_m", "height_m"],
};
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 field = (key) =>
section().querySelector(
`[data-option-keys="${key}"] input, [data-option-keys="${key}"] select`,
);
const set = async (key, value) => {
const input = field(key);
input.value = String(value);
input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new Event("change", { bubbles: true }));
await w(500);
};
const shape = (type) => {
const boxes = [...section().querySelectorAll("[data-b05-group]")].filter(visible);
const box = boxes[0];
const form = box?.querySelector(
'[data-option-keys="form"] select, [data-option-keys="wall_form"] select',
);
return {
boxes: boxes.map((b) => `${b.dataset.b05Group}:${b.querySelector("legend")?.textContent}`),
fields: box
? [...box.querySelectorAll("[data-option-keys]")].map((n) => n.dataset.optionKeys)
: [],
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,
twoRow: box ? box.querySelectorAll("[data-b05-summary=row]").length : 0,
link: box?.querySelector(".b05-structure__summary-name")?.value.trim(),
choices: form ? [...form.options].filter((o) => o.value).map((o) => o.textContent) : [],
lamps: [...(listItem(KINDS[type][2])?.querySelectorAll(".b05-route__lamps > *") ?? [])].map(
(d) => d.className,
),
};
};
const ok = (type, s) =>
s.boxes.length === 1 &&
s.boxes[0] === `${KINDS[type][0]}:${KINDS[type][1]}` &&
JSON.stringify(s.fields) === JSON.stringify(TABLE[type]) &&
s.loose === 0 &&
s.links === 1 &&
s.twoRow === 1 &&
s.choices.length === KINDS[type][3] &&
s.lamps.length === 1;
const save = async () => {
document.querySelector('[data-dock-action="save"]').click();
await w(500);
return until(() => !document.querySelector(".ui-overlay.is-active"), 60000);
};
const soilFile = async () =>
(await api.fetchStructures(projectId)).structures.find((s) => s.type_id === "soil_guard");
const out = { mode: MODE, page: location.hash };
if (MODE === "seed") {
const now = await api.fetchStructures(projectId);
sessionStorage.setItem("__5426_original", JSON.stringify(now.structures));
const add = (type_id, at, options, extra = {}) => ({
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-26",
...extra,
});
const next = [
...now.structures,
add(
"soil_guard",
400,
{ form: "돌쌓기(찰)", height_m: 1.5 },
{ group_items: { gsoilgdetail: LINK } },
),
add("berm", 560, { width_m: 1, height_m: 5 }),
];
await api.saveStructures(projectId, now.revision, next);
for (const k of Object.keys(sessionStorage))
if (k.includes("draft:structures") || k.includes("result:section-detail"))
sessionStorage.removeItem(k);
out.seeded = (await api.fetchStructures(projectId)).structures
.filter((s) => s.type_id in KINDS)
.map((s) => s.type_id);
}
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.read = r;
if (!location.hash.includes("b06")) sessionStorage.setItem("__5426_b05", JSON.stringify(r));
else out.sameAsB05 = sessionStorage.getItem("__5426_b05") === JSON.stringify(r);
}
if (MODE === "split") {
await open(KINDS.soil_guard[2]);
await set("length_m", 10);
await set("before_m", 3);
out.after = field("after_m").value;
out.ok = Number(out.after) === 7;
}
if (MODE === "height") {
// 독립 벽 조정창은 위치 전용(8-29 지시) — 높이 = 좌측 칸 하나 · 횡단 그림이 그 값을 따라감
await open(KINDS.soil_guard[2]);
await w(1500);
const card = () =>
[...document.querySelectorAll(".b06-cross-card")].find((c) =>
/^\s*20\+0400\.0m/.test(c.textContent),
);
const drawn = () => {
const tip = card()?.querySelector(".b06-chart__culvert-revet title")?.textContent ?? "";
return Number(tip.match(/H=([\d.]+)m/)?.[1]);
};
const near = (a, b) => Math.abs(a - b) < 0.01;
out.steps = [];
for (const h of [1.8, 2.3]) {
await set("height_m", h);
await until(() => near(drawn(), h), 8000);
out.steps.push({ left: Number(field("height_m").value), drawn: drawn() });
}
out.ok = out.steps.every((s) => near(s.left, s.drawn));
}
if (MODE === "save") {
out.saved = await save();
const soil = await soilFile();
out.file = soil.options;
out.ok =
soil.options.form === "돌쌓기(찰)" &&
soil.options.length_m === 10 &&
soil.options.before_m === 3 &&
soil.options.after_m === 7 &&
soil.options.height_m === 2.3 &&
JSON.stringify(soil.group_items) === JSON.stringify({ gsoilgdetail: LINK });
}
if (MODE === "verify") {
await open(KINDS.soil_guard[2]);
const soil = await soilFile();
out.form = TABLE.soil_guard.map((k) => field(k).value);
out.file = TABLE.soil_guard.map((k) => String(soil.options[k]));
out.ok = out.form.every((v, i) => Number(v) === Number(out.file[i]) || v === out.file[i]);
await reset();
}
if (MODE === "restore") {
const original = JSON.parse(sessionStorage.getItem("__5426_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.includes("result:section-detail") ||
k.startsWith("__5426_")
)
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 ?? null,
]),
);
out.restored = key(after.structures) === key(original);
}
return JSON.stringify(out);
})();