merge: main_laptop_1 → sub_laptop_7 52-3 — 충돌 다섯 = 양쪽 다 살림
- 명세 목록 = 박스암거 · 세월교 · 물넘이 포장 · 노면공 · 기타 전부 - B05 시설 폼 = BOX 명세 폼(메인) + 물넘이 포장 명세 폼(52-3) · 옛 BOX 프리셋 · 월류 칸 뺌 - 병합 키 = BOX 명세 키 + 물넘이 포장 경사각도 · 도수로/돌붙임 머리 - 표 채우기 컨테이너 = 물넘이 포장 도수로/돌붙임 + 기타 상세 내용 - 구조물정의 = 양쪽 그룹(box_culvert · ford_pavement) · 판 12 · 직전 메인 판 11 은 이력 판_11 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
/* =============================================================================
|
||||
* design_structure_box.ts
|
||||
* 횡단배수 › 박스암거 명세 — 사용자 구조물 표 02 고친 판(`docs/raw/구조물정의_표/02_횡단배수_박스암거.md`) 그대로.
|
||||
*
|
||||
* 컨테이너 셋(모두 필수): 본체 · 유입 구조 · 유출 구조. 구조 · 정본 = 이 명세 · 이름만 정의(M02)에서 바꿈.
|
||||
* 저장 키 = pipe_points.json 옛 키 그대로(B06 횡단 · B07 · B08 이 읽는 자리):
|
||||
* · 본체 = `body_width_m` · `body_height_m` 수 — 규격 칸은 저장 안 함(폭 · 높이로 되찾음 · `bodyOf`)
|
||||
* · 유입 · 유출 구조 = `wing_in` · `wing_out`(없음 · 날개벽 · 흙막이 · 옛 「있음」 = 날개벽)
|
||||
* — 엔진은 날개벽만 세움(`_wing_spec` ↔ `wingSpec` 짝)
|
||||
* 표 02 에 없는 옛 날개벽 치수 칸은 화면에서 빼고 값은 남김(지난 정의 값).
|
||||
* import 없음(꼴만) — 시험이 이 파일만 옮겨 돌림.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { Instance, SpecItem, StructureSpec } from "./design_structure_spec";
|
||||
|
||||
export const BOX_CUSTOM = "사용자 입력";
|
||||
/** 본체 규격 → [가로, 세로](m) — 실무 관측 규격(2026-08-17 사용자 확정). */
|
||||
const BOX_PRESETS: Record<string, readonly [number, number]> = {
|
||||
"2.0×2.0": [2, 2],
|
||||
"3.0×3.0": [3, 3],
|
||||
};
|
||||
|
||||
/** 유입 · 유출 구조 — 같은 선택지 · 키만 다름. */
|
||||
const structureItem = (key: string): SpecItem => ({
|
||||
id: "structure",
|
||||
label: "구조",
|
||||
kind: "select",
|
||||
key,
|
||||
choices: [
|
||||
{ value: "없음", label: "없음" },
|
||||
{ value: "날개벽", label: "날개벽" },
|
||||
{ value: "흙막이", label: "흙막이" },
|
||||
],
|
||||
});
|
||||
|
||||
/** 사용자 입력일 때만 서는 숫자 칸. */
|
||||
const customSide = (id: string, label: string, key: string): SpecItem => ({
|
||||
id,
|
||||
label,
|
||||
kind: "number",
|
||||
step: 0.1,
|
||||
by: { item: "size", map: { [BOX_CUSTOM]: { key } } },
|
||||
});
|
||||
|
||||
export const BOX_SPEC: StructureSpec = {
|
||||
typeId: "box_culvert",
|
||||
containers: [
|
||||
{
|
||||
id: "body",
|
||||
title: "본체",
|
||||
required: true,
|
||||
link: "gboxbody",
|
||||
items: [
|
||||
{
|
||||
id: "size",
|
||||
label: "본체 규격",
|
||||
kind: "select",
|
||||
choices: [
|
||||
...Object.keys(BOX_PRESETS).map((value) => ({
|
||||
value,
|
||||
label: value.replace("×", " × "),
|
||||
})),
|
||||
{ value: BOX_CUSTOM, label: BOX_CUSTOM },
|
||||
],
|
||||
},
|
||||
customSide("width", "가로 (m)", "body_width_m"),
|
||||
customSide("height", "세로 (m)", "body_height_m"),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "inlet",
|
||||
title: "유입 구조",
|
||||
required: true,
|
||||
link: "gboxin",
|
||||
items: [structureItem("wing_in")],
|
||||
},
|
||||
{
|
||||
id: "outlet",
|
||||
title: "유출 구조",
|
||||
required: true,
|
||||
link: "gboxout",
|
||||
items: [structureItem("wing_out")],
|
||||
},
|
||||
],
|
||||
// 표 02 에 없는 옛 날개벽 치수 — 화면에서 빼고 값은 남김(지난 정의 값)
|
||||
retired: ["in", "out"].flatMap((side) =>
|
||||
["height_m", "length_m", "angle_deg"].map((field) => `wing_${side}_${field}`),
|
||||
),
|
||||
};
|
||||
|
||||
/** 옛 저장 → 폼에 줄 옵션 — 설치 「있음」 = 날개벽(같은 뜻 · 저장하면 새 값으로 굳음). */
|
||||
export function boxOptionsIn(
|
||||
options: Record<string, string | number>,
|
||||
): Record<string, string | number> {
|
||||
const out = { ...options };
|
||||
for (const key of ["wing_in", "wing_out"]) if (out[key] === "있음") out[key] = "날개벽";
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 저장 폭 · 높이 → 본체 칸 값 — 규격과 맞으면 그 규격 · 아니면 사용자 입력 · 없으면 빈 값. */
|
||||
export function bodyOf(options: Record<string, string | number>): Instance {
|
||||
const width = Number(options.body_width_m);
|
||||
const height = Number(options.body_height_m);
|
||||
if (!(width > 0 && height > 0)) return { size: "" };
|
||||
const preset = Object.entries(BOX_PRESETS).find(
|
||||
([, [w, h]]) => Math.abs(w - width) < 0.05 && Math.abs(h - height) < 0.05,
|
||||
);
|
||||
return preset
|
||||
? { size: preset[0] }
|
||||
: { size: BOX_CUSTOM, width: String(width), height: String(height) };
|
||||
}
|
||||
|
||||
/** 본체 칸 값 → 저장 폭 · 높이 — 규격이면 규격 수(사용자 입력은 명세 키가 적음). */
|
||||
export function bodyOptions(body: Instance | undefined): Record<string, number> {
|
||||
const preset = BOX_PRESETS[body?.size ?? ""];
|
||||
return preset ? { body_width_m: preset[0], body_height_m: preset[1] } : {};
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
* 구조물 명세(`design_structure_spec`) 그리기 — B05 · B06 「구조물 배치」 칸이 같은 노드를 씀.
|
||||
*
|
||||
* · 필수 컨테이너 = 늘 보임(한 벌) · 선택 컨테이너 = [추가] 로 인스턴스를 늘리고 인스턴스마다 [빼기]
|
||||
* · 항목: 고르기(상위 값에 따른 선택지 · 저장 키) · 숫자(맞물림 = 길이 · 전 · 후) · 자동(읽기만)
|
||||
* · 항목: 고르기(상위 값에 따른 선택지 · 저장 키) · 숫자(맞물림 = 길이 · 전 · 후) · 글(손 입력) · 자동(읽기만)
|
||||
* · 인스턴스마다 「구조물 조합」 잇기 줄(`buildSummaryRow` · 고르기 모달 = 패널의 그룹 [고르기] 길 그대로) —
|
||||
* 값은 패널 `group_items` 에 `linkOf` 키로 둠(빼면 뒤 인스턴스 잇기를 한 칸씩 당김)
|
||||
* · 값은 폼이 쥠 — 저장은 패널이 `read()` 로 읽어 정본 자리(옵션)에 씀
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
linkOf,
|
||||
readState,
|
||||
solveSplit,
|
||||
TEXT_MAX,
|
||||
writeOptions,
|
||||
type SpecContainer,
|
||||
type SpecItem,
|
||||
@@ -111,6 +112,13 @@ export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = {
|
||||
select.value = now;
|
||||
return select;
|
||||
}
|
||||
if (item.kind === "text") {
|
||||
const input = document.createElement("input");
|
||||
input.type = "text";
|
||||
input.maxLength = TEXT_MAX;
|
||||
input.value = values[item.id] ?? "";
|
||||
return input;
|
||||
}
|
||||
const input = numberInput(String(item.step ?? 0.1), "0", hooks.hint?.(c, item, state) ?? "");
|
||||
if (item.kind === "auto") {
|
||||
input.type = "text";
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import type { SpecChoice, SpecItem, StructureSpec } from "./design_structure_spec";
|
||||
import { BOX_SPEC } from "./design_structure_box";
|
||||
import { FORD_SPEC } from "./design_structure_ford";
|
||||
import { FORD_PAVEMENT_SPEC } from "./design_structure_ford_pavement";
|
||||
import { PAVEMENT_SPEC } from "./design_structure_road";
|
||||
import { ETC_SPEC, PAVEMENT_SPEC } from "./design_structure_road";
|
||||
|
||||
const SOIL = "기슭막이"; // 저장 값(옛 키 그대로) — 화면 이름은 흙막이
|
||||
const BASIN = "집수정";
|
||||
@@ -189,7 +190,9 @@ export const PIPE_SPEC: StructureSpec = {
|
||||
/** 명세로 그리는 종류 — 종류 키 → 명세(표가 오는 대로 더함). */
|
||||
export const STRUCTURE_SPECS: Readonly<Record<string, StructureSpec>> = {
|
||||
pipe: PIPE_SPEC,
|
||||
box_culvert: BOX_SPEC,
|
||||
ford_bridge: FORD_SPEC,
|
||||
ford_pavement: FORD_PAVEMENT_SPEC,
|
||||
pavement_concrete: PAVEMENT_SPEC,
|
||||
etc: ETC_SPEC,
|
||||
};
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
* 기준 측점(패널 위치 칸) + 필수 컨테이너 「상세 내용」 한 벌 = 포장사양 고르기 · 높이(m) 손 입력.
|
||||
* 구간 길이 · 전 · 후는 정의 옵션 칸 그대로(구간 배치) · 표에 없는 옛 상세 칸은 화면에서 빼고 값은 남김.
|
||||
* 저장 = structures.json `options` — 새 키 `pave_spec` · `height_m`.
|
||||
* 기타(52-14) = 기준 측점 + 상세 종류 이름(정의 옵션 칸 `name`) + 선택 컨테이너 「상세 내용」(비고 글) ·
|
||||
* 저장 키 `etc_detail_{n}_note` · 잇기 `getcdetail{n}`.
|
||||
* import 없음 — 시험이 이 파일만 옮겨 돌림.
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -43,3 +45,18 @@ export const PAVEMENT_SPEC: StructureSpec = {
|
||||
"wire_mesh",
|
||||
],
|
||||
};
|
||||
|
||||
export const ETC_SPEC: StructureSpec = {
|
||||
typeId: "etc",
|
||||
containers: [
|
||||
{
|
||||
id: "detail",
|
||||
title: "상세 내용",
|
||||
required: false,
|
||||
prefix: "etc_detail",
|
||||
link: "getcdetail",
|
||||
items: [{ id: "note", label: "비고", kind: "text" }],
|
||||
},
|
||||
],
|
||||
retired: [],
|
||||
};
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
*
|
||||
* · 종류 = 기본 옵션(기준 측점 No + 거리 · 패널 위치 칸이 받음) + 컨테이너[] + 항목[]
|
||||
* · 컨테이너 = 제목 · 필수(늘 한 벌) / 선택([추가] · [빼기] · 여러 개)
|
||||
* · 항목 꼴 = 고르기 · 숫자 · 자동(계산이 채움 · 저장 안 함) · 맞물림(길이 = 전 + 후)
|
||||
* · 항목 꼴 = 고르기 · 숫자 · 글(손 입력 · 길이 상한 `TEXT_MAX`) · 자동(계산이 채움 · 저장 안 함) ·
|
||||
* 맞물림(길이 = 전 + 후)
|
||||
* · 상위 값에 따른 선택지 · 저장 키 = `by`(같은 컨테이너 안 상위 항목 값 → 갈래)
|
||||
* · 저장 = 정본 자리 옵션 그대로(관이면 pipe_points.json `options`) — 필수 컨테이너는 항목 `key` ·
|
||||
* 선택 컨테이너는 인스턴스마다 `{prefix}_{n}_{항목}`(n 은 1부터)
|
||||
@@ -31,7 +32,7 @@ export type SplitRole = "length" | "before" | "after";
|
||||
export interface SpecItem {
|
||||
id: string;
|
||||
label: string;
|
||||
kind: "select" | "number" | "auto";
|
||||
kind: "select" | "number" | "text" | "auto";
|
||||
/** 저장 키(필수 컨테이너 · `by` 없을 때). */
|
||||
key?: string;
|
||||
choices?: SpecChoice[];
|
||||
@@ -64,6 +65,9 @@ export interface StructureSpec {
|
||||
retired: string[];
|
||||
}
|
||||
|
||||
/** 글 항목 길이 상한(자) — 칸 · 저장이 함께 자름. */
|
||||
export const TEXT_MAX = 100;
|
||||
|
||||
/** 컨테이너 인스턴스 한 벌 값 `{항목 id: 글}`. */
|
||||
export type Instance = Record<string, string>;
|
||||
export type SpecState = Record<string, Instance[]>;
|
||||
@@ -135,7 +139,8 @@ export function readState(
|
||||
return state;
|
||||
}
|
||||
|
||||
/** 컨테이너 값 → 옵션(빈 칸 뺌 · 숫자 칸은 수 · 선택 컨테이너는 차례대로 1 부터 다시 매김). */
|
||||
/** 컨테이너 값 → 옵션(빈 칸 뺌 · 숫자 칸은 수 · 선택 컨테이너는 차례대로 1 부터 다시 매김 ·
|
||||
* 다 빈 선택 인스턴스는 첫 칸을 빈 글로 남겨 자리를 지킴 — 잇기 · 집계 줄 · 다시 열기). */
|
||||
export function writeOptions(
|
||||
spec: StructureSpec,
|
||||
state: SpecState,
|
||||
@@ -143,6 +148,7 @@ export function writeOptions(
|
||||
const out: Record<string, string | number> = {};
|
||||
for (const c of spec.containers) {
|
||||
(state[c.id] ?? []).forEach((values, index) => {
|
||||
const before = Object.keys(out).length;
|
||||
for (const item of c.items) {
|
||||
const key = keyOf(c, item, index + 1, values);
|
||||
const raw = (values[item.id] ?? "").trim();
|
||||
@@ -152,10 +158,13 @@ export function writeOptions(
|
||||
if (Number.isFinite(n)) out[key] = Math.round(n * 100) / 100;
|
||||
continue;
|
||||
}
|
||||
out[key] = raw;
|
||||
out[key] = item.kind === "text" ? raw.slice(0, TEXT_MAX) : raw;
|
||||
if (c.required)
|
||||
Object.assign(out, choicesOf(item, values).find((ch) => ch.value === raw)?.also);
|
||||
}
|
||||
const first = c.items.find((item) => item.kind !== "auto");
|
||||
if (!c.required && first && Object.keys(out).length === before)
|
||||
out[instanceKey(c, index + 1, first)] = "";
|
||||
});
|
||||
}
|
||||
return out;
|
||||
|
||||
@@ -18,11 +18,9 @@
|
||||
* 유입·유출 길이가 정해지면 위치 칸(시작·종료 측점)을 기준 − 유입 ~ 기준 + 유출로
|
||||
* 자동 채운다. 형식·치수 제원은 B06/B07 필수 승격 대상이라 보이되 비필수다.
|
||||
*
|
||||
* BOX암거(2026-08-17 사용자 확정): 본체는 실무 관측 규격 2.0×2.0·3.0×3.0을 프리셋
|
||||
* 셀렉트로 받고(기본 2.0×2.0), 그 밖의 규격만 "사용자 지정"으로 폭·높이를 직접
|
||||
* 받는다 — 저장 키는 어느 쪽이든 body_width_m·body_height_m 숫자 그대로다.
|
||||
* 날개벽은 유입·유출이 같은 조각(createWingFields)을 쓰고 설치 있음·짧은쪽 높이
|
||||
* 1m·길이 2m·각도 45°가 기본이며, 설치를 "없음"으로 바꾸면 제원 칸이 접힌다.
|
||||
* BOX암거 = 사용자 구조물 표 02 명세(PLAN 52-2 · `design_structure_box`) — 본체 규격 ·
|
||||
* 유입 · 유출 구조. 저장 키는 body_width_m·body_height_m 숫자 · wing_in·wing_out 그대로.
|
||||
* 날개벽 조각(createWingFields)은 숨김 — B06 조정 행 자리만 BOX · 세월교 명세 유입 · 유출 칸으로 옮겨 붙음.
|
||||
*
|
||||
* 짝: 시설 속성 붙이기 `carryIndices` ↔ 서버 `common_util/common_util_drainage_pipes.py`
|
||||
* `carry_indices` — 같은 규칙(허용 오차 안 · 한 원본 한 번) · 거울 시험 test_b05_facility_carry.
|
||||
@@ -35,8 +33,6 @@ import type {
|
||||
} from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
|
||||
import {
|
||||
BOX_SIZE_CUSTOM,
|
||||
BOX_SIZE_PRESETS,
|
||||
createRevetSideGroup,
|
||||
createWingFields,
|
||||
grid,
|
||||
@@ -44,15 +40,19 @@ import {
|
||||
INLET_REVET_KEYS,
|
||||
type InletStructureKind,
|
||||
labeled,
|
||||
numberInput,
|
||||
optionalSelect,
|
||||
OUTLET_REVET_KEYS,
|
||||
putNumber,
|
||||
WING_IN_KEYS,
|
||||
WING_OUT_KEYS,
|
||||
} from "./B05_Profile_UI_Drainage_Facility_Fields";
|
||||
import { createSpecForm, type SpecLinks } from "../A00_Common/design/design_structure_form";
|
||||
import { PIPE_SPEC } from "../A00_Common/design/design_structure_pipe";
|
||||
import {
|
||||
BOX_SPEC,
|
||||
bodyOf,
|
||||
bodyOptions,
|
||||
boxOptionsIn,
|
||||
} from "../A00_Common/design/design_structure_box";
|
||||
import { FORD_SPEC, withLegacyGuards } from "../A00_Common/design/design_structure_ford";
|
||||
import { fordHeightHint, fordHeightM } from "../A00_Common/design/design_structure_ford_calc";
|
||||
import { guardHeightHint, pipeLengthM } from "../A00_Common/design/design_structure_pipe_calc";
|
||||
@@ -359,23 +359,9 @@ export function createFacilityOptionsForm(
|
||||
return merged;
|
||||
};
|
||||
|
||||
// ── BOX암거 — 본체 규격(프리셋 + 사용자 지정)·날개벽(유입·유출 개별) ────
|
||||
// 폭·높이 자유 입력은 "사용자 지정"을 골랐을 때만 펼친다(2026-08-17 사용자 확정).
|
||||
const boxSize = optionalSelect("— 안 정함 —", [
|
||||
...BOX_SIZE_PRESETS.map(([label]) => label),
|
||||
BOX_SIZE_CUSTOM,
|
||||
]);
|
||||
const boxWidth = numberInput("0.1", "0");
|
||||
const boxHeight = numberInput("0.1", "0");
|
||||
const boxCustomRow = grid(
|
||||
labeled("본체 폭 (m)", boxWidth, "body_width_m"),
|
||||
labeled("본체 높이 (m)", boxHeight, "body_height_m"),
|
||||
);
|
||||
const boxWrap = document.createElement("div");
|
||||
boxWrap.append(
|
||||
grid(labeled("본체 규격 (m)", boxSize, ["body_width_m", "body_height_m"])),
|
||||
boxCustomRow,
|
||||
);
|
||||
// ── BOX암거 = 사용자 구조물 표 02 명세(PLAN 52-2) — 본체 · 유입 구조 · 유출 구조 ──
|
||||
const boxForm = createSpecForm(specLinks, { onChange: () => emit() });
|
||||
// 날개벽 칸 — 세월교 · BOX 가 같은 조각을 쓰던 자리 · 조정 행은 명세 유입 · 유출 칸으로 옮겨 붙음
|
||||
const wingInFields = createWingFields("날개벽(유입)", WING_IN_KEYS);
|
||||
const wingOutFields = createWingFields("날개벽(유출)", WING_OUT_KEYS);
|
||||
const wingInHome = wingInFields.slot.parentElement!;
|
||||
@@ -383,18 +369,6 @@ export function createFacilityOptionsForm(
|
||||
|
||||
const hiddenOptions = createHiddenOptions(root);
|
||||
|
||||
/** 고른 프리셋을 폭·높이 칸에 실어 둔다 — 저장도, "사용자 지정"으로 바꿨을 때의
|
||||
* 출발값도 이 두 칸이 정본이다. 사용자 지정 상태에서는 손대지 않는다. */
|
||||
function syncBoxSize(): void {
|
||||
const preset = BOX_SIZE_PRESETS.find(([label]) => label === boxSize.value);
|
||||
if (preset) {
|
||||
boxWidth.value = String(preset[1]);
|
||||
boxHeight.value = String(preset[2]);
|
||||
}
|
||||
// 「안 정함」도 폭·높이 칸을 접음 — 값이 없는 상태 그대로.
|
||||
boxCustomRow.hidden = boxSize.value !== BOX_SIZE_CUSTOM;
|
||||
}
|
||||
|
||||
// ── 물넘이 포장 = 사용자 구조물 표 03 명세(PLAN 52-3) — 경사각도 · 도수로/돌붙임 ·
|
||||
// 옛 월류 폭 · 높이 · 바닥 경사 · 두께 · 길이 칸은 뺌(지난 정의 값) ──
|
||||
const paveForm = createSpecForm(specLinks, { onChange: () => emit() });
|
||||
@@ -416,7 +390,7 @@ export function createFacilityOptionsForm(
|
||||
revetTopRow,
|
||||
revetInlet.root,
|
||||
revetOutlet.root,
|
||||
boxWrap,
|
||||
boxForm.root,
|
||||
wingInFields.root,
|
||||
wingOutFields.root,
|
||||
);
|
||||
@@ -430,14 +404,14 @@ export function createFacilityOptionsForm(
|
||||
const isBox = current === "box_culvert";
|
||||
const isBridge = current === "ford_bridge";
|
||||
pipeForm.root.hidden = !isPipe;
|
||||
boxForm.root.hidden = !isBox;
|
||||
fordForm.root.hidden = !isBridge;
|
||||
boxWrap.hidden = !isBox;
|
||||
// 날개벽 칸 = BOX암거 · 세월교는 유입 · 유출 구조 컨테이너(52-4) — B06 조정 행 자리만 그 컨테이너 끝으로
|
||||
const hasWing = isBox;
|
||||
wingInFields.root.hidden = !hasWing;
|
||||
wingOutFields.root.hidden = !hasWing;
|
||||
(isBridge ? fordForm.slot("inlet") : wingInHome).append(wingInFields.slot);
|
||||
(isBridge ? fordForm.slot("outlet") : wingOutHome).append(wingOutFields.slot);
|
||||
// 날개벽 칸 = 옛 자리 — BOX(52-2) · 세월교(52-4)는 명세 유입 · 유출 구조 컨테이너 · B06 조정 행 자리만 그 컨테이너 끝으로
|
||||
const spec = isBox ? boxForm : isBridge ? fordForm : null;
|
||||
wingInFields.root.hidden = true;
|
||||
wingOutFields.root.hidden = true;
|
||||
(spec ? spec.slot("inlet") : wingInHome).append(wingInFields.slot);
|
||||
(spec ? spec.slot("outlet") : wingOutHome).append(wingOutFields.slot);
|
||||
paveForm.root.hidden = current !== "ford_pavement";
|
||||
const isRevet = current === "revetment";
|
||||
revetTopRow.hidden = !isRevet;
|
||||
@@ -450,11 +424,6 @@ export function createFacilityOptionsForm(
|
||||
// 추가 기슭막이 칸은 **내용이 있을 때만** 선다 — 고른 다단 벽의 행이 여기 오면 뜨고,
|
||||
// 선택이 풀리면 스스로 사라진다.
|
||||
extraGroup.root.hidden = extraSlot.childElementCount === 0;
|
||||
if (isBox) syncBoxSize();
|
||||
if (hasWing) {
|
||||
wingInFields.syncVisibility();
|
||||
wingOutFields.syncVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
function emit(): void {
|
||||
@@ -465,7 +434,7 @@ export function createFacilityOptionsForm(
|
||||
revetInlet.fields.onChange(emit);
|
||||
revetOutlet.fields.onChange(emit);
|
||||
revetSide.addEventListener("change", emit);
|
||||
[boxSize, boxWidth, boxHeight, ...wingInFields.inputs, ...wingOutFields.inputs].forEach((input) =>
|
||||
[...wingInFields.inputs, ...wingOutFields.inputs].forEach((input) =>
|
||||
input.addEventListener("change", emit),
|
||||
);
|
||||
|
||||
@@ -497,11 +466,13 @@ export function createFacilityOptionsForm(
|
||||
bindLinks(links) {
|
||||
linkSource = links;
|
||||
pipeForm.paintLinks();
|
||||
boxForm.paintLinks();
|
||||
fordForm.paintLinks();
|
||||
paveForm.paintLinks();
|
||||
},
|
||||
paintLinks: () => {
|
||||
pipeForm.paintLinks();
|
||||
boxForm.paintLinks();
|
||||
fordForm.paintLinks();
|
||||
paveForm.paintLinks();
|
||||
},
|
||||
@@ -527,30 +498,17 @@ export function createFacilityOptionsForm(
|
||||
options[key] !== undefined ? String(options[key]) : "";
|
||||
// 받은 값만 칸에 — 없으면 「안 정함」(새 시설은 부르는 쪽이 등록부 기본값을 실어 줌 · 46-7).
|
||||
const groups = Object.fromEntries((definition?.groups ?? []).map((g) => [g.id, g.name]));
|
||||
if (facility === "pipe")
|
||||
pipeForm.write(PIPE_SPEC, options, definition?.spec_names ?? {}, groups);
|
||||
const names = definition?.spec_names ?? {};
|
||||
if (facility === "pipe") pipeForm.write(PIPE_SPEC, options, names, groups);
|
||||
if (facility === "box_culvert") {
|
||||
// 본체 규격은 저장 안 함 — 폭 · 높이로 되찾음 · 옛 설치 「있음」 = 날개벽
|
||||
boxForm.write(BOX_SPEC, boxOptionsIn(options), names, groups);
|
||||
for (const [id, value] of Object.entries(bodyOf(options)))
|
||||
boxForm.setValue("body", id, value);
|
||||
}
|
||||
if (facility === "ford_bridge")
|
||||
fordForm.write(FORD_SPEC, withLegacyGuards(options), definition?.spec_names ?? {}, groups);
|
||||
if (facility === "ford_pavement")
|
||||
paveForm.write(FORD_PAVEMENT_SPEC, options, definition?.spec_names ?? {}, groups);
|
||||
// 저장된 폭·높이가 프리셋과 맞으면 그 프리셋을, 아니면 "사용자 지정"을 고른다.
|
||||
// 값이 없으면 「안 정함」.
|
||||
const savedWidth = Number.parseFloat(text("body_width_m"));
|
||||
const savedHeight = Number.parseFloat(text("body_height_m"));
|
||||
const hasSaved =
|
||||
Number.isFinite(savedWidth) &&
|
||||
savedWidth > 0 &&
|
||||
Number.isFinite(savedHeight) &&
|
||||
savedHeight > 0;
|
||||
const preset = hasSaved
|
||||
? BOX_SIZE_PRESETS.find(
|
||||
([, width, height]) =>
|
||||
Math.abs(width - savedWidth) < 0.05 && Math.abs(height - savedHeight) < 0.05,
|
||||
)
|
||||
: undefined;
|
||||
boxSize.value = hasSaved ? (preset?.[0] ?? BOX_SIZE_CUSTOM) : "";
|
||||
boxWidth.value = hasSaved ? String(savedWidth) : "";
|
||||
boxHeight.value = hasSaved ? String(savedHeight) : "";
|
||||
fordForm.write(FORD_SPEC, withLegacyGuards(options), names, groups);
|
||||
if (facility === "ford_pavement") paveForm.write(FORD_PAVEMENT_SPEC, options, names, groups);
|
||||
wingInFields.write(options);
|
||||
wingOutFields.write(options);
|
||||
revetSide.value = text("side");
|
||||
@@ -570,11 +528,8 @@ export function createFacilityOptionsForm(
|
||||
if (current === "pipe") {
|
||||
Object.assign(options, pipeForm.read()); // 명세 컨테이너 값 = 옛 키 그대로(PLAN 52-1)
|
||||
} else if (current === "box_culvert") {
|
||||
// 프리셋을 골랐든 사용자 지정을 적었든 정본은 폭·높이 두 칸이다.
|
||||
putNumber(options, "body_width_m", boxWidth.value);
|
||||
putNumber(options, "body_height_m", boxHeight.value);
|
||||
wingInFields.read(options);
|
||||
wingOutFields.read(options);
|
||||
// 정본은 폭 · 높이 두 칸 — 규격이면 규격 수 · 사용자 입력이면 명세 숫자 칸(PLAN 52-2)
|
||||
Object.assign(options, boxForm.read(), bodyOptions(boxForm.state().body?.[0]));
|
||||
} else if (current === "ford_pavement") {
|
||||
Object.assign(options, paveForm.read()); // 표 03 명세 값 · 옛 월류 · 경사 · 두께 · 길이는 병합이 이어 붙임(52-3)
|
||||
} else if (current === "ford_bridge") {
|
||||
|
||||
@@ -461,18 +461,6 @@ export function createBasinSplitFields(length: HTMLInputElement): {
|
||||
};
|
||||
}
|
||||
|
||||
/* ── BOX암거 ────────────────────────────────────────────────────────────── */
|
||||
|
||||
/** 본체 규격 프리셋 — 실무 관측 규격이다(2026-08-17 사용자 확정). 그 밖의 규격은
|
||||
* "사용자 지정"으로 폭·높이를 직접 받는다. 저장 키는 프리셋이든 자유 입력이든
|
||||
* body_width_m·body_height_m 숫자 그대로다 — 프리셋 전용 키를 만들지 않아야
|
||||
* 하류(B06 횡단도·수량)가 폭·높이만 읽고 끝난다. */
|
||||
export const BOX_SIZE_PRESETS: ReadonlyArray<readonly [string, number, number]> = [
|
||||
["2.0×2.0", 2, 2],
|
||||
["3.0×3.0", 3, 3],
|
||||
];
|
||||
export const BOX_SIZE_CUSTOM = "사용자 지정";
|
||||
|
||||
/** 날개벽 옵션 키 묶음 — 유입·유출이 같은 조각을 쓰되 키만 다르다. */
|
||||
interface WingKeys {
|
||||
install: string;
|
||||
|
||||
@@ -17,12 +17,6 @@
|
||||
|
||||
const revetKeys = (side: "inlet" | "outlet"): string[] =>
|
||||
["form", "length_m", "height_m", "before_m", "after_m"].map((key) => `${side}_revet_${key}`);
|
||||
const wingKeys = (side: "in" | "out"): string[] => [
|
||||
`wing_${side}`,
|
||||
`wing_${side}_height_m`,
|
||||
`wing_${side}_length_m`,
|
||||
`wing_${side}_angle_deg`,
|
||||
];
|
||||
|
||||
/** 시설 종류별로 **폼이 적는 칸** — `createFacilityOptionsForm().readOptions()` 가 쓰는 키 전부. */
|
||||
export const FORM_OPTION_KEYS: Readonly<Record<string, readonly string[]>> = {
|
||||
@@ -40,7 +34,8 @@ export const FORM_OPTION_KEYS: Readonly<Record<string, readonly string[]>> = {
|
||||
"outlet_type",
|
||||
...revetKeys("outlet"),
|
||||
],
|
||||
box_culvert: ["body_width_m", "body_height_m", ...wingKeys("in"), ...wingKeys("out")],
|
||||
// BOX = 표 02 명세 키(PLAN 52-2) — 옛 날개벽 치수는 폼에서 빠져 이어 붙임(지난 정의 값)
|
||||
box_culvert: ["body_width_m", "body_height_m", "wing_in", "wing_out"],
|
||||
// 물넘이 포장 = 표 03 명세 키(PLAN 52-3 · `design_structure_ford_pavement`) — 옛 월류 · 경사 · 두께 · 길이는
|
||||
// 폼에서 빠져 이어 붙임 · 도수로/돌붙임 키는 `FORM_OPTION_PREFIXES`
|
||||
ford_pavement: ["ford_slope_deg"],
|
||||
|
||||
@@ -57,11 +57,9 @@ const SIDE_ROWS = [
|
||||
".b06-structure-panel__struct--wall-height",
|
||||
].join(", ");
|
||||
|
||||
/** 이식 자리가 지금 화면에 서 있는지 — 숨겨진 그룹(다른 시설)이면 옮기지 않는다. */
|
||||
/** 이식 자리가 지금 화면에 서 있는지 — 그 위 어느 줄이든 숨었으면(다른 시설 명세 폼 등) 옮기지 않는다. */
|
||||
function slotUsable(target: HTMLElement | undefined): target is HTMLElement {
|
||||
if (!target) return false;
|
||||
const group = target.closest("fieldset");
|
||||
return !!group && !group.hidden;
|
||||
return !!target?.closest("fieldset") && !target.closest("[hidden]");
|
||||
}
|
||||
|
||||
/** 배관이면 유입구·유출구 칸, 독립 기슭막이면 좌·우 칸, 세월교면 날개벽 칸 —
|
||||
|
||||
@@ -8,7 +8,12 @@
|
||||
* 원천이라 배수관 구간값과 같은 저장기로 되돌려 쓴다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { boxSpanM, pipeDiameterM, wingSlabExtendM } from "@util/common_util_culvert_sets";
|
||||
import {
|
||||
boxSpanM,
|
||||
pipeDiameterM,
|
||||
wingInstalled,
|
||||
wingSlabExtendM,
|
||||
} from "@util/common_util_culvert_sets";
|
||||
import type { CrossDesign, CrossSection } from "./B06_Section_Api_Fetch";
|
||||
import { DEFAULT_FORD_WALL_ADJUST } from "./B06_Section_UI_Cross_Ford";
|
||||
import type { FordAdjust, FordWallAdjust, FordWallRole } from "./B06_Section_UI_Cross_Ford";
|
||||
@@ -76,8 +81,8 @@ function wingPatchFrom(patch: Record<string, number | string>, prefix: string):
|
||||
return Number.isFinite(value) ? value : undefined;
|
||||
};
|
||||
const wing: WingPatch = {};
|
||||
// 설치 값은 폼이 "있음"/"없음" 문자열로 낸다(백엔드 `_wing_spec`과 같은 규약).
|
||||
if (patch[prefix] !== undefined) wing.installed = patch[prefix] !== "없음";
|
||||
// 설치 값 글(있음 · 없음 · BOX 날개벽 · 흙막이) — 백엔드 `_wing_spec` 과 같은 판정.
|
||||
if (patch[prefix] !== undefined) wing.installed = wingInstalled(patch[prefix]);
|
||||
const heightM = num(`${prefix}_height_m`);
|
||||
if (heightM !== undefined) wing.height_m = heightM;
|
||||
const lengthM = num(`${prefix}_length_m`);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { FORD_BRIDGE_DEFAULT_WIDTH_M } from "@config/config_frontend";
|
||||
import type { StructureType } from "../B05_Profile/B05_Profile_Api_Structures";
|
||||
import { PIPE_MATCH_M } from "../A00_Common/design/design_selection";
|
||||
import { wingInstalled } from "@util/common_util_culvert_sets";
|
||||
import type { SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||||
|
||||
type Merged = Record<string, string | number>;
|
||||
@@ -50,7 +51,9 @@ function keepWings(
|
||||
for (const [wing, prefix] of wings) {
|
||||
// 백엔드 `_wing_spec` 과 같은 채움 — 설치는 「없음」이 아니면 있음 · 치수는 기본 0/0/45.
|
||||
const install = String(defaults[prefix] ?? "") === "없음" ? "없음" : "있음";
|
||||
keepSpec(merged, stored, prefix, wing.installed ? "있음" : "없음", install);
|
||||
// 저장 글이 그린 설치와 같은 뜻이면 그대로 둠(BOX 유입·유출 구조 날개벽 · 흙막이 글을 안 덮음)
|
||||
if (stored?.[prefix] === undefined || wingInstalled(stored[prefix]) !== wing.installed)
|
||||
keepSpec(merged, stored, prefix, wing.installed ? "있음" : "없음", install);
|
||||
keepSpec(
|
||||
merged,
|
||||
stored,
|
||||
|
||||
@@ -90,8 +90,11 @@ def collect_items(
|
||||
|
||||
한 건 = `{type_id, placement, chainage_m, start_m, end_m, options, 관연장, summary_item,
|
||||
복사본}` ·
|
||||
`복사본` = 그 구조물 집계 항목 복사본 폴더의 구조물 id(관 지점은 `pipe@측점` · 26-2b).
|
||||
`복사본` = 그 구조물 집계 항목 복사본 폴더의 구조물 id(관 지점은 `pipe@측점` · 26-2b) ·
|
||||
기타는 `그룹` = 사용자가 적은 상세 종류 이름(52-14).
|
||||
"""
|
||||
from M02_MasterTemplete.M02_Table_Fill_Spec import etc_label
|
||||
|
||||
types = types or structure_type_map()
|
||||
lengths = pipe_lengths or {}
|
||||
items: list[dict[str, Any]] = []
|
||||
@@ -111,6 +114,7 @@ def collect_items(
|
||||
"summary_item": item.summary_item,
|
||||
"복사본": item.structure_id or f"#{index}",
|
||||
"group_items": item.group_items,
|
||||
**etc_label(item.type_id, item.options),
|
||||
}
|
||||
)
|
||||
for point in read_pipe_points_file(pipe_points_path_in(Path(project_root))):
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
집계 항목 = 잇기 `group_items[{잇기}{n}]` · 복사본 `{구조물 id}__{잇기}{n}` · 옵션 = 그 컨테이너 값.
|
||||
열 바인딩이 읽는 칸이 갈래마다 달라(독립 기슭막이 `form` · `length_m` … · 관 `{유입|유출}_revet_*`)
|
||||
컨테이너 값을 그 칸들에 함께 얹음 — 어느 출처로 읽어도 그 컨테이너 값.
|
||||
짝: `A00_Common/design/design_structure_pipe.ts` 선택 컨테이너(머리 · 잇기 · 제목) — 시험이 대조.
|
||||
짝: `A00_Common/design/design_structure_pipe.ts` · `_road.ts` 선택 컨테이너(머리 · 잇기 · 제목) —
|
||||
시험이 대조.
|
||||
기타(52-14) = 사용자가 적은 상세 종류 이름이 집계 건 이름(`그룹`) · 비면 「기타」.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -21,8 +23,16 @@ SPEC_CONTAINERS: dict[str, tuple[tuple[str, str, str], ...]] = {
|
||||
("chute_add", "gchuteadd", "도수로/돌붙임"),
|
||||
),
|
||||
"ford_pavement": (("ford_chute", "gfordpavechute", "도수로/돌붙임"),),
|
||||
"etc": (("etc_detail", "getcdetail", "상세 내용"),),
|
||||
}
|
||||
_TEXT = ("structure", "form")
|
||||
_TEXT = ("structure", "form", "note")
|
||||
|
||||
|
||||
def etc_label(type_id: str, options: dict[str, Any] | None) -> dict[str, str]:
|
||||
"""기타 집계 건 이름 `{그룹: 상세 종류 이름}`(비면 「기타」) — 다른 종류는 빈 묶음."""
|
||||
if type_id != "etc":
|
||||
return {}
|
||||
return {"그룹": str((options or {}).get("name") or "").strip() or "기타"}
|
||||
|
||||
|
||||
def _as_options(values: dict[str, Any]) -> dict[str, Any]:
|
||||
@@ -58,7 +68,7 @@ def container_rows(item: dict[str, Any], links: dict[str, Any]) -> list[dict[str
|
||||
"options": {**item["options"], **_as_options(found[n])},
|
||||
"summary_item": links.get(gid),
|
||||
"복사본": f"{item['복사본']}{GROUP_JOIN}{gid}",
|
||||
"그룹": f"{title} {n}",
|
||||
"그룹": f"{item.get('그룹', title)} {n}",
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
@@ -28,8 +28,8 @@ const TAG: Record<NonNullable<SpecNameRow["tag"]>, OptionsTextKey> = {
|
||||
optional: "SpecOptional",
|
||||
select: "FormSelect",
|
||||
number: "FormNumber",
|
||||
auto: "FormAuto",
|
||||
text: "FormText",
|
||||
auto: "FormAuto",
|
||||
bool: "FormBool",
|
||||
};
|
||||
|
||||
|
||||
@@ -231,6 +231,12 @@ export function wingSlabExtendM(
|
||||
* 날개벽 한쪽 제원 + 그 각도가 만드는 바닥판 연장량.
|
||||
* 연장량 = 길이 × cos(각도) — 각도는 관축(계류 방향) 기준 벌어짐각이다.
|
||||
*/
|
||||
/** 날개벽을 세우는 설치 값인가 — BOX암거 유입·유출 구조(표 02) 없음 · 날개벽 · 흙막이 · 옛 있음 = 날개벽.
|
||||
* 날개벽만 세움 · 빈 값은 세움(기본). 파이썬 `_wing_spec` 과 짝. */
|
||||
export function wingInstalled(value: unknown): boolean {
|
||||
return !["없음", "흙막이"].includes(String(value ?? "").trim());
|
||||
}
|
||||
|
||||
function wingSpec(
|
||||
values: Record<string, unknown>,
|
||||
defaults: Record<string, unknown>,
|
||||
@@ -241,8 +247,7 @@ function wingSpec(
|
||||
const length = num(values[`${prefix}_length_m`], num(defaults[`${prefix}_length_m`], 0.0));
|
||||
const angle = num(values[`${prefix}_angle_deg`], num(defaults[`${prefix}_angle_deg`], 45.0));
|
||||
const height = num(values[`${prefix}_height_m`], num(defaults[`${prefix}_height_m`], 0.0));
|
||||
// BOX암거 유입·유출 구조(표 02) — 없음 · 날개벽 · 흙막이 · 옛 값 있음 = 날개벽. 날개벽만 세움.
|
||||
const installed = !["없음", "흙막이"].includes(String(install ?? "").trim());
|
||||
const installed = wingInstalled(install);
|
||||
return {
|
||||
installed,
|
||||
height_m: height,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"판": 10,
|
||||
"판": 12,
|
||||
"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`가 새 키로 옮긴다.",
|
||||
"구조물군": {
|
||||
@@ -382,13 +382,14 @@
|
||||
},
|
||||
{
|
||||
"key": "wing_in",
|
||||
"label": "날개벽(유입)",
|
||||
"label": "유입 구조",
|
||||
"input": "select",
|
||||
"choices": [
|
||||
"있음",
|
||||
"없음"
|
||||
"없음",
|
||||
"날개벽",
|
||||
"흙막이"
|
||||
],
|
||||
"default": "있음",
|
||||
"default": "날개벽",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
@@ -417,13 +418,14 @@
|
||||
},
|
||||
{
|
||||
"key": "wing_out",
|
||||
"label": "날개벽(유출)",
|
||||
"label": "유출 구조",
|
||||
"input": "select",
|
||||
"choices": [
|
||||
"있음",
|
||||
"없음"
|
||||
"없음",
|
||||
"날개벽",
|
||||
"흙막이"
|
||||
],
|
||||
"default": "있음",
|
||||
"default": "날개벽",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
@@ -3335,7 +3337,23 @@
|
||||
"계곡통과시설",
|
||||
"BOX_본체규격",
|
||||
"날개벽"
|
||||
]
|
||||
],
|
||||
"이름": {
|
||||
"body.size": "본체 규격",
|
||||
"body.size=2.0×2.0": "2.0 × 2.0",
|
||||
"body.size=3.0×3.0": "3.0 × 3.0",
|
||||
"body.size=사용자 입력": "사용자 입력",
|
||||
"body.width": "가로 (m)",
|
||||
"body.height": "세로 (m)",
|
||||
"inlet.structure": "구조",
|
||||
"inlet.structure=없음": "없음",
|
||||
"inlet.structure=날개벽": "날개벽",
|
||||
"inlet.structure=흙막이": "흙막이",
|
||||
"outlet.structure": "구조",
|
||||
"outlet.structure=없음": "없음",
|
||||
"outlet.structure=날개벽": "날개벽",
|
||||
"outlet.structure=흙막이": "흙막이"
|
||||
}
|
||||
},
|
||||
"ford_pavement": {
|
||||
"동작": [
|
||||
@@ -3465,6 +3483,33 @@
|
||||
"형상": {}
|
||||
}
|
||||
],
|
||||
"box_culvert": [
|
||||
{
|
||||
"id": "gboxbody",
|
||||
"이름": "본체",
|
||||
"옵션": [
|
||||
"body_width_m",
|
||||
"body_height_m"
|
||||
],
|
||||
"형상": {}
|
||||
},
|
||||
{
|
||||
"id": "gboxin",
|
||||
"이름": "유입 구조",
|
||||
"옵션": [
|
||||
"wing_in"
|
||||
],
|
||||
"형상": {}
|
||||
},
|
||||
{
|
||||
"id": "gboxout",
|
||||
"이름": "유출 구조",
|
||||
"옵션": [
|
||||
"wing_out"
|
||||
],
|
||||
"형상": {}
|
||||
}
|
||||
],
|
||||
"ford_pavement": [
|
||||
{
|
||||
"id": "gfordpaveslope",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -56,6 +56,33 @@ if (process.argv[2] === "road") {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// 기타(PLAN 52-14) — 선택 컨테이너 「상세 내용」 비고 글 · 길이 상한 · 비우기 · 이름 표
|
||||
if (process.argv[2] === "etc") {
|
||||
const spec = STRUCTURE_SPECS.etc;
|
||||
const old = { name: "배수판", etc_detail_2_note: "옛 비고", etc_detail_5_note: "둘째" };
|
||||
const state = s.readState(spec, old);
|
||||
const edited = JSON.parse(JSON.stringify(state));
|
||||
edited.detail[0].note = " 새 비고 ";
|
||||
edited.detail[1].note = "";
|
||||
edited.detail.push({ note: "가".repeat(s.TEXT_MAX + 30) });
|
||||
const out = s.writeOptions(spec, edited);
|
||||
process.stdout.write(
|
||||
JSON.stringify({
|
||||
same: spec === road.ETC_SPEC,
|
||||
containers: spec.containers.map((c) => [c.id, c.title, c.required, c.prefix, c.link]),
|
||||
items: spec.containers[0].items.map((i) => [i.id, i.label, i.kind]),
|
||||
state,
|
||||
edited: out,
|
||||
max: s.TEXT_MAX,
|
||||
longLen: out.etc_detail_3_note?.length ?? 0,
|
||||
keys: s.specKeys(spec),
|
||||
names: s.nameTable(spec),
|
||||
links: [1, 2].map((n) => s.linkOf(spec.containers[0], n)),
|
||||
}),
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (process.argv[2] === "names") {
|
||||
process.stdout.write(JSON.stringify(s.nameTable(PIPE_SPEC)));
|
||||
process.exit(0);
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/* 박스암거 명세 단위검증 헬퍼(PLAN 52-2) — `design_structure_spec.ts` · `design_structure_box.ts` 를
|
||||
* 그 자리에서 트랜스파일해 Node 로 돌림(꼴만 import). 결과 = JSON 한 벌.
|
||||
* 인자 `names` 면 박스암거 이름 표(정의 `종류.box_culvert.이름`)만 냄.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const load = (file) => {
|
||||
const src = fs.readFileSync(path.join(ROOT, "A00_Common", "design", file), "utf8");
|
||||
const js = ts.transpileModule(src, {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
new Function("exports", "module", "require", js)(box.exports, box, require);
|
||||
return box.exports;
|
||||
};
|
||||
const s = load("design_structure_spec.ts");
|
||||
const b = load("design_structure_box.ts");
|
||||
const SPEC = b.BOX_SPEC;
|
||||
|
||||
if (process.argv[2] === "names") {
|
||||
process.stdout.write(JSON.stringify(s.nameTable(SPEC)));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
/** 화면이 하는 차례 그대로 — 옛 값 정리 → 명세 읽기 → 본체 칸 되찾기 → 저장 옵션. */
|
||||
const roundTrip = (options, edit) => {
|
||||
const state = s.readState(SPEC, b.boxOptionsIn(options));
|
||||
Object.assign(state.body[0], b.bodyOf(options));
|
||||
edit?.(state);
|
||||
return { state, out: { ...s.writeOptions(SPEC, state), ...b.bodyOptions(state.body[0]) } };
|
||||
};
|
||||
const body = SPEC.containers.find((c) => c.id === "body");
|
||||
const width = body.items.find((i) => i.id === "width");
|
||||
|
||||
const old = { body_width_m: 3, body_height_m: 2.5, wing_in: "있음", wing_in_length_m: 2 };
|
||||
const out = {
|
||||
containers: SPEC.containers.map((c) => [c.id, c.title, c.required]),
|
||||
links: SPEC.containers.map((c) => s.linkOf(c, 1)),
|
||||
sizes: body.items[0].choices.map((c) => c.value),
|
||||
structures: SPEC.containers[1].items[0].choices.map((c) => c.value),
|
||||
customHidden: s.branchOf(width, { size: "2.0×2.0" }),
|
||||
customShown: s.branchOf(width, { size: "사용자 입력" }),
|
||||
old: roundTrip(old),
|
||||
preset: roundTrip({ body_width_m: 2, body_height_m: 2, wing_out: "없음" }),
|
||||
toPreset: roundTrip(old, (st) => {
|
||||
st.body[0].size = "3.0×3.0";
|
||||
st.outlet[0].structure = "흙막이";
|
||||
}),
|
||||
toCustom: roundTrip({ body_width_m: 2, body_height_m: 2 }, (st) => {
|
||||
st.body[0].size = "사용자 입력";
|
||||
st.body[0].width = "2.4";
|
||||
st.body[0].height = "1.8";
|
||||
}),
|
||||
blank: roundTrip({}),
|
||||
keys: s.specKeys(SPEC),
|
||||
retired: SPEC.retired,
|
||||
};
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -0,0 +1,106 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""공용 틀 글 손 입력 · 기타 선택 컨테이너 · 기타 이름 = 집계 건 이름 — PLAN 52-14.
|
||||
|
||||
순수 규칙은 Node 헬퍼(`helper_52_structure_spec.cjs etc`)가 돌림 ·
|
||||
화면은 ORCA `ui_checks/52-10_노면공_기타.js`(52-14 덧).
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
from M02_MasterTemplete import M02_Table_Fill as fill
|
||||
from M02_MasterTemplete.M02_Table_Fill_Spec import SPEC_CONTAINERS, container_rows, etc_label
|
||||
from resources.tester.test_structures_summary_snapshot import ( # noqa: F401 — world 고정물
|
||||
MS,
|
||||
RV,
|
||||
_save,
|
||||
world,
|
||||
)
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(os.path.dirname(HERE))
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out():
|
||||
proc = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_52_structure_spec.cjs"), "etc"],
|
||||
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 out["same"] is True
|
||||
assert out["containers"] == [["detail", "상세 내용", False, "etc_detail", "getcdetail"]]
|
||||
assert out["items"] == [["note", "비고", "text"]]
|
||||
assert out["links"] == ["getcdetail1", "getcdetail2"]
|
||||
|
||||
|
||||
def test_글_저장_비우기_길이_상한(out):
|
||||
assert out["state"] == {"detail": [{"note": "옛 비고"}, {"note": "둘째"}]} # n 차례대로 당김
|
||||
edited = out["edited"]
|
||||
assert edited["etc_detail_1_note"] == "새 비고" # 앞뒤 빈칸 뺌
|
||||
assert edited["etc_detail_2_note"] == "" # 비운 인스턴스도 자리를 지킴(잇기 · 집계 줄)
|
||||
assert out["longLen"] == out["max"] == 100
|
||||
assert out["keys"] == {"keys": [], "prefixes": ["etc_detail_"]}
|
||||
|
||||
|
||||
def test_이름_표에_글_항목(out):
|
||||
assert out["names"] == {"detail": "상세 내용", "detail.note": "비고"}
|
||||
|
||||
|
||||
def test_기타_이름이_집계_건_이름():
|
||||
assert etc_label("etc", {"name": " 배수판 "}) == {"그룹": "배수판"}
|
||||
assert etc_label("etc", {"name": ""}) == {"그룹": "기타"}
|
||||
assert etc_label("etc", {}) == {"그룹": "기타"}
|
||||
assert etc_label("pipe", {"name": "x"}) == {}
|
||||
item = {
|
||||
"type_id": "etc",
|
||||
"options": {"name": "배수판", "etc_detail_1_note": "왼쪽", "etc_detail_2_note": ""},
|
||||
"복사본": "st1",
|
||||
"그룹": "배수판",
|
||||
}
|
||||
rows = container_rows(item, {"getcdetail1": RV, "getcdetail2": MS})
|
||||
assert [r["그룹"] for r in rows] == ["배수판 1", "배수판 2"]
|
||||
assert [r["복사본"] for r in rows] == ["st1__getcdetail1", "st1__getcdetail2"]
|
||||
assert [r["summary_item"] for r in rows] == [RV, MS]
|
||||
assert rows[0]["options"]["note"] == "왼쪽"
|
||||
|
||||
|
||||
def test_집계_모음에_기타_이름과_컨테이너_건(world): # noqa: F811 — 고정물
|
||||
root = world["root"]
|
||||
etc = {
|
||||
"structure_id": "etc1",
|
||||
"type_id": "etc",
|
||||
"placement": "point",
|
||||
"chainage_m": 60.0,
|
||||
"options": {"name": "배수판", "etc_detail_1_note": "왼쪽"},
|
||||
"group_items": {"getcdetail1": RV},
|
||||
}
|
||||
blank = {**etc, "structure_id": "etc2", "chainage_m": 80.0, "options": {"name": ""}}
|
||||
_save(root, [etc, blank], [])
|
||||
items = {i["복사본"]: i for i in fill.collect_items(root)}
|
||||
assert items["etc1"]["그룹"] == "배수판"
|
||||
assert items["etc1__getcdetail1"]["그룹"] == "배수판 1"
|
||||
assert items["etc1__getcdetail1"]["summary_item"] == RV
|
||||
assert items["etc2"]["그룹"] == "기타"
|
||||
|
||||
|
||||
def test_서버_컨테이너_표는_기타_명세와_짝():
|
||||
with open(
|
||||
os.path.join(ROOT, "A00_Common/design/design_structure_road.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["etc"])
|
||||
@@ -0,0 +1,90 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""횡단배수 › 박스암거(표 02 고친 판) 명세 — PLAN 52-2.
|
||||
|
||||
순수 규칙은 Node 헬퍼(`helper_52_structure_spec_box.cjs`)가 돌림 · 화면은 ORCA `ui_checks/52-2_박스암거_컨테이너.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))
|
||||
|
||||
|
||||
def _node(*args):
|
||||
proc = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_52_structure_spec_box.cjs"), *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out():
|
||||
return _node()
|
||||
|
||||
|
||||
def test_컨테이너_셋_모두_필수(out):
|
||||
assert out["containers"] == [
|
||||
["body", "본체", True],
|
||||
["inlet", "유입 구조", True],
|
||||
["outlet", "유출 구조", True],
|
||||
]
|
||||
assert out["links"] == ["gboxbody", "gboxin", "gboxout"]
|
||||
|
||||
|
||||
def test_선택지_표_02_그대로(out):
|
||||
assert out["sizes"] == ["2.0×2.0", "3.0×3.0", "사용자 입력"]
|
||||
assert out["structures"] == ["없음", "날개벽", "흙막이"]
|
||||
|
||||
|
||||
def test_사용자_입력일_때만_가로_세로_칸(out):
|
||||
assert out["customHidden"] is None
|
||||
assert out["customShown"] == {"key": "body_width_m"}
|
||||
|
||||
|
||||
def test_옛_값_읽기_규격_밖은_사용자_입력_있음은_날개벽(out):
|
||||
st = out["old"]["state"]
|
||||
assert st["body"][0] == {"size": "사용자 입력", "width": "3", "height": "2.5"}
|
||||
assert st["inlet"][0]["structure"] == "날개벽"
|
||||
back = out["old"]["out"]
|
||||
assert back == {"body_width_m": 3, "body_height_m": 2.5, "wing_in": "날개벽"}
|
||||
# 옛 날개벽 치수는 폼이 안 적음(병합이 이어 붙임 = 지난 정의 값)
|
||||
assert "wing_in_length_m" not in back
|
||||
assert "wing_in_length_m" in out["retired"]
|
||||
|
||||
|
||||
def test_규격은_폭_높이_수로_저장_규격_칸은_저장_안_함(out):
|
||||
assert out["preset"]["state"]["body"][0]["size"] == "2.0×2.0"
|
||||
assert out["preset"]["out"] == {"body_width_m": 2, "body_height_m": 2, "wing_out": "없음"}
|
||||
to = out["toPreset"]["out"]
|
||||
assert to["body_width_m"] == 3 and to["body_height_m"] == 3
|
||||
assert to["wing_out"] == "흙막이"
|
||||
assert out["toCustom"]["out"] == {"body_width_m": 2.4, "body_height_m": 1.8}
|
||||
assert "size" not in json.dumps(out["keys"])
|
||||
|
||||
|
||||
def test_빈_값은_빈_채로(out):
|
||||
assert out["blank"]["out"] == {}
|
||||
assert out["blank"]["state"]["body"][0]["size"] == ""
|
||||
|
||||
|
||||
def test_정의_파일에_이름_표_그룹_선택지가_올라_있음():
|
||||
names = _node("names")
|
||||
with open(
|
||||
os.path.join(ROOT, "resources", "master_template", "구조물정의.json"), encoding="utf-8"
|
||||
) as f:
|
||||
doc = json.load(f)
|
||||
assert set(names) == set(doc["종류"]["box_culvert"]["이름"])
|
||||
assert [g["id"] for g in doc["그룹"]["box_culvert"]] == ["gboxbody", "gboxin", "gboxout"]
|
||||
box = next(t for t in doc["types"] if t["type_id"] == "box_culvert")
|
||||
wing = {o["key"]: o for o in box["options"]}
|
||||
for key in ("wing_in", "wing_out"):
|
||||
assert wing[key]["choices"] == ["없음", "날개벽", "흙막이"]
|
||||
@@ -38,7 +38,7 @@ def test_새_관만_기본값_빈_키에만():
|
||||
assert fresh.options["pipe_kind"] == "파형강관" and fresh.options["inlet_type"] == "기슭막이"
|
||||
box = PipePoint(chainage_m=20.0, source="stream", facility="box_culvert")
|
||||
basins._apply_new_defaults([box])
|
||||
assert box.options["body_width_m"] == 2.0 and box.options["wing_in"] == "있음"
|
||||
assert box.options["body_width_m"] == 2.0 and box.options["wing_in"] == "날개벽"
|
||||
|
||||
|
||||
def test_원본에_있던_관은_빈칸_그대로(monkeypatch):
|
||||
|
||||
@@ -547,8 +547,9 @@ def test_box_culvert_wing_wall_options_defined():
|
||||
for side in ("in", "out"):
|
||||
toggle = options[f"wing_{side}"]
|
||||
assert toggle.phase == "b05" and not toggle.required
|
||||
assert toggle.choices == ["있음", "없음"]
|
||||
assert toggle.default == "있음", f"wing_{side} 기본 설치"
|
||||
# 표 02 고친 판(PLAN 52-2) — 유입 · 유출 구조 = 없음 · 날개벽 · 흙막이
|
||||
assert toggle.choices == ["없음", "날개벽", "흙막이"]
|
||||
assert toggle.default == "날개벽", f"wing_{side} 기본 구조"
|
||||
for dim, value in expected.items():
|
||||
option = options[f"wing_{side}_{dim}"]
|
||||
assert option.phase == "b05" and not option.required, f"wing_{side}_{dim}"
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
* 기대 수치:
|
||||
* 포장 — containers ["detail"] · 상세 내용 · 포장사양 선택지 ["콘크리트"] · 높이 0.35 · 잇기 지정 ·
|
||||
* 종류 단위 잇기 줄 가림 · 옛 상세 칸(두께 · 줄눈 …) 안 보임 · 길이 · 전 · 후 칸 보임
|
||||
* 기타 — 「상세 종류 이름」 글 칸 = 「52-10 시험 기타」 · 목록 이름도 그 글 · 종류 단위 잇기 줄 보임 · 지정
|
||||
* 기타 — 「상세 종류 이름」 글 칸 = 「52-10 시험 기타」 · 목록 이름도 그 글 ·
|
||||
* 선택 컨테이너 「상세 내용」(52-14) 셋 추가 · 비고 글(길이 상한 100) · 1 · 3 잇기 · 2 빼기 →
|
||||
* 둘 남음(비고 ["52-14 비고", ""] · 잇기 둘 다 · 3 의 잇기가 2 로 당겨짐) · 종류 단위 잇기 줄 가림
|
||||
* verify — 같은 값 · 같은 모양(B05 = B06)
|
||||
*/
|
||||
(async () => {
|
||||
@@ -75,6 +77,13 @@
|
||||
paveSpec: specCtl("spec")?.value ?? null,
|
||||
height: specCtl("height")?.value ?? null,
|
||||
containerLink: !!boxes[0]?.querySelector(".b05-structure__summary-name.is-assigned"),
|
||||
containerLinks: boxes.map(
|
||||
(b) => !!b.querySelector(".b05-structure__summary-name.is-assigned"),
|
||||
),
|
||||
notes: boxes.map(
|
||||
(b) => b.querySelector('[data-spec-item="detail.note"] input')?.value ?? null,
|
||||
),
|
||||
noteMax: spec()?.querySelector('[data-spec-item="detail.note"] input')?.maxLength ?? null,
|
||||
typeSummaryVisible: vis(typeSummary()),
|
||||
typeSummaryAfterPosition: after === typeSummary(),
|
||||
typeLink: !!typeSummary().querySelector(".b05-structure__summary-name.is-assigned"),
|
||||
@@ -152,7 +161,28 @@
|
||||
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"]'));
|
||||
// 52-14 — 상세 내용 셋 추가 · 비고 · 1 · 3 잇기 · 2 빼기
|
||||
for (let k = 0; k < 3; k++) {
|
||||
spec().querySelector('[data-spec-add="detail"]').click();
|
||||
await w(200);
|
||||
}
|
||||
const notes = () => [...spec().querySelectorAll('[data-spec-item="detail.note"] input')];
|
||||
for (const [k, text] of [
|
||||
[0, "52-14 비고"],
|
||||
[1, "지울 것"],
|
||||
]) {
|
||||
notes()[k].value = text;
|
||||
notes()[k].dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await w(200);
|
||||
}
|
||||
const pickBtns = () => [
|
||||
...spec().querySelectorAll('[data-spec-link] [data-b05-summary="pick"]'),
|
||||
];
|
||||
row.picked = await linkFirstLeaf(pickBtns()[0]);
|
||||
row.picked3 = await linkFirstLeaf(pickBtns()[2]);
|
||||
row.addedShape = shape(item.typeId);
|
||||
spec().querySelectorAll('[data-spec-remove="detail"]')[1].click();
|
||||
await w(300);
|
||||
}
|
||||
Object.assign(row, shape(item.typeId), { phase: "edit" });
|
||||
sec().querySelector(".is-primary").click(); // 추가
|
||||
@@ -207,9 +237,11 @@
|
||||
!!etc &&
|
||||
!etc.missing &&
|
||||
etc.name === NAME &&
|
||||
etc.containers.length === 0 &&
|
||||
etc.typeSummaryVisible &&
|
||||
etc.typeSummaryAfterPosition &&
|
||||
etc.typeLink;
|
||||
JSON.stringify(etc.containers) === '["detail","detail"]' &&
|
||||
JSON.stringify(etc.legend) === '["상세 내용 1","상세 내용 2"]' &&
|
||||
JSON.stringify(etc.notes) === '["52-14 비고",""]' &&
|
||||
JSON.stringify(etc.containerLinks) === "[true,true]" &&
|
||||
etc.noteMax === 100 &&
|
||||
!etc.typeSummaryVisible;
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
|
||||
@@ -39,7 +39,9 @@
|
||||
/** 구조물집계표 채움 줄 수(전구간 고정 줄 뺌) — 52-1b. */
|
||||
const filledRows = async () =>
|
||||
(await m02Api.readFilled(projectId, "구조물집계표")).문서.줄.filter((r) => !r.고정).length;
|
||||
const spec = () => document.querySelector(".b05-structure__spec");
|
||||
// 명세 폼은 둘(관 시설 · 일반 구조물 52-10) — 보이는 것
|
||||
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 ctl = (c, item, i = 0) =>
|
||||
box(c, i)?.querySelector(`[data-spec-item="${c}.${item}"]`)?.querySelector("select, input");
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
/* 52-2 — B05 · B06 좌측 「구조물 배치」 박스암거 = 사용자 구조물 표 02 고친 판(컨테이너 셋)
|
||||
* 시작 화면: B05 종단설계(#/b05-profile) · 검증 프로젝트(sub3_검증) · 목록에 「BOX암거」
|
||||
* 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m522 = "단계";` 를 붙여
|
||||
* orca eval --expression "window.__m522='edit'; $(cat 이 파일)" → 고치고 [저장] (원래 관 지점은 sessionStorage 에 둠)
|
||||
* (orca reload 로 다시 열기)
|
||||
* orca eval --expression "window.__m522='verify'; $(cat 이 파일)" → 다시 연 값 · B06 같은 그리기
|
||||
* orca eval --expression "window.__m522='restore'; $(cat 이 파일)" → 원래 관 지점으로 되돌림(PUT) · 초안 비움
|
||||
* (연결이 끊기면 결과는 `sessionStorage.getItem("__522_out_{단계}")` 로 읽음)
|
||||
* 쓰는 것: 검증 프로젝트 pipe_points.json(되돌림 단계가 원래 값으로 다시 씀)
|
||||
* 기대 수치(edit): 컨테이너 3(본체 · 유입 구조 · 유출 구조 · 모두 필수 · [추가] 0) · 옛 값 3 × 2.5 = 사용자 입력 ·
|
||||
* 옛 「있음」 = 날개벽 · 규격 2.0 × 2.0 고르면 숫자 칸 0 · 사용자 입력 고르면 가로 · 세로 2 ·
|
||||
* 구조 선택지 없음/날개벽/흙막이 · 잇기 본체 · 유입 지정 · 유출 지정 뒤 풂 ·
|
||||
* 저장 = body 2.4 × 1.8 · wing_in 흙막이 · wing_out 없음 · group_items gboxbody · gboxin
|
||||
* 기대 수치(verify): 저장 값 그대로 · 같은 그리기(B05 = B06)
|
||||
*/
|
||||
(async () => {
|
||||
const MODE = window.__m522 || "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 fetchApi = await import(modUrl("/B04_PreProcess/B04_PreProcess_Api_Fetch.ts"));
|
||||
const isBox = (p) => p.facility === "box_culvert";
|
||||
// 보이는 명세 폼(배관 · BOX 가 각자 폼을 가짐 — 숨은 것은 뺌)
|
||||
const spec = () =>
|
||||
[...document.querySelectorAll(".b05-structure__spec")].find((n) => n.offsetParent !== null);
|
||||
const box = (c) => spec()?.querySelector(`[data-spec-container="${c}"]`);
|
||||
const field = (c, item) => box(c)?.querySelector(`[data-spec-item="${c}.${item}"]`);
|
||||
const ctl = (c, item) => field(c, item)?.querySelector("select, input");
|
||||
const set = async (c, item, v) => {
|
||||
const el = ctl(c, item);
|
||||
el.value = v;
|
||||
el.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await w(200);
|
||||
};
|
||||
const labels = (c, item) => [...ctl(c, item).options].filter((o) => o.value).map((o) => o.text);
|
||||
const pickBox = async () => {
|
||||
await until(() => document.querySelectorAll(".b05-route__irregular-list li").length > 0);
|
||||
const li = [...document.querySelectorAll(".b05-route__irregular-list li")].find((l) =>
|
||||
/BOX암거$/.test(l.textContent.trim()),
|
||||
);
|
||||
// 다시 열면 고른 것이 복원돼 있음 — 누르면 풀리므로 풀렸으면 한 번 더(재선택 = 해제)
|
||||
(li.querySelector("button") ?? li).click();
|
||||
await w(800);
|
||||
if (!box("body")) (li.querySelector("button") ?? li).click();
|
||||
await until(() => !!box("body"));
|
||||
await w(500);
|
||||
};
|
||||
const shape = () =>
|
||||
[...spec().querySelectorAll("[data-spec-container]")].map((b) => ({
|
||||
c: b.dataset.specContainer,
|
||||
legend: b.querySelector("legend").firstChild.textContent,
|
||||
items: [...b.querySelectorAll("[data-spec-item]")].map((n) => n.dataset.specItem),
|
||||
values: [...b.querySelectorAll("[data-spec-item]")].map(
|
||||
(n) => n.querySelector("select, input").value,
|
||||
),
|
||||
link: b.querySelector(".b05-structure__summary-name")?.textContent,
|
||||
}));
|
||||
const linked = (c) =>
|
||||
box(c).querySelector(".b05-structure__summary-name").classList.contains("is-assigned");
|
||||
const pickLink = async (c) => {
|
||||
box(c).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(500);
|
||||
};
|
||||
const out = { mode: MODE };
|
||||
|
||||
if (MODE === "edit") {
|
||||
const before = await fetchApi.fetchDetailPipePoints(projectId);
|
||||
// 원래 값은 처음 한 번만 둠(끊겨 다시 돌려도 고친 값으로 덮지 않음 · 되돌림이 비움)
|
||||
if (!sessionStorage.getItem("__522_original"))
|
||||
sessionStorage.setItem("__522_original", JSON.stringify(before.pipe_points));
|
||||
out.original = before.pipe_points.find(isBox)?.options;
|
||||
await pickBox();
|
||||
const s0 = shape();
|
||||
out.containers = s0.map((b) => [b.c, b.legend]);
|
||||
out.allVisible = ["body", "inlet", "outlet"].every((c) => box(c)?.offsetParent !== null);
|
||||
out.addButtons = spec().querySelectorAll("[data-spec-add]").length;
|
||||
out.oldRead = {
|
||||
body: s0[0].values,
|
||||
inlet: ctl("inlet", "structure").value,
|
||||
outlet: ctl("outlet", "structure").value,
|
||||
};
|
||||
out.sizes = labels("body", "size");
|
||||
out.structures = labels("inlet", "structure");
|
||||
await set("body", "size", "2.0×2.0");
|
||||
out.presetNumberFields = ["width", "height"].filter((i) => field("body", i)).length;
|
||||
await set("body", "size", "사용자 입력");
|
||||
out.customNumberFields = ["width", "height"]
|
||||
.filter((i) => field("body", i))
|
||||
.map((i) => field("body", i).querySelector("label, span")?.textContent ?? i);
|
||||
await set("body", "width", "2.4");
|
||||
await set("body", "height", "1.8");
|
||||
await set("inlet", "structure", "흙막이");
|
||||
await set("outlet", "structure", "없음");
|
||||
await pickLink("body");
|
||||
await pickLink("inlet");
|
||||
await pickLink("outlet");
|
||||
out.linkPicked = ["body", "inlet", "outlet"].map(linked);
|
||||
box("outlet").querySelector('[data-b05-summary="clear"]').click();
|
||||
await w(300);
|
||||
out.linkOutletCleared = !linked("outlet");
|
||||
document.querySelector('[data-dock-action="save"]').click();
|
||||
await w(500);
|
||||
out.saved = await until(() => !document.querySelector(".ui-overlay.is-active"), 60000);
|
||||
const after = await fetchApi.fetchDetailPipePoints(projectId);
|
||||
const p = after.pipe_points.find(isBox);
|
||||
out.file = p && { options: p.options, group_items: Object.keys(p.group_items ?? {}) };
|
||||
}
|
||||
|
||||
if (MODE === "verify") {
|
||||
await pickBox();
|
||||
const b05 = shape();
|
||||
out.reopened = b05.map((b) => [b.c, b.values, b.link]);
|
||||
out.linkReopened = ["body", "inlet", "outlet"].map(linked);
|
||||
location.hash = "#/b06-section";
|
||||
await until(() => location.hash.includes("b06"), 5000);
|
||||
await w(6000);
|
||||
await pickBox();
|
||||
const b06 = shape();
|
||||
out.sameDrawing = JSON.stringify(b05) === JSON.stringify(b06);
|
||||
if (!out.sameDrawing) out.diff = { b05, b06 };
|
||||
}
|
||||
|
||||
if (MODE === "restore") {
|
||||
const original = JSON.parse(sessionStorage.getItem("__522_original") || "null");
|
||||
if (!original) return JSON.stringify({ error: "원래 값 없음" });
|
||||
const keys = [
|
||||
"chainage_m",
|
||||
"source",
|
||||
"facility",
|
||||
"start_m",
|
||||
"end_m",
|
||||
"options",
|
||||
"summary_item",
|
||||
"group_items",
|
||||
];
|
||||
const points = original.map((p) =>
|
||||
Object.fromEntries(
|
||||
keys.filter((k) => p[k] !== undefined && p[k] !== null).map((k) => [k, p[k]]),
|
||||
),
|
||||
);
|
||||
await fetchApi.saveDetailPipePoints(projectId, points);
|
||||
for (const k of Object.keys(sessionStorage))
|
||||
if (k.startsWith("aislo:draft:pipes:") || k === "__522_original")
|
||||
sessionStorage.removeItem(k);
|
||||
const now = await fetchApi.fetchDetailPipePoints(projectId);
|
||||
const pick = (list) =>
|
||||
JSON.stringify(list.map((p) => [p.chainage_m, p.options ?? {}, p.group_items ?? {}]));
|
||||
out.restored = pick(now.pipe_points) === pick(original);
|
||||
}
|
||||
// 긴 단계는 ORCA 연결이 끊길 수 있어 결과를 남김 — `sessionStorage.__522_out_{단계}` 로 읽음
|
||||
sessionStorage.setItem(`__522_out_${MODE}`, JSON.stringify(out));
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
@@ -0,0 +1,83 @@
|
||||
/* 52-2b — B06 횡단 조정 줄(단 수 · 옵션 · 읽기)이 서는 자리 = 지금 보이는 명세 폼의 유입 · 유출 칸
|
||||
* 시작 화면: B06 횡단설계(#/b06-section) · 검증 프로젝트(sub3_검증) · 목록에 「배관 1」 「BOX암거」 「세월교 1」
|
||||
* 부르는 법: orca eval --expression "$(cat 이 파일)" — 쓰는 것 없음(가짜 조정 사본을 세웠다 내림)
|
||||
* 차례: 배관 1 → BOX암거 → 세월교 1 → 배관 1 (앞 시설 명세 폼이 숨은 채 남는 경우까지)
|
||||
* 기대 수치: 모든 단계 · 양쪽 — 줄이 숨은 조상 없이 보이는 자리 · 그 시설 명세 폼의 inlet/outlet 칸 ·
|
||||
* 숨은 명세 칸을 「쓸 수 있음」으로 보던 옛 판정 건수(oldWouldPickHidden)는 참고값
|
||||
*/
|
||||
(async () => {
|
||||
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 modUrl = (part) =>
|
||||
performance
|
||||
.getEntriesByType("resource")
|
||||
.map((e) => e.name)
|
||||
.filter((n) => n.includes(part))
|
||||
.pop();
|
||||
const dock = await import(modUrl("/B06_Section/B06_Section_UI_Adjust_Dock.ts"));
|
||||
const visibleSpec = () =>
|
||||
[...document.querySelectorAll(".b05-structure__spec")].find((n) => !n.closest("[hidden]"));
|
||||
const firstContainer = () =>
|
||||
visibleSpec()?.querySelector("[data-spec-container]")?.dataset.specContainer;
|
||||
const pick = async (name, head) => {
|
||||
await until(() => document.querySelectorAll(".b05-route__irregular-list li").length > 0);
|
||||
const li = [...document.querySelectorAll(".b05-route__irregular-list li")].find(
|
||||
(l) => l.offsetParent !== null && name.test(l.textContent.trim()),
|
||||
);
|
||||
(li.querySelector("button") ?? li).click();
|
||||
await w(800);
|
||||
if (firstContainer() !== head) (li.querySelector("button") ?? li).click();
|
||||
return until(() => firstContainer() === head, 8000);
|
||||
};
|
||||
// 옛 판정 — 바로 위 fieldset 만 봄
|
||||
const oldUsable = (t) => !!t.closest("fieldset") && !t.closest("fieldset").hidden;
|
||||
const fakePanel = (side) => {
|
||||
const panel = document.createElement("div");
|
||||
panel.dataset.side = side;
|
||||
panel.dataset.panelTitle = "52-2b 시험";
|
||||
const row = document.createElement("div");
|
||||
row.className = "b06-structure-panel__struct--tier";
|
||||
row.dataset.m522b = side;
|
||||
panel.append(row);
|
||||
return { panel, row };
|
||||
};
|
||||
|
||||
const steps = [
|
||||
[/배관 1$/, "pipe"],
|
||||
[/BOX암거$/, "body"],
|
||||
[/세월교 1$/, "ford"],
|
||||
[/배관 1$/, "pipe"],
|
||||
];
|
||||
const out = { steps: [], oldWouldPickHidden: 0 };
|
||||
for (const [name, head] of steps) {
|
||||
const step = { name: String(name), shown: await pick(name, head) };
|
||||
for (const side of ["inlet", "outlet"]) {
|
||||
// 숨은 명세 폼의 같은 칸 — 옛 판정이 「쓸 수 있음」으로 봤는지
|
||||
for (const s of document.querySelectorAll(
|
||||
`[data-spec-container="${side}"] .b05-structure__adjust-slot`,
|
||||
))
|
||||
if (s.closest("[hidden]") && oldUsable(s)) out.oldWouldPickHidden++;
|
||||
const { panel, row } = fakePanel(side);
|
||||
dock.adoptAdjustPanel(panel);
|
||||
step[side] = {
|
||||
visible: !row.closest("[hidden]"),
|
||||
container: row.closest("[data-spec-container]")?.dataset.specContainer ?? null,
|
||||
inShownForm: !!visibleSpec()?.contains(row),
|
||||
};
|
||||
dock.releaseAdjustPanel(panel);
|
||||
}
|
||||
out.steps.push(step);
|
||||
}
|
||||
out.pass = out.steps.every(
|
||||
(s) =>
|
||||
s.shown &&
|
||||
["inlet", "outlet"].every((k) => s[k].visible && s[k].inShownForm && s[k].container === k),
|
||||
);
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
Reference in New Issue
Block a user