Merge remote-tracking branch 'origin/dev' into sub_laptop_5
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
/* =============================================================================
|
||||
* design_structure_ford.ts
|
||||
* 횡단배수 › 세월교 명세 — 사용자 구조물 표 04(`docs/raw/구조물정의_표/04_횡단배수_세월교.md`) 그대로.
|
||||
*
|
||||
* 컨테이너 셋: 세월교(필수) · 유입 구조(필수) · 유출 구조(필수). 구조 · 정본 = 이 명세 · 이름만 정의(M02)에서 바꿈.
|
||||
* 저장 키 = pipe_points.json 옛 키 그대로(B06 세월교 세트 · B07 · B08 이 읽는 자리) — 새 키 = 유입 · 유출 구조
|
||||
* (`inlet_type` · `outlet_type` = 없음 · 날개벽 · 흙막이) · 고르면 옛 날개벽 설치 칸(`wing_in` · `wing_out`)도 함께 적음.
|
||||
* 월류 높이 = 월류 폭과 연동(폭을 바꾸면 필요 수심으로 채움 · 그 아래로는 안 받음) — 계산은 `design_structure_ford_calc`.
|
||||
* import 없음 — 시험이 이 파일만 옮겨 돌림.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { SpecContainer, SpecChoice, StructureSpec } from "./design_structure_spec";
|
||||
|
||||
/** 유입 · 유출 구조 — 날개벽만 옛 설치 칸이 「있음」. */
|
||||
const guard = (side: "in" | "out"): SpecChoice[] => [
|
||||
{ value: "없음", label: "없음", also: { [`wing_${side}`]: "없음" } },
|
||||
{ value: "날개벽", label: "날개벽", also: { [`wing_${side}`]: "있음" } },
|
||||
{ value: "흙막이", label: "흙막이", also: { [`wing_${side}`]: "없음" } },
|
||||
];
|
||||
|
||||
const guardContainer = (id: "inlet" | "outlet", title: string, link: string): SpecContainer => ({
|
||||
id,
|
||||
title,
|
||||
required: true,
|
||||
link,
|
||||
items: [
|
||||
{
|
||||
id: "structure",
|
||||
label: "구조",
|
||||
kind: "select",
|
||||
key: `${id}_type`,
|
||||
choices: guard(id === "inlet" ? "in" : "out"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const FORD_SPEC: StructureSpec = {
|
||||
typeId: "ford_bridge",
|
||||
containers: [
|
||||
{
|
||||
id: "ford",
|
||||
title: "세월교",
|
||||
required: true,
|
||||
link: "gford",
|
||||
items: [
|
||||
{
|
||||
id: "diameter",
|
||||
label: "관경",
|
||||
kind: "select",
|
||||
key: "pipe_diameter_mm",
|
||||
asNumber: true,
|
||||
choices: ["800", "1000", "1200", "1500"].map((v) => ({ value: v, label: v })),
|
||||
},
|
||||
{ id: "width", label: "월류 폭 (m)", kind: "number", key: "ford_width_m", step: 0.1 },
|
||||
{ id: "height", label: "월류 높이 (m)", kind: "number", key: "ford_height_m", step: 0.01 },
|
||||
{ id: "count", label: "수량 (련)", kind: "number", key: "pipe_count", step: 1 },
|
||||
],
|
||||
},
|
||||
guardContainer("inlet", "유입 구조", "gfordin"),
|
||||
guardContainer("outlet", "유출 구조", "gfordout"),
|
||||
],
|
||||
// 표 04 에 없는 옛 칸 — 화면에서 빼고 값은 남김(지난 정의 값)
|
||||
retired: [
|
||||
"pipe_kind",
|
||||
"wing_in_height_m",
|
||||
"wing_in_length_m",
|
||||
"wing_in_angle_deg",
|
||||
"wing_out_height_m",
|
||||
"wing_out_length_m",
|
||||
"wing_out_angle_deg",
|
||||
],
|
||||
};
|
||||
|
||||
/** 구조를 날개벽 설치 칸(옛 저장 · B06 조정창이 고치는 칸)에 맞춤 — 있음 = 날개벽 · 없음 = 날개벽이면 없음
|
||||
* (흙막이는 그대로) · 설치 칸이 없으면 구조 그대로. */
|
||||
export function withLegacyGuards(
|
||||
options: Record<string, string | number>,
|
||||
): Record<string, string | number> {
|
||||
const out = { ...options };
|
||||
for (const [type, wing] of [
|
||||
["inlet_type", "wing_in"],
|
||||
["outlet_type", "wing_out"],
|
||||
]) {
|
||||
if (out[wing] === "있음") out[type] = "날개벽";
|
||||
else if (out[wing] === "없음" && (out[type] ?? "날개벽") === "날개벽") out[type] = "없음";
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/* =============================================================================
|
||||
* design_structure_ford_calc.ts
|
||||
* 세월교 명세 계산 — 월류 폭 ↔ 월류 높이 연동(PLAN 52-4).
|
||||
*
|
||||
* 높이 = 담당 유역 설계유량을 그 월류 폭으로 넘기는 필요 수심(Manning 개수로 · `fordSection` 그대로).
|
||||
* 근거: Manning 공식 `resources/knowledge/technical_info/01_임도/03_계산정보/통수단면.md:23~33`
|
||||
* (KDS 44 40 00 2.7.1~2.7.2) · 조도계수 0.017 = 같은 파일 :59(콘크리트 수로 보통 · 울진 물넘이) ·
|
||||
* 경사 10 % = 실무 수리계산서 역산값(`config_frontend.FORD_SLOPE` · 지식DB 수치 없음).
|
||||
* 폭을 바꾸면 높이 칸을 이 값으로 채움 · 사용자는 올려 적을 수 있고 이 값 아래로는 안 받음.
|
||||
* 높이는 저장되는 입력값 — 서버는 받은 높이로 노면을 내림(`B06_Section_Engine_Culvert._ford_set`) · 재계산 안 함.
|
||||
* 설계유량이 없으면 null = 손 입력.
|
||||
* ========================================================================== */
|
||||
|
||||
import { fordSection } from "../../B05_Profile/B05_Profile_UI_Drainage_Facility_Fields";
|
||||
|
||||
/** 월류 폭(m) → 필요 월류 높이(m) · 0.01 m 올림. 유량 · 폭이 없으면 null. */
|
||||
export function fordHeightM(designFlowM3s: number | null, widthM: number): number | null {
|
||||
const section = fordSection(designFlowM3s, widthM);
|
||||
return section ? Math.ceil(section.depthM * 100 - 1e-9) / 100 : null;
|
||||
}
|
||||
|
||||
/** 높이 빈 칸 안내. */
|
||||
export function fordHeightHint(designFlowM3s: number | null, widthM: number): string {
|
||||
const need = fordHeightM(designFlowM3s, widthM);
|
||||
if (need !== null) return `필요 수심 ${need.toFixed(2)} 이상`;
|
||||
return designFlowM3s ? "월류 폭을 넣으면 셈" : "설계유량 없음 — 손 입력";
|
||||
}
|
||||
@@ -8,10 +8,11 @@
|
||||
* (`inlet_type` · `*_revet_*`)에 둠. 새 키 = 집수정 높이 · 선택 컨테이너(`soil_add_{n}_*` · `chute_add_{n}_*`).
|
||||
* 정함(PLAN 52): 유출 형태 = 흙막이 6가지 · 도수로/돌붙임 = 구조만 · 길이 = 전 + 후 맞물림.
|
||||
* 계산(관 길이 · 높이 안내)은 `design_structure_pipe_calc`.
|
||||
* import 없음 — 시험이 이 파일만 옮겨 돌림.
|
||||
* import = 같은 폴더 명세뿐 — 시험이 이 폴더 파일만 옮겨 돌림.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { SpecChoice, SpecItem, StructureSpec } from "./design_structure_spec";
|
||||
import { FORD_SPEC } from "./design_structure_ford";
|
||||
|
||||
const SOIL = "기슭막이"; // 저장 값(옛 키 그대로) — 화면 이름은 흙막이
|
||||
const BASIN = "집수정";
|
||||
@@ -184,4 +185,7 @@ export const PIPE_SPEC: StructureSpec = {
|
||||
};
|
||||
|
||||
/** 명세로 그리는 종류 — 종류 키 → 명세(표가 오는 대로 더함). */
|
||||
export const STRUCTURE_SPECS: Readonly<Record<string, StructureSpec>> = { pipe: PIPE_SPEC };
|
||||
export const STRUCTURE_SPECS: Readonly<Record<string, StructureSpec>> = {
|
||||
pipe: PIPE_SPEC,
|
||||
ford_bridge: FORD_SPEC,
|
||||
};
|
||||
|
||||
@@ -53,6 +53,8 @@ import {
|
||||
} 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 { 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";
|
||||
import {
|
||||
hasLinks,
|
||||
@@ -250,45 +252,31 @@ export function createFacilityOptionsForm(
|
||||
root.className = "b05-drainage__facility";
|
||||
root.hidden = true;
|
||||
|
||||
// ── 관종 · 관경 한 행 — 세월교 몫(배관은 아래 명세 폼 · PLAN 52-1) ─────────
|
||||
const pipeMaterial = optionalSelect("— 안 정함 —", ["흄관", "VR관", "파형강관"]);
|
||||
const pipeDiameter = document.createElement("select");
|
||||
pipeDiameter.replaceChildren(
|
||||
new Option("— 안 정함 —", ""),
|
||||
...["800", "1000", "1200", "1500"].map((size) => new Option(`Ø${size}`, size)),
|
||||
);
|
||||
const pipeRow = grid(
|
||||
labeled("관종", pipeMaterial, "pipe_kind"),
|
||||
labeled("관경", pipeDiameter, "pipe_diameter_mm"),
|
||||
);
|
||||
|
||||
// ── 배관 = 사용자 구조물 표 01 명세(PLAN 52-1) — 컨테이너 다섯 · 옛 관종 · 날개벽 칸은 뺌(지난 정의 값) ──
|
||||
let linkSource: SpecLinks | null = null;
|
||||
let chainage: number | null = null;
|
||||
const pipeForm = createSpecForm(
|
||||
{
|
||||
get: () => linkSource?.get() ?? {},
|
||||
pick: (key) => linkSource?.pick(key),
|
||||
clear: (key) => linkSource?.clear(key),
|
||||
set: (key, item) => linkSource?.set(key, item),
|
||||
const specLinks: SpecLinks = {
|
||||
get: () => linkSource?.get() ?? {},
|
||||
pick: (key) => linkSource?.pick(key),
|
||||
clear: (key) => linkSource?.clear(key),
|
||||
set: (key, item) => linkSource?.set(key, item),
|
||||
};
|
||||
const pipeForm = createSpecForm(specLinks, {
|
||||
auto: (_c, item) => {
|
||||
if (item.id !== "length") return "";
|
||||
const length = pipeLengthM(chainage);
|
||||
return length === null ? "횡단 설계 뒤 셈" : length.toFixed(2);
|
||||
},
|
||||
{
|
||||
auto: (_c, item) => {
|
||||
if (item.id !== "length") return "";
|
||||
const length = pipeLengthM(chainage);
|
||||
return length === null ? "횡단 설계 뒤 셈" : length.toFixed(2);
|
||||
},
|
||||
hint: (c, item, state) => {
|
||||
const soil = c.id !== "inlet" || state.inlet?.[0]?.structure === "기슭막이";
|
||||
return item.id === "height" && soil ? guardHeightHint(state.pipe?.[0]?.diameter ?? "") : "";
|
||||
},
|
||||
onChange: (c, item) => {
|
||||
if (c.id === "inlet" && (item.id === "structure" || item.id === "form"))
|
||||
inletStructureHandler?.(inletStructureOf());
|
||||
emit();
|
||||
},
|
||||
hint: (c, item, state) => {
|
||||
const soil = c.id !== "inlet" || state.inlet?.[0]?.structure === "기슭막이";
|
||||
return item.id === "height" && soil ? guardHeightHint(state.pipe?.[0]?.diameter ?? "") : "";
|
||||
},
|
||||
);
|
||||
onChange: (c, item) => {
|
||||
if (c.id === "inlet" && (item.id === "structure" || item.id === "form"))
|
||||
inletStructureHandler?.(inletStructureOf());
|
||||
emit();
|
||||
},
|
||||
});
|
||||
// 조정창에서 옮겨 오는 행(단 수·옵션)이 붙을 자리 — 유입 · 유출 컨테이너 끝(2026-08-29 지시 4)
|
||||
const inletSlot = pipeForm.slot("inlet");
|
||||
const outletSlot = pipeForm.slot("outlet");
|
||||
@@ -301,6 +289,26 @@ export function createFacilityOptionsForm(
|
||||
: "auto";
|
||||
};
|
||||
|
||||
// ── 세월교 = 사용자 구조물 표 04 명세(PLAN 52-4) — 세월교 · 유입 구조 · 유출 구조 ──
|
||||
// 월류 높이 = 월류 폭과 연동(폭을 바꾸면 필요 수심으로 채움 · 그 아래는 안 받음 · `design_structure_ford_calc`)
|
||||
let fordFlow: number | null = null;
|
||||
const fordNeed = (): number | null =>
|
||||
fordHeightM(fordFlow, Number(fordForm.state().ford?.[0]?.width ?? ""));
|
||||
const fordForm = createSpecForm(specLinks, {
|
||||
hint: (c, item, state) =>
|
||||
c.id === "ford" && item.id === "height"
|
||||
? fordHeightHint(fordFlow, Number(state.ford?.[0]?.width ?? ""))
|
||||
: "",
|
||||
onChange: (c, item) => {
|
||||
const need = c.id === "ford" ? fordNeed() : null;
|
||||
const height = fordForm.state().ford?.[0]?.height ?? "";
|
||||
const low = height !== "" && !(Number(height) >= (need ?? 0));
|
||||
if (need !== null && (item.id === "width" || (item.id === "height" && low)))
|
||||
fordForm.setValue("ford", "height", need.toFixed(2));
|
||||
emit();
|
||||
},
|
||||
});
|
||||
|
||||
// ── 추가 기슭막이(다단) — 횡단도에서 다단 벽을 고를 때만 뜬다(2026-08-30 사용자
|
||||
// 지시 4). 유입구·유출구와 같은 양식이되, 값은 조정창 사본의 행이 그대로 옮겨 온다
|
||||
// (다단 벽은 폼에 자기 칸이 없다 — 높이·형태·구간값이 전부 조정 채널에 있다).
|
||||
@@ -370,6 +378,8 @@ export function createFacilityOptionsForm(
|
||||
);
|
||||
const wingInFields = createWingFields("날개벽(유입)", WING_IN_KEYS);
|
||||
const wingOutFields = createWingFields("날개벽(유출)", WING_OUT_KEYS);
|
||||
const wingInHome = wingInFields.slot.parentElement!;
|
||||
const wingOutHome = wingOutFields.slot.parentElement!;
|
||||
|
||||
const hiddenOptions = createHiddenOptions(root);
|
||||
|
||||
@@ -399,8 +409,8 @@ export function createFacilityOptionsForm(
|
||||
// 월류 폭·높이 → 바닥 경사 → 수량 → 개략 단면 결과. 다른 시설에서는 전부 숨는다.
|
||||
root.append(
|
||||
saveNote,
|
||||
pipeRow,
|
||||
pipeForm.root,
|
||||
fordForm.root,
|
||||
ford.widthRow,
|
||||
ford.slopeRow,
|
||||
ford.countRow,
|
||||
@@ -421,15 +431,18 @@ export function createFacilityOptionsForm(
|
||||
if (current === null) return;
|
||||
const isPipe = current === "pipe";
|
||||
const isBox = current === "box_culvert";
|
||||
pipeRow.hidden = current !== "ford_bridge";
|
||||
const isBridge = current === "ford_bridge";
|
||||
pipeForm.root.hidden = !isPipe;
|
||||
fordForm.root.hidden = !isBridge;
|
||||
boxWrap.hidden = !isBox;
|
||||
// 날개벽은 BOX암거와 세월교가 같은 옵션 한 벌을 쓴다(2026-08-25 사용자 확정).
|
||||
const hasWing = isBox || current === "ford_bridge";
|
||||
// 날개벽 칸 = BOX암거 · 세월교는 유입 · 유출 구조 컨테이너(52-4) — B06 조정 행 자리만 그 컨테이너 끝으로
|
||||
const hasWing = isBox;
|
||||
wingInFields.root.hidden = !hasWing;
|
||||
wingOutFields.root.hidden = !hasWing;
|
||||
const isFord = current === "ford_pavement" || current === "ford_bridge";
|
||||
ford.countRow.hidden = current !== "ford_bridge";
|
||||
(isBridge ? fordForm.slot("inlet") : wingInHome).append(wingInFields.slot);
|
||||
(isBridge ? fordForm.slot("outlet") : wingOutHome).append(wingOutFields.slot);
|
||||
const isFord = current === "ford_pavement";
|
||||
ford.countRow.hidden = true;
|
||||
ford.widthRow.hidden = !isFord;
|
||||
// 바닥 경사는 물넘이포장만 쓴다 — 세월교는 구체 위 노면이라 파임이 없다.
|
||||
ford.slopeRow.hidden = current !== "ford_pavement";
|
||||
@@ -462,8 +475,6 @@ export function createFacilityOptionsForm(
|
||||
revetOutlet.fields.onChange(emit);
|
||||
revetSide.addEventListener("change", emit);
|
||||
[
|
||||
pipeMaterial,
|
||||
pipeDiameter,
|
||||
boxSize,
|
||||
boxWidth,
|
||||
boxHeight,
|
||||
@@ -500,8 +511,12 @@ export function createFacilityOptionsForm(
|
||||
bindLinks(links) {
|
||||
linkSource = links;
|
||||
pipeForm.paintLinks();
|
||||
fordForm.paintLinks();
|
||||
},
|
||||
paintLinks: () => {
|
||||
pipeForm.paintLinks();
|
||||
fordForm.paintLinks();
|
||||
},
|
||||
paintLinks: () => pipeForm.paintLinks(),
|
||||
setChainage(chainageM) {
|
||||
chainage = chainageM;
|
||||
if (current === "pipe") pipeForm.refresh();
|
||||
@@ -516,6 +531,7 @@ export function createFacilityOptionsForm(
|
||||
current = facility;
|
||||
hiddenOptions.remember(options);
|
||||
ford.setDesignFlow(designFlow ?? null);
|
||||
fordFlow = designFlow ?? null;
|
||||
if (facility === null) {
|
||||
syncVisibility();
|
||||
return;
|
||||
@@ -523,12 +539,11 @@ export function createFacilityOptionsForm(
|
||||
const text = (key: string): string =>
|
||||
options[key] !== undefined ? String(options[key]) : "";
|
||||
// 받은 값만 칸에 — 없으면 「안 정함」(새 시설은 부르는 쪽이 등록부 기본값을 실어 줌 · 46-7).
|
||||
pipeMaterial.value = text("pipe_kind");
|
||||
pipeDiameter.value = text("pipe_diameter_mm");
|
||||
if (facility === "pipe") {
|
||||
const groups = Object.fromEntries((definition?.groups ?? []).map((g) => [g.id, g.name]));
|
||||
const groups = Object.fromEntries((definition?.groups ?? []).map((g) => [g.id, g.name]));
|
||||
if (facility === "pipe")
|
||||
pipeForm.write(PIPE_SPEC, options, definition?.spec_names ?? {}, groups);
|
||||
}
|
||||
if (facility === "ford_bridge")
|
||||
fordForm.write(FORD_SPEC, withLegacyGuards(options), definition?.spec_names ?? {}, groups);
|
||||
// 저장된 폭·높이가 프리셋과 맞으면 그 프리셋을, 아니면 "사용자 지정"을 고른다.
|
||||
// 값이 없으면 「안 정함」.
|
||||
const savedWidth = Number.parseFloat(text("body_width_m"));
|
||||
@@ -549,7 +564,7 @@ export function createFacilityOptionsForm(
|
||||
boxHeight.value = hasSaved ? String(savedHeight) : "";
|
||||
wingInFields.write(options);
|
||||
wingOutFields.write(options);
|
||||
if (facility === "ford_pavement" || facility === "ford_bridge") ford.write(facility, options);
|
||||
if (facility === "ford_pavement") ford.write(facility, options);
|
||||
revetSide.value = text("side");
|
||||
const spread = legacyRevetOptions(options);
|
||||
revetInlet.fields.write(spread);
|
||||
@@ -558,15 +573,13 @@ export function createFacilityOptionsForm(
|
||||
},
|
||||
setDesignFlow(designFlow) {
|
||||
ford.setDesignFlow(designFlow ?? null);
|
||||
fordFlow = designFlow ?? null;
|
||||
if (current === "ford_bridge") fordForm.refresh(); // 높이 안내
|
||||
syncVisibility();
|
||||
},
|
||||
readOptions() {
|
||||
const options: Record<string, string | number> = {};
|
||||
// 고른 칸만 싣는다 — 「안 정함」은 빈 값(기본값을 몰래 확정으로 안 바꿈).
|
||||
const putPipe = (): void => {
|
||||
if (pipeDiameter.value) options.pipe_diameter_mm = Number(pipeDiameter.value);
|
||||
if (pipeMaterial.value) options.pipe_kind = pipeMaterial.value;
|
||||
};
|
||||
if (current === "pipe") {
|
||||
Object.assign(options, pipeForm.read()); // 명세 컨테이너 값 = 옛 키 그대로(PLAN 52-1)
|
||||
} else if (current === "box_culvert") {
|
||||
@@ -578,10 +591,7 @@ export function createFacilityOptionsForm(
|
||||
} else if (current === "ford_pavement") {
|
||||
ford.read(current, options);
|
||||
} else if (current === "ford_bridge") {
|
||||
putPipe();
|
||||
ford.read(current, options);
|
||||
wingInFields.read(options);
|
||||
wingOutFields.read(options);
|
||||
Object.assign(options, fordForm.read()); // 표 04 명세 값 · 옛 관종 · 날개벽 치수는 병합이 이어 붙임(52-4)
|
||||
} else if (current === "revetment") {
|
||||
if (revetSide.value) options.side = revetSide.value;
|
||||
// 단 수는 폼이 갖지 않는다 — 횡단 설계 patch(extra_wall_counts)가 정본이고
|
||||
|
||||
@@ -42,14 +42,16 @@ export const FORM_OPTION_KEYS: Readonly<Record<string, readonly string[]>> = {
|
||||
],
|
||||
box_culvert: ["body_width_m", "body_height_m", ...wingKeys("in"), ...wingKeys("out")],
|
||||
ford_pavement: ["ford_width_m", "ford_height_m", "ford_slope_pct", "thickness_cm", "length_m"],
|
||||
// 세월교 = 표 04 명세 키(PLAN 52-4 · `design_structure_ford`) — 옛 관종 · 날개벽 치수는 폼에서 빠져 이어 붙임
|
||||
ford_bridge: [
|
||||
"pipe_kind",
|
||||
"pipe_diameter_mm",
|
||||
"pipe_count",
|
||||
"ford_width_m",
|
||||
"ford_height_m",
|
||||
...wingKeys("in"),
|
||||
...wingKeys("out"),
|
||||
"inlet_type",
|
||||
"outlet_type",
|
||||
"wing_in",
|
||||
"wing_out",
|
||||
],
|
||||
revetment: ["side", ...revetKeys("inlet"), ...revetKeys("outlet")],
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from "./B05_Profile_UI_IrregularStations";
|
||||
import { structureAnchorM, type StructureInstance } from "./B05_Profile_Api_Structures";
|
||||
import { GROUP_LABELS } from "./B05_Profile_UI_Structures_Panel";
|
||||
import { HIDDEN_PICK_GROUPS } from "./B05_Profile_UI_Structures_Panel_Types";
|
||||
|
||||
/** 우클릭 메뉴용 최소 타입 정보 — 사이드 「구조물 배치」 종류 목록과 같은 원천. */
|
||||
export interface StructureMenuType {
|
||||
@@ -32,7 +33,9 @@ export function structureGroupMenuItems(
|
||||
types: ReadonlyArray<StructureMenuType>,
|
||||
onPick: (typeId: string) => void,
|
||||
): MapContextMenuItem[] {
|
||||
const groups = [...new Set(types.map((type) => type.group))];
|
||||
const groups = [...new Set(types.map((type) => type.group))].filter(
|
||||
(group) => !HIDDEN_PICK_GROUPS.has(group),
|
||||
);
|
||||
return groups.map((group) => ({
|
||||
label: GROUP_LABELS[group] ?? group,
|
||||
children: types
|
||||
|
||||
@@ -78,7 +78,9 @@ export function createFacilityDefinition(root: HTMLElement): FacilityDefinition
|
||||
root
|
||||
.querySelectorAll(".is-rule-hidden")
|
||||
.forEach((node) => node.classList.remove("is-rule-hidden"));
|
||||
root.querySelectorAll<HTMLOptionElement>("option[hidden]").forEach((o) => (o.hidden = false));
|
||||
root
|
||||
.querySelectorAll<HTMLOptionElement>("option[hidden]:not([data-pick-hidden])")
|
||||
.forEach((o) => (o.hidden = false));
|
||||
paint();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@ import type { PipeFacility } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
import { type FacilityAttributes } from "./B05_Profile_UI_Drainage_Facility";
|
||||
import { field, optionDefaults } from "./B05_Profile_UI_Structures_Fields";
|
||||
import {
|
||||
hasSpecContainers,
|
||||
hidesTypeSummary,
|
||||
paintStale,
|
||||
renderOptionFields as renderOptionFieldsInto,
|
||||
} from "./B05_Profile_UI_Structures_Panel_Options";
|
||||
@@ -54,6 +54,7 @@ export type { PipeFacilityItem, StructuresSection } from "./B05_Profile_UI_Struc
|
||||
import {
|
||||
DEFAULT_INTERVAL_LENGTH_M,
|
||||
GROUP_LABELS,
|
||||
groupOption,
|
||||
hasComputedRange,
|
||||
readBeforeOption,
|
||||
readLengthOption,
|
||||
@@ -227,7 +228,7 @@ export function createStructuresSection(
|
||||
!!type && !managed && type.placement === "interval" && !hasComputedRange(type);
|
||||
positionRow.hidden = !type;
|
||||
positionDivider.hidden = !type;
|
||||
summary.root.hidden = !type || hasSpecContainers(type); // 컨테이너 종류는 컨테이너마다 잇기(52-1)
|
||||
summary.root.hidden = !type || hidesTypeSummary(type); // 컨테이너 종류는 컨테이너마다 잇기(52-1)
|
||||
startFields.wrap.hidden = !isInterval;
|
||||
endFields.wrap.hidden = !isInterval;
|
||||
primary.disabled = !type;
|
||||
@@ -544,7 +545,7 @@ export function createStructuresSection(
|
||||
// 첫 항목은 빈칸 — 진입·리셋 상태에서 군·종류를 강제로 고르게 두지 않는다. 이름은 정의(48) 먼저.
|
||||
groupSelect.replaceChildren(
|
||||
new Option("— 선택 —", ""),
|
||||
...[...groups].map(([g, name]) => new Option(name ?? GROUP_LABELS[g] ?? g, g)),
|
||||
...[...groups].map(([g, name]) => groupOption(g, name ?? GROUP_LABELS[g] ?? g)),
|
||||
);
|
||||
groupSelect.value = "";
|
||||
syncTypeOptions();
|
||||
|
||||
@@ -23,9 +23,10 @@ import { FORM_OPTION_KEYS } from "./B05_Profile_UI_Drainage_Facility_Merge";
|
||||
import { STRUCTURE_SPECS } from "../A00_Common/design/design_structure_pipe";
|
||||
import { specKeys } from "../A00_Common/design/design_structure_spec";
|
||||
|
||||
/** 구조물 명세 컨테이너로 그리는 종류인지 — 그때 종류 단위 「구조물 조합」 줄은 가림(PLAN 52-1). */
|
||||
export const hasSpecContainers = (type: StructureType): boolean =>
|
||||
!!STRUCTURE_SPECS[type.type_id]?.containers.length;
|
||||
/** 종류 단위 「구조물 조합」 줄을 가릴 종류인지 — 구조물 명세 컨테이너 · 정의 그룹 상자(그룹마다 잇기 줄)로
|
||||
* 그리는 종류(PLAN 52-1 · 52-6b) · 종단배수(B군 · 조합 잇기 없음 · 표 07). */
|
||||
export const hidesTypeSummary = (type: StructureType): boolean =>
|
||||
!!STRUCTURE_SPECS[type.type_id]?.containers.length || !!type.groups?.length || type.group === "B";
|
||||
|
||||
/** 패널이 쥔 값 중 옵션 그리기에 필요한 것들. 이름은 분리 전 지역변수와 같다. */
|
||||
export interface OptionRenderContext {
|
||||
|
||||
@@ -34,6 +34,18 @@ export const GROUP_LABELS: Record<string, string> = {
|
||||
호환: "기타",
|
||||
};
|
||||
|
||||
/** 새로 놓을 때 고르기 목록(군 드롭다운 · 우클릭 메뉴)에서 뺄 군 — 종단배수·계류사방(표 07).
|
||||
* 옛 값 · 도면 · 집계는 그대로이고 데이터도 지우지 않는다. */
|
||||
export const HIDDEN_PICK_GROUPS: ReadonlySet<string> = new Set(["B", "D"]);
|
||||
|
||||
/** 군 드롭다운 항목 — 뺄 군은 가림(옛 항목을 열 때는 값으로 그대로 고를 수 있음). */
|
||||
export function groupOption(group: string, name: string): HTMLOptionElement {
|
||||
const option = new Option(name, group);
|
||||
option.hidden = HIDDEN_PICK_GROUPS.has(group);
|
||||
if (option.hidden) option.dataset.pickHidden = ""; // 정의 폼의 「가림 풀기」가 건드리지 않게
|
||||
return option;
|
||||
}
|
||||
|
||||
/** 구간형 신규 추가 시 기본 구간 길이(m). 사용자가 종점을 바로 고칠 수 있게 짧게 잡는다. */
|
||||
export const DEFAULT_INTERVAL_LENGTH_M = 15;
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ import {
|
||||
} from "./M02_SO_Def_Model";
|
||||
import { buildOptionsBlock } from "./M02_SO_Def_Options";
|
||||
import { buildRulesBlock } from "./M02_SO_Def_Rules";
|
||||
import { buildSpecBlock } from "./M02_SO_Def_Spec";
|
||||
import { STRUCTURE_SPECS } from "../../A00_Common/design/design_structure_pipe";
|
||||
import { checkBox, textBox } from "./M02_SO_Def_Ui";
|
||||
import { buildGroupBlock } from "./M02_SO_Groups";
|
||||
import { to } from "./M02_SO_Text";
|
||||
@@ -182,6 +184,8 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
|
||||
}
|
||||
const edit = canEdit();
|
||||
const extra = (): NonNullable<StructDef["종류"][string]> => (d.종류[type.type_id] ??= {});
|
||||
// 명세가 있는 종류(PLAN 52) = 이름만 고침 — 옛 편집기(옵션 · 배치 · 그룹 · 규칙 · 기준 측점)는 숨김
|
||||
const spec = STRUCTURE_SPECS[type.type_id];
|
||||
detail.replaceChildren(
|
||||
el("div", {
|
||||
className: "m02-sod__kindhead",
|
||||
@@ -196,16 +200,20 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
|
||||
edit,
|
||||
attr: "data-sod-kind-off",
|
||||
}),
|
||||
checkBox(
|
||||
to("BaseStationShow"),
|
||||
baseShown(d, type.type_id),
|
||||
(v) => {
|
||||
if (v) delete extra().기준측점;
|
||||
else extra().기준측점 = false;
|
||||
changed();
|
||||
},
|
||||
{ edit, attr: "data-sod-base" },
|
||||
),
|
||||
...(spec
|
||||
? []
|
||||
: [
|
||||
checkBox(
|
||||
to("BaseStationShow"),
|
||||
baseShown(d, type.type_id),
|
||||
(v) => {
|
||||
if (v) delete extra().기준측점;
|
||||
else extra().기준측점 = false;
|
||||
changed();
|
||||
},
|
||||
{ edit, attr: "data-sod-base" },
|
||||
),
|
||||
]),
|
||||
...(d.종류[type.type_id]?.동작 ?? []).map((a) => {
|
||||
const tag = createTag(a, "neutral");
|
||||
tag.title = d.동작[a] ?? "";
|
||||
@@ -213,10 +221,14 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
|
||||
}),
|
||||
],
|
||||
}),
|
||||
buildOptionsBlock({ def: d, type, edit, open, changed }),
|
||||
buildLayoutBlock({ def: d, type, edit, changed }),
|
||||
buildGroupBlock({ list: (d.그룹[type.type_id] ??= []), edit, changed }),
|
||||
buildRulesBlock({ def: d, type, edit, changed }),
|
||||
...(spec
|
||||
? [buildSpecBlock({ def: d, type, spec, edit, changed })]
|
||||
: [
|
||||
buildOptionsBlock({ def: d, type, edit, open, changed }),
|
||||
buildLayoutBlock({ def: d, type, edit, changed }),
|
||||
buildGroupBlock({ list: (d.그룹[type.type_id] ??= []), edit, changed }),
|
||||
buildRulesBlock({ def: d, type, edit, changed }),
|
||||
]),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -57,7 +57,8 @@ export interface StructDef {
|
||||
구조물군: Record<string, string>;
|
||||
동작: Record<string, string>;
|
||||
types: TypeDef[];
|
||||
종류: Record<string, { 기준측점?: boolean; 동작?: string[] }>;
|
||||
/** 이름 = 구조물 명세 이름 바꿈 표(PLAN 52 · 키 `{컨테이너}[.{항목}[={값}]]`) */
|
||||
종류: Record<string, { 기준측점?: boolean; 동작?: string[]; 이름?: Record<string, string> }>;
|
||||
표시: Record<string, Record<string, false>>;
|
||||
그룹: Record<string, GroupDef[]>;
|
||||
배치: Record<string, Record<string, Spot>>;
|
||||
@@ -257,6 +258,10 @@ export function problems(def: StructDef): string[] {
|
||||
if (!o.choices?.length) out.push(`${t.name}.${o.label} 고르기 꼴은 선택지가 있어야 함`);
|
||||
}
|
||||
const grp = badNames((def.그룹[t.type_id] ?? []).map((g) => g.이름));
|
||||
const spec = Object.entries(def.종류[t.type_id]?.이름 ?? {})
|
||||
.filter(([, v]) => !v.trim() || v.length > NAME_MAX)
|
||||
.map(([k]) => k);
|
||||
if (spec.length) out.push(`${t.name} 명세 이름: ${spec.join(", ")}`);
|
||||
if (grp.length) out.push(`${t.name} 그룹 이름: ${grp.join(", ")}`);
|
||||
const lap = overlaps(placeAll(t.options, def.배치[t.type_id]));
|
||||
if (lap.length) out.push(`${t.name} 배치 겹침: ${lap.join(", ")}`);
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/* =============================================================================
|
||||
* M02_SO_Def_Spec.ts
|
||||
* 구조물 명세가 있는 종류(PLAN 52 · `STRUCTURE_SPECS`)의 이름 고치기 칸(52-11) — 나무 꼴
|
||||
* 컨테이너 제목 › 항목 이름 › 값 이름 · 이름만 고침 · 키 · 필수/선택 · 꼴은 읽기만 ·
|
||||
* 아래 미리보기 = B05 · B06 이 그리는 같은 명세 폼(`createSpecForm`)에 고친 이름을 얹음.
|
||||
* 구조물군 · 종류 이름은 왼쪽 목록 · 머리 칸이 그대로 고침.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createTag, el } from "@ui/ui_template_elements";
|
||||
import { createSpecForm } from "../../A00_Common/design/design_structure_form";
|
||||
import type { StructureSpec } from "../../A00_Common/design/design_structure_spec";
|
||||
import type { StructDef, TypeDef } from "./M02_SO_Def_Model";
|
||||
import { setSpecName, specNameRows, type SpecNameRow } from "./M02_SO_Def_Spec_Model";
|
||||
import { head, textBox } from "./M02_SO_Def_Ui";
|
||||
import { to, type OptionsTextKey } from "./M02_SO_Text";
|
||||
|
||||
const TAG: Record<NonNullable<SpecNameRow["tag"]>, OptionsTextKey> = {
|
||||
required: "SpecRequired",
|
||||
optional: "SpecOptional",
|
||||
select: "FormSelect",
|
||||
number: "FormNumber",
|
||||
auto: "FormAuto",
|
||||
};
|
||||
|
||||
export function buildSpecBlock(opt: {
|
||||
def: StructDef;
|
||||
type: TypeDef;
|
||||
spec: StructureSpec;
|
||||
edit: boolean;
|
||||
changed: () => void;
|
||||
}): HTMLElement {
|
||||
const { def, type, spec, edit, changed } = opt;
|
||||
const rows = specNameRows(def, spec).map((row) =>
|
||||
el("div", {
|
||||
className: `m02-sod__specrow is-level-${row.level}`,
|
||||
attrs: { "data-sod-spec": row.nameKey },
|
||||
children: [
|
||||
textBox(row.label, (v) => setSpecName(def, type.type_id, row, v), changed, {
|
||||
edit,
|
||||
attr: "data-sod-spec-name",
|
||||
cls: "m02-sod__name",
|
||||
}),
|
||||
el("small", { className: "m02-opts__key", text: row.key }),
|
||||
...(row.tag ? [createTag(to(TAG[row.tag]), "neutral")] : []),
|
||||
],
|
||||
}),
|
||||
);
|
||||
// 미리보기 — 값 없는 새 구조물 폼 · 잇기 줄 없음(조합은 프로젝트 몫)
|
||||
const preview = createSpecForm(null);
|
||||
const groups = Object.fromEntries((def.그룹[type.type_id] ?? []).map((g) => [g.id, g.이름]));
|
||||
preview.write(spec, {}, def.종류[type.type_id]?.이름 ?? {}, groups);
|
||||
preview.root.inert = true;
|
||||
return el("div", {
|
||||
className: "m02-sod__block",
|
||||
attrs: { "data-sod-spec-block": "" },
|
||||
children: [
|
||||
...head(to("SpecHead"), to("SpecNote")),
|
||||
...rows,
|
||||
...head(to("SpecPreview")),
|
||||
el("div", {
|
||||
className: "m02-sod__specpreview",
|
||||
attrs: { "data-sod-spec-preview": "" },
|
||||
children: [preview.root],
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/* =============================================================================
|
||||
* M02_SO_Def_Spec_Model.ts
|
||||
* 구조물 명세(PLAN 52 · `design_structure_spec`) 이름 고치기(52-11)의 자료 손질 — 화면 그리기 없음.
|
||||
*
|
||||
* · 명세 = 구조 정본(키 · 컨테이너 · 필수/선택 · 꼴) — 여기서는 **이름만** 고침
|
||||
* · 이름 자리 = 정의 `종류.{종류}.이름`(키 `{컨테이너}` · `{컨테이너}.{항목}` · `{컨테이너}.{항목}={값}`)
|
||||
* · 필수 컨테이너 제목 = 이름 표에 없으면 정의 그룹(`그룹.{종류}` 의 `id = link`) 이름 — B05 그리기
|
||||
* (`containerTitle`)와 같은 차례로 읽고 그 자리에 씀
|
||||
* import 는 꼴만 — 시험이 이 파일만 옮겨 돌림.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { StructureSpec } from "../../A00_Common/design/design_structure_spec";
|
||||
import type { StructDef } from "./M02_SO_Def_Model";
|
||||
|
||||
export interface SpecNameRow {
|
||||
/** 0 = 컨테이너 · 1 = 항목 · 2 = 값 */
|
||||
level: 0 | 1 | 2;
|
||||
/** 이름 표 키 */
|
||||
nameKey: string;
|
||||
/** 읽기만 표시 — 컨테이너 id · 항목 id · 값 */
|
||||
key: string;
|
||||
/** 지금 이름 */
|
||||
label: string;
|
||||
/** 읽기만 표시 — 필수/선택 · 꼴 */
|
||||
tag: "required" | "optional" | "select" | "number" | "auto" | null;
|
||||
/** 필수 컨테이너 제목이 정의 그룹 이름에서 옴 — 고치면 그 그룹 이름에 씀 */
|
||||
group: string | null;
|
||||
}
|
||||
|
||||
/** 명세 차례대로 이름 줄 — 같은 이름 키(갈래마다 겹친 값)는 한 줄. */
|
||||
export function specNameRows(def: StructDef, spec: StructureSpec): SpecNameRow[] {
|
||||
const names = def.종류[spec.typeId]?.이름 ?? {};
|
||||
const groups = def.그룹[spec.typeId] ?? [];
|
||||
const rows: SpecNameRow[] = [];
|
||||
const seen = new Set<string>();
|
||||
const push = (row: SpecNameRow): void => {
|
||||
if (seen.has(row.nameKey)) return;
|
||||
seen.add(row.nameKey);
|
||||
rows.push(row);
|
||||
};
|
||||
for (const c of spec.containers) {
|
||||
const group =
|
||||
c.required && names[c.id] === undefined ? groups.find((g) => g.id === c.link) : undefined;
|
||||
push({
|
||||
level: 0,
|
||||
nameKey: c.id,
|
||||
key: c.id,
|
||||
label: names[c.id] ?? group?.이름 ?? c.title,
|
||||
tag: c.required ? "required" : "optional",
|
||||
group: group ? group.id : null,
|
||||
});
|
||||
for (const item of c.items) {
|
||||
const itemKey = `${c.id}.${item.id}`;
|
||||
push({
|
||||
level: 1,
|
||||
nameKey: itemKey,
|
||||
key: item.id,
|
||||
label: names[itemKey] ?? item.label,
|
||||
tag: item.kind,
|
||||
group: null,
|
||||
});
|
||||
const choices = [
|
||||
...(item.choices ?? []),
|
||||
...Object.values(item.by?.map ?? {}).flatMap((b) => b.choices ?? []),
|
||||
];
|
||||
for (const ch of choices) {
|
||||
const choiceKey = `${itemKey}=${ch.value}`;
|
||||
push({
|
||||
level: 2,
|
||||
nameKey: choiceKey,
|
||||
key: ch.value,
|
||||
label: names[choiceKey] ?? ch.label,
|
||||
tag: null,
|
||||
group: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/** 이름 하나를 사본에 씀 — 그룹에서 온 제목은 그 그룹 이름 · 나머지는 이름 표. */
|
||||
export function setSpecName(def: StructDef, typeId: string, row: SpecNameRow, value: string): void {
|
||||
const group = row.group ? def.그룹[typeId]?.find((g) => g.id === row.group) : undefined;
|
||||
if (group) {
|
||||
group.이름 = value;
|
||||
return;
|
||||
}
|
||||
const kind = (def.종류[typeId] ??= {});
|
||||
(kind.이름 ??= {})[row.nameKey] = value;
|
||||
}
|
||||
@@ -332,3 +332,28 @@
|
||||
margin: 0;
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
/* 구조물 명세 이름 고치기(PLAN 52-11) — 컨테이너 › 항목 › 값 들여쓰기 */
|
||||
.m02-sod__specrow {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.m02-sod__specrow.is-level-0 {
|
||||
margin-top: var(--spacing-8);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.m02-sod__specrow.is-level-1 {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.m02-sod__specrow.is-level-2 {
|
||||
padding-left: 3rem;
|
||||
}
|
||||
|
||||
.m02-sod__specpreview {
|
||||
max-width: 28rem;
|
||||
}
|
||||
|
||||
@@ -45,6 +45,15 @@ const TEXT = {
|
||||
FormNumber: ["숫자", "Number"],
|
||||
FormText: ["글", "Text"],
|
||||
FormBool: ["예/아니오", "Yes/No"],
|
||||
FormAuto: ["자동", "Auto"],
|
||||
SpecHead: ["구조물 명세 이름", "Structure spec names"],
|
||||
SpecNote: [
|
||||
"컨테이너 제목 › 항목 이름 › 값 이름만 고침 — 키(작은 글) · 필수/선택 · 꼴 · 구조는 구조물 명세가 정함 · [저장]하면 B05 · B06 이 이 이름으로 그림",
|
||||
"Rename containers › items › values only — keys, required/optional, form and structure come from the spec",
|
||||
],
|
||||
SpecRequired: ["필수", "Required"],
|
||||
SpecOptional: ["선택", "Optional"],
|
||||
SpecPreview: ["미리보기 — B05 · B06 폼", "Preview — B05 · B06 form"],
|
||||
Yes: ["예", "Yes"],
|
||||
No: ["아니오", "No"],
|
||||
Unit: ["단위", "Unit"],
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# 구조물 표 08 — 안전부대용지 사토장 · 토취장 · 노면공 콘크리트 포장 · 기타 (사용자 10-5 · 원문 그대로)
|
||||
|
||||
구조물군 상세 종류 기본 옵션 컨테이너 1 컨테이너 1 옵셩 항목 … 상세 설계 필수 선택
|
||||
안전부대용지 유용토운반작업장(사토장) 측점 No, + 측점거리 (필수)컨테이너 제목 : 상세 내용
|
||||
사토장 부피 수기입력
|
||||
|
||||
안전부대용지 토사채취장(토취장) 측점 No, + 측점거리 (필수)컨테이너 제목 : 상세 내용
|
||||
토취장 부피 수기입력
|
||||
|
||||
노면공 콘크리트 포장 측점 No, + 측점거리 (필수)컨테이너 제목 : 상세 내용
|
||||
포장사양 콘크리트, XXX
|
||||
높이 수기입력
|
||||
|
||||
기타 수기입력 측점 No, + 측점거리 (선택)컨테이너 제목 : 상세 내용 (상세 설계) 도수로(or 돌붙임)
|
||||
비고 수기 입력 (상세 설계) 도수로(or 돌붙임) 관보호공, 옹벽, 돌쌓기, 흙막이, 큰톨쌓기 가 같은 형상으로 보임.
|
||||
|
||||
(가드레일은 표 07 덧과 같음)
|
||||
@@ -119,3 +119,6 @@
|
||||
|
||||
추가로 구조물 조합의 일위대가 상세페이지의 로직 테이블의 순서 열에서 존재하는 버튼을 2행 구조로 변경
|
||||
단가 승격에도 옵션 버튼이있어야할것같고. 일위조합의 경우에는 바로 옵션으로 지정할수 없으니 수정버튼(모달)도 잇어할것 같은데?
|
||||
|
||||
맹암거처럼 cad연결이나 횡단도에 그릴지 말지는 이번 수정 후에 작업할거야. 너는 B05/B06 좌측 패널의 생성과 기존 로직 연결에만 신경쓸것.
|
||||
나는 이제 자러 갈거니 너가 다른 워크트리들을 잘 제어해서 계획서의 내용을 반영하고 최종검증을 진행 후 아티팩트로 결과를 보여주는데 만약 개선이 필요하다고 판단되는 부분이 있다면 계획서에 내용을 추가해서 개선할것.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"판": 6,
|
||||
"판": 8,
|
||||
"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`가 새 키로 옮긴다.",
|
||||
"구조물군": {
|
||||
@@ -676,28 +676,41 @@
|
||||
"cross_section",
|
||||
"quantity"
|
||||
],
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"type_id": "cross_underdrain",
|
||||
"group": "A",
|
||||
"name": "맹암거",
|
||||
"placement": "point",
|
||||
"style": {
|
||||
"color": "#3aa0a8",
|
||||
"abbr": "횡맹"
|
||||
},
|
||||
"drawing_views": [
|
||||
"plan",
|
||||
"profile",
|
||||
"cross_section",
|
||||
"quantity"
|
||||
],
|
||||
"options": [
|
||||
{
|
||||
"key": "ditch_spec",
|
||||
"label": "규격",
|
||||
"input": "select",
|
||||
"choices": [
|
||||
"콘크리트 개거 150×200",
|
||||
"L형수로 H=0.2"
|
||||
],
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 실무 붙박이(콘크리트 개거)로 돎"
|
||||
},
|
||||
{
|
||||
"key": "length_m",
|
||||
"label": "연장",
|
||||
"key": "culvert_height_m",
|
||||
"label": "암거 높이",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": false,
|
||||
"empty_means": "비우면 표가 안 서고 「연장을 적어야 섬」 사유가 뜸 — 개거는 m당 원단위라 연장이 곧 밑수(2026-09-14 A2)"
|
||||
"empty_means": "비우면 값 없음 — 사용자 표 06 손 입력 칸(기본값 근거 없음)"
|
||||
},
|
||||
{
|
||||
"key": "culvert_slope_pct",
|
||||
"label": "암거 경사",
|
||||
"input": "number",
|
||||
"unit": "%",
|
||||
"default": null,
|
||||
"required": false,
|
||||
"empty_means": "비우면 값 없음 — 사용자 표 06 손 입력 칸(기본값 근거 없음)"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -3308,7 +3321,25 @@
|
||||
"계곡통과시설",
|
||||
"월류단면",
|
||||
"날개벽"
|
||||
]
|
||||
],
|
||||
"이름": {
|
||||
"ford.diameter": "관경",
|
||||
"ford.diameter=800": "800",
|
||||
"ford.diameter=1000": "1000",
|
||||
"ford.diameter=1200": "1200",
|
||||
"ford.diameter=1500": "1500",
|
||||
"ford.width": "월류 폭 (m)",
|
||||
"ford.height": "월류 높이 (m)",
|
||||
"ford.count": "수량 (련)",
|
||||
"inlet.structure": "구조",
|
||||
"inlet.structure=없음": "없음",
|
||||
"inlet.structure=날개벽": "날개벽",
|
||||
"inlet.structure=흙막이": "흙막이",
|
||||
"outlet.structure": "구조",
|
||||
"outlet.structure=없음": "없음",
|
||||
"outlet.structure=날개벽": "날개벽",
|
||||
"outlet.structure=흙막이": "흙막이"
|
||||
}
|
||||
},
|
||||
"revetment": {
|
||||
"동작": [
|
||||
@@ -3352,6 +3383,17 @@
|
||||
"옵션": [],
|
||||
"형상": {}
|
||||
}
|
||||
],
|
||||
"cross_underdrain": [
|
||||
{
|
||||
"id": "gcrossud",
|
||||
"이름": "맹암거",
|
||||
"옵션": [
|
||||
"culvert_height_m",
|
||||
"culvert_slope_pct"
|
||||
],
|
||||
"형상": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"배치": {},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,86 @@
|
||||
/* 세월교 명세(표 04) · 월류 폭 ↔ 높이 계산 단위검증 헬퍼(PLAN 52-4) — `design_structure_spec.ts` ·
|
||||
* `design_structure_ford.ts` · `design_structure_ford_calc.ts`(+ `fordSection` 이 든 B05 칸 조각)를 그 자리에서
|
||||
* 트랜스파일해 Node 로 돌림. 결과 = JSON 한 벌. 인자 `names` 면 세월교 이름 표(정의 `종류.ford_bridge.이름`)만 냄.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const cache = new Map();
|
||||
/** TS 한 파일을 CommonJS 로 — 상대 import 는 그 파일 자리 기준 · `@config/` 는 config 폴더 · .json 은 그대로. */
|
||||
const load = (file) => {
|
||||
if (cache.has(file)) return cache.get(file);
|
||||
if (file.endsWith(".json")) return JSON.parse(fs.readFileSync(file, "utf8"));
|
||||
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(file, box.exports);
|
||||
const near = (m) => {
|
||||
const base = m.startsWith("@config/")
|
||||
? path.join(ROOT, "config", m.slice(8))
|
||||
: path.join(path.dirname(file), m);
|
||||
return load(base.endsWith(".json") ? base : `${base}.ts`);
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, near);
|
||||
cache.set(file, box.exports);
|
||||
return box.exports;
|
||||
};
|
||||
const D = (f) => path.join(ROOT, "A00_Common", "design", f);
|
||||
const s = load(D("design_structure_spec.ts"));
|
||||
const { FORD_SPEC, withLegacyGuards } = load(D("design_structure_ford.ts"));
|
||||
const calc = load(D("design_structure_ford_calc.ts"));
|
||||
|
||||
if (process.argv[2] === "names") {
|
||||
process.stdout.write(JSON.stringify(s.nameTable(FORD_SPEC)));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const old = {
|
||||
pipe_kind: "파형강관",
|
||||
pipe_diameter_mm: 1000,
|
||||
pipe_count: 2,
|
||||
ford_width_m: 10,
|
||||
ford_height_m: 0.3,
|
||||
wing_in: "있음",
|
||||
wing_in_height_m: 1,
|
||||
wing_out: "없음",
|
||||
};
|
||||
const legacy = withLegacyGuards(old);
|
||||
const state = s.readState(FORD_SPEC, legacy);
|
||||
const back = s.writeOptions(FORD_SPEC, state);
|
||||
const soil = JSON.parse(JSON.stringify(state));
|
||||
soil.inlet[0].structure = "흙막이";
|
||||
soil.outlet[0].structure = "날개벽";
|
||||
const soilOut = s.writeOptions(FORD_SPEC, soil);
|
||||
|
||||
const flows = [0.5, 2.54, 8];
|
||||
const widths = [3, 5, 10, 20];
|
||||
const table = flows.flatMap((q) => widths.map((b) => [q, b, calc.fordHeightM(q, b)]));
|
||||
|
||||
const out = {
|
||||
containers: FORD_SPEC.containers.map((c) => [c.id, c.title, c.required]),
|
||||
items: FORD_SPEC.containers.map((c) => c.items.map((i) => [i.id, i.kind, i.key ?? null])),
|
||||
choices: FORD_SPEC.containers.map((c) =>
|
||||
c.items.filter((i) => i.kind === "select").map((i) => s.choicesOf(i, {}).map((ch) => ch.label)),
|
||||
),
|
||||
legacy: { inlet: legacy.inlet_type, outlet: legacy.outlet_type },
|
||||
keepSoil: withLegacyGuards({ inlet_type: "흙막이", wing_in: "없음" }).inlet_type,
|
||||
b06Off: withLegacyGuards({ inlet_type: "날개벽", wing_in: "없음" }).inlet_type,
|
||||
b06On: withLegacyGuards({ inlet_type: "없음", wing_in: "있음" }).inlet_type,
|
||||
state,
|
||||
back,
|
||||
soilOut,
|
||||
links: FORD_SPEC.containers.map((c) => s.linkOf(c, 1)),
|
||||
keys: s.specKeys(FORD_SPEC),
|
||||
retired: FORD_SPEC.retired,
|
||||
table,
|
||||
none: [calc.fordHeightM(null, 10), calc.fordHeightM(2.54, 0)],
|
||||
hints: [
|
||||
calc.fordHeightHint(2.54, 10),
|
||||
calc.fordHeightHint(2.54, 0),
|
||||
calc.fordHeightHint(null, 10),
|
||||
],
|
||||
};
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -13,7 +13,8 @@ const load = (file, deps = {}) => {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
new Function("exports", "module", "require", js)(box.exports, box, (m) => deps[m] ?? require(m));
|
||||
const near = (m) => (m.startsWith("./") ? load(`${m.slice(2)}.ts`) : require(m)); // 같은 폴더 명세
|
||||
new Function("exports", "module", "require", js)(box.exports, box, (m) => deps[m] ?? near(m));
|
||||
return box.exports;
|
||||
};
|
||||
const s = load("design_structure_spec.ts");
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/* M02 구조물 명세 이름 고치기(`M02_SO_Def_Spec_Model.ts` · PLAN 52-11) 단위검증 헬퍼 —
|
||||
* TS 를 그 자리에서 트랜스파일해 Node 로 돌리고 결과를 JSON 으로 냄(pytest 가 판정).
|
||||
* 정의는 진짜 `구조물정의.json` 을 읽어 메모리 사본만 고침(파일 쓰기 없음). */
|
||||
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 = (...parts) => {
|
||||
const js = ts.transpileModule(fs.readFileSync(path.join(ROOT, ...parts), "utf8"), {
|
||||
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 m = load("M02_MasterTemplete", "StructOptions", "M02_SO_Def_Spec_Model.ts");
|
||||
const { PIPE_SPEC } = load("A00_Common", "design", "design_structure_pipe.ts");
|
||||
const real = () =>
|
||||
JSON.parse(
|
||||
fs.readFileSync(path.join(ROOT, "resources", "master_template", "구조물정의.json"), "utf8"),
|
||||
);
|
||||
|
||||
const out = {};
|
||||
const def = real();
|
||||
const rows = m.specNameRows(def, PIPE_SPEC);
|
||||
out.levels = [0, 1, 2].map((lv) => rows.filter((r) => r.level === lv).length);
|
||||
out.unique = new Set(rows.map((r) => r.nameKey)).size === rows.length;
|
||||
out.pipeTitle = rows.find((r) => r.nameKey === "pipe");
|
||||
out.optional = rows.find((r) => r.nameKey === "soil_add");
|
||||
out.basinForm = rows.find((r) => r.nameKey === "inlet.form=I");
|
||||
|
||||
// 이름 고침 — 그룹 제목 · 항목 · 값 · 선택 컨테이너
|
||||
const work = real();
|
||||
const pick = (k) => m.specNameRows(work, PIPE_SPEC).find((r) => r.nameKey === k);
|
||||
m.setSpecName(work, "pipe", pick("pipe"), "관 본체");
|
||||
m.setSpecName(work, "pipe", pick("pipe.diameter"), "관 지름");
|
||||
m.setSpecName(work, "pipe", pick("pipe.diameter=800"), "Ø800");
|
||||
m.setSpecName(work, "pipe", pick("soil_add"), "흙막이 더");
|
||||
out.groupName = work.그룹.pipe.find((g) => g.id === "grv6lny").이름;
|
||||
out.names = ["pipe", "pipe.diameter", "pipe.diameter=800", "soil_add"].map(
|
||||
(k) => work.종류.pipe.이름[k] ?? null,
|
||||
);
|
||||
out.reread = ["pipe", "pipe.diameter", "pipe.diameter=800", "soil_add"].map((k) => pick(k).label);
|
||||
// 구조는 그대로 — types · 동작 · 다른 종류 · 이름 표 키 수(새 키 없음)
|
||||
const before = real();
|
||||
out.sameTypes = JSON.stringify(work.types) === JSON.stringify(before.types);
|
||||
out.sameKeys =
|
||||
Object.keys(work.종류.pipe.이름).length === Object.keys(before.종류.pipe.이름).length;
|
||||
|
||||
// 이름 표가 빈 종류 — 명세 글로 읽고 고치면 이름 표가 생김
|
||||
const bare = real();
|
||||
delete bare.종류.pipe.이름;
|
||||
const bareRows = m.specNameRows(bare, PIPE_SPEC);
|
||||
out.bareLabel = bareRows.find((r) => r.nameKey === "inlet.structure=기슭막이").label;
|
||||
m.setSpecName(
|
||||
bare,
|
||||
"pipe",
|
||||
bareRows.find((r) => r.nameKey === "inlet"),
|
||||
"유입",
|
||||
);
|
||||
out.bareGroup = bare.그룹.pipe.find((g) => g.id === "gnbf9m9").이름;
|
||||
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -0,0 +1,109 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""횡단배수 › 세월교(표 04) 명세 + 월류 폭 ↔ 높이 연동 — PLAN 52-4.
|
||||
|
||||
순수 규칙은 Node 헬퍼(`helper_52_4_ford.cjs`)가 돌림 · 화면은 ORCA `ui_checks/52-4_세월교_컨테이너.js`.
|
||||
높이 거울 = 이 파일이 Manning(통수단면.md §2 · n 0.017 · S 0.10)을 따로 풀어 같은 값인지 봄.
|
||||
"""
|
||||
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(os.path.dirname(HERE))
|
||||
N, S = 0.017, 0.10
|
||||
|
||||
|
||||
def _node(*args):
|
||||
proc = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_52_4_ford.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"] == [
|
||||
["ford", "세월교", True],
|
||||
["inlet", "유입 구조", True],
|
||||
["outlet", "유출 구조", True],
|
||||
]
|
||||
assert out["items"][0] == [
|
||||
["diameter", "select", "pipe_diameter_mm"],
|
||||
["width", "number", "ford_width_m"],
|
||||
["height", "number", "ford_height_m"],
|
||||
["count", "number", "pipe_count"],
|
||||
]
|
||||
assert out["choices"] == [
|
||||
[["800", "1000", "1200", "1500"]],
|
||||
[["없음", "날개벽", "흙막이"]],
|
||||
[["없음", "날개벽", "흙막이"]],
|
||||
]
|
||||
|
||||
|
||||
def test_옛_날개벽_칸으로_구조를_세우고_같은_키로_씀(out):
|
||||
assert out["legacy"] == {"inlet": "날개벽", "outlet": "없음"}
|
||||
assert out["keepSoil"] == "흙막이" # 흙막이는 설치 칸 없음이어도 그대로
|
||||
assert out["b06Off"] == "없음" and out["b06On"] == "날개벽" # B06 조정창 설치 칸을 따름
|
||||
back = out["back"]
|
||||
assert back["pipe_diameter_mm"] == 1000 and back["pipe_count"] == 2
|
||||
assert back["ford_width_m"] == 10 and back["ford_height_m"] == 0.3
|
||||
assert back["wing_in"] == "있음" and back["wing_out"] == "없음"
|
||||
# 표 04 에 없는 옛 칸은 폼이 안 적음(병합이 이어 붙임 = 지난 정의 값)
|
||||
assert "pipe_kind" not in back and "wing_in_height_m" not in back
|
||||
assert "pipe_kind" in out["retired"]
|
||||
|
||||
|
||||
def test_흙막이_날개벽_고르면_옛_설치_칸도_맞춰_씀(out):
|
||||
s = out["soilOut"]
|
||||
assert s["inlet_type"] == "흙막이" and s["wing_in"] == "없음"
|
||||
assert s["outlet_type"] == "날개벽" and s["wing_out"] == "있음"
|
||||
|
||||
|
||||
def test_잇기_키_컨테이너마다(out):
|
||||
assert out["links"] == ["gford", "gfordin", "gfordout"]
|
||||
|
||||
|
||||
def _manning_depth(q, b):
|
||||
"""넓은 직사각 수로 Manning 을 h 에 대해 이분법으로 풂 — 헬퍼와 다른 풀이."""
|
||||
|
||||
def cap(h):
|
||||
return b * h * (1 / N) * ((b * h) / (b + 2 * h)) ** (2 / 3) * math.sqrt(S)
|
||||
|
||||
lo, hi = 0.0, 5.0
|
||||
for _ in range(100):
|
||||
mid = (lo + hi) / 2
|
||||
lo, hi = (mid, hi) if cap(mid) < q else (lo, mid)
|
||||
return hi
|
||||
|
||||
|
||||
def test_월류_높이는_폭과_연동_필요_수심_올림(out):
|
||||
for q, b, h in out["table"]:
|
||||
need = _manning_depth(q, b)
|
||||
assert h == pytest.approx(math.ceil(need * 100 - 1e-9) / 100), (q, b)
|
||||
# 폭이 넓을수록 높이가 낮아짐
|
||||
row = [h for q, b, h in out["table"] if q == 2.54]
|
||||
assert row == sorted(row, reverse=True) and len(set(row)) == len(row)
|
||||
assert out["none"] == [None, None] # 설계유량 · 폭이 없으면 손 입력
|
||||
assert out["hints"] == ["필요 수심 0.08 이상", "월류 폭을 넣으면 셈", "설계유량 없음 — 손 입력"]
|
||||
|
||||
|
||||
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 doc["종류"]["ford_bridge"]["이름"] == names
|
||||
@@ -0,0 +1,91 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""52-6 횡단배수 › 맹암거(표 06 · 새 종류) — 정의 · 컨테이너 · 서버 저장 왕복.
|
||||
|
||||
새 키 `cross_underdrain`(횡단배수 · 점형) — 종단배수 맹암거(`underdrain`)와 따로.
|
||||
컨테이너 = 정의 그룹 「맹암거」(필수 · 늘 한 벌) — 암거 높이(m) · 암거 경사(%) 손 입력(비워 둘 수 있음) ·
|
||||
그룹 잇기(`group_items`)로 구조물 조합 항목. 저장은 임시 storage 에서만.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from B05_Profile.B05_Profile_Structures_Schema import (
|
||||
StructureInstance,
|
||||
is_station_planting_type,
|
||||
load_definition,
|
||||
structure_type_map,
|
||||
)
|
||||
|
||||
KEY = "cross_underdrain"
|
||||
GROUP = "gcrossud"
|
||||
|
||||
|
||||
def test_새_종류는_횡단배수_점형_종단배수_맹암거와_따로():
|
||||
types = structure_type_map()
|
||||
new, old = types[KEY], types["underdrain"]
|
||||
assert (new.group, new.placement, new.name, new.enabled) == ("A", "point", "맹암거", True)
|
||||
assert (old.group, old.placement) == ("B", "interval")
|
||||
assert new.style["abbr"] != old.style["abbr"] and new.style["color"] != old.style["color"]
|
||||
assert is_station_planting_type(new) # 횡단배수 = 그 측점에 횡단도
|
||||
|
||||
|
||||
def test_컨테이너_맹암거_높이_경사_손_입력():
|
||||
opts = {o.key: o for o in structure_type_map()[KEY].options}
|
||||
assert list(opts) == ["culvert_height_m", "culvert_slope_pct"]
|
||||
for key, label, unit in (
|
||||
("culvert_height_m", "암거 높이", "m"),
|
||||
("culvert_slope_pct", "암거 경사", "%"),
|
||||
):
|
||||
o = opts[key]
|
||||
assert (o.label, o.input, o.unit, o.default, o.required) == (
|
||||
label,
|
||||
"number",
|
||||
unit,
|
||||
None,
|
||||
False,
|
||||
)
|
||||
assert o.empty_means # 손 입력 · 기본값 근거 없음 — 비워 둘 수 있음(B05 단계 필수 금지)
|
||||
groups = load_definition()["그룹"][KEY]
|
||||
assert groups == [
|
||||
{"id": GROUP, "이름": "맹암거", "옵션": list(opts), "형상": {}},
|
||||
]
|
||||
|
||||
|
||||
def test_정의_검사_통과():
|
||||
from M02_MasterTemplete.StructOptions import M02_SO_Definition as definition
|
||||
|
||||
assert definition.check(load_definition()) == []
|
||||
|
||||
|
||||
def _item(**options):
|
||||
return StructureInstance(
|
||||
type_id=KEY,
|
||||
placement="point",
|
||||
chainage_m=63.5,
|
||||
options=options,
|
||||
group_items={
|
||||
GROUP: {"layer": "system", "owner": None, "key": "SS000001", "name": "맹암거"}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_저장_다시_읽기_그대로(tmp_path):
|
||||
from B05_Profile.B05_Profile_Structures_Repository import load_structures, save_structures
|
||||
|
||||
item = _item(culvert_height_m=0.6, culvert_slope_pct=2.5)
|
||||
save_structures(str(tmp_path), [item], base_revision=0)
|
||||
_, loaded = load_structures(str(tmp_path))
|
||||
assert [(s.type_id, s.chainage_m, s.options) for s in loaded] == [
|
||||
(KEY, 63.5, {"culvert_height_m": 0.6, "culvert_slope_pct": 2.5})
|
||||
]
|
||||
assert loaded[0].group_items == item.group_items
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"options",
|
||||
[{"culvert_height_m": -1}, {"culvert_slope_pct": -0.5}],
|
||||
)
|
||||
def test_음수는_서버가_막음(tmp_path, options):
|
||||
from B05_Profile.B05_Profile_Structures_Repository import save_structures
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
save_structures(str(tmp_path), [_item(**options)], base_revision=0)
|
||||
@@ -74,7 +74,7 @@ def test_정의_덧칸_얹기(out):
|
||||
|
||||
|
||||
def test_판1_정의는_지금_화면과_같음(out):
|
||||
assert out["v1Types"] == 37
|
||||
assert out["v1Types"] == 38 # 52-6 횡단배수 맹암거 더함
|
||||
assert out["v1Same"] is True
|
||||
assert out["v1Columns"] == [2]
|
||||
assert out["v1Anchor"] is True
|
||||
|
||||
@@ -189,7 +189,7 @@ def test_quick_add_types_exclude_pipe_family():
|
||||
|
||||
|
||||
def test_all_enabled_non_managed_types_are_quick_addable():
|
||||
"""잔존 B05 선택지 = A군 2(노출형·개거) + C군 5 + D군 3 + E군 10 + G군 1(콘크리트 포장) + 기타 1.
|
||||
"""잔존 B05 선택지 = A군 3(노출형·개거·맹암거) + C군 5 + D군 3 + E군 10 + G군 1(콘크리트 포장) + 기타 1.
|
||||
|
||||
콘크리트 포장은 2026-08-28 사용자 지시로 되살렸다 — 포장 구간을 기준측점+길이+전/후로
|
||||
받는 정본이 됐다(종단경사 자동 포장 적용을 대체).
|
||||
@@ -202,7 +202,7 @@ def test_all_enabled_non_managed_types_are_quick_addable():
|
||||
그래서 C군 구간형 시설과 같은 자리에 선다. (측구쪽 `ditch_berm` 은 우클릭 추가 대상이
|
||||
아니라 여기 수에 안 잡힌다.)
|
||||
"""
|
||||
assert len(_quick_add_types()) == 23
|
||||
assert len(_quick_add_types()) == 24
|
||||
|
||||
|
||||
def test_removed_types_are_absent():
|
||||
@@ -366,6 +366,7 @@ def test_a6_a7_types_exist_for_manual_placement():
|
||||
item = by_id[type_id]
|
||||
assert item.group == "A" and item.managed_by is None
|
||||
assert item.placement == "point"
|
||||
assert not item.options, f"{type_id} 옵션 없음 — 기준 측점 + 종류 단위 잇기만(PLAN 52-5)"
|
||||
|
||||
|
||||
def test_ford_bridge_shares_pipe_kind_and_diameter():
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
"""M02 구조물 명세 이름 고치기(`M02_SO_Def_Spec_Model.ts` · PLAN 52-11) 단위검증.
|
||||
|
||||
Node 헬퍼(`helper_m02_so_def_spec.cjs`)가 TS 를 트랜스파일해 진짜 정의 사본(메모리)으로 돌리고
|
||||
pytest 는 그 결과(JSON)를 판정.
|
||||
|
||||
못박는 것
|
||||
- 배관 명세 나무: 컨테이너 5 · 항목 · 값 줄 · 이름 키 겹침 없음 · 필수/선택 · 꼴 표시.
|
||||
- 필수 컨테이너 제목은 정의 그룹 이름에 씀 · 항목 · 값 · 선택 컨테이너는 이름 표에 씀.
|
||||
- 이름만 바뀜 — types(옵션 · 키) 그대로 · 이름 표에 없는 키를 새로 만들지 않음.
|
||||
- 이름 표가 빈 종류는 명세 글로 읽음.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def r() -> dict:
|
||||
proc = subprocess.run(
|
||||
["node", str(HERE / "helper_m02_so_def_spec.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_배관_나무(r) -> None:
|
||||
assert r["levels"][0] == 5
|
||||
assert r["levels"][1] > 5 and r["levels"][2] > 10
|
||||
assert r["unique"] is True
|
||||
assert r["pipeTitle"]["tag"] == "required" and r["pipeTitle"]["group"] == "grv6lny"
|
||||
assert r["optional"]["tag"] == "optional" and r["optional"]["group"] is None
|
||||
assert r["basinForm"]["label"] == "I형" and r["basinForm"]["key"] == "I"
|
||||
|
||||
|
||||
def test_이름_고침_자리(r) -> None:
|
||||
assert r["groupName"] == "관 본체"
|
||||
assert r["names"] == [None, "관 지름", "Ø800", "흙막이 더"]
|
||||
assert r["reread"] == ["관 본체", "관 지름", "Ø800", "흙막이 더"]
|
||||
|
||||
|
||||
def test_구조는_그대로(r) -> None:
|
||||
assert r["sameTypes"] is True and r["sameKeys"] is True
|
||||
|
||||
|
||||
def test_이름_표_없는_종류(r) -> None:
|
||||
assert r["bareLabel"] == "흙막이"
|
||||
assert r["bareGroup"] == "유입"
|
||||
@@ -0,0 +1,155 @@
|
||||
/* 52-11 — M02 「구조물 옵션 정의」 = 구조물 명세 이름 고치기(명세 있는 종류) · B05 가 그 이름으로 그림
|
||||
* 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m5211 = "단계";` 를 붙여
|
||||
* M02 마스터 층(#/m02-master-template?root=master) · 시스템 관리자:
|
||||
* orca eval --expression "window.__m5211='m02'; $(cat 이 파일)"
|
||||
* B05 종단설계(#/b05-profile) · 목록에 「배관 1」:
|
||||
* orca eval --expression "window.__m5211='b05'; $(cat 이 파일)"
|
||||
* 쓰는 것: 없음 — M02 는 화면 사본만 고치고 [저장] 안 누름 · 되돌린 뒤 판 그대로를 GET 으로 봄 ·
|
||||
* B05 는 받아 둔 종류 객체(메모리)만 고침 · 끝나면 orca reload 로 사본을 버림
|
||||
* 기대 수치(m02):
|
||||
* - spec = 배관 나무 줄(컨테이너 5) · old = 옛 편집기 0(옵션 · 배치 · 그룹 · 규칙 · 기준 측점) · other = 명세 없는 종류는 옛 편집기
|
||||
* - 이름 칸만 고침: inputs = 줄 수 · 키 · 꼴은 글(입력 칸 아님)
|
||||
* - 컨테이너 제목 · 항목 이름 · 값 이름 고침 → 미리보기 legend · 칸 이름 · 선택지 글 따라감 · [저장] 켜짐
|
||||
* - 빈 이름 → issues 「명세 이름」 · [저장] 꺼짐 · 되돌리면 이름 그대로 · 판 그대로
|
||||
* 기대 수치(b05): 사본 이름(컨테이너 · 항목 · 값)이 배관 폼에 그대로 → named.* = true
|
||||
*/
|
||||
(async () => {
|
||||
const MODE = window.__m5211 || "m02";
|
||||
const W = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const $ = (s, root = document) => root.querySelector(s);
|
||||
const $$ = (s, root = document) => [...root.querySelectorAll(s)];
|
||||
const until = async (fn, ms = 10000) => {
|
||||
for (let t = 0; t < ms; t += 150) {
|
||||
const v = fn();
|
||||
if (v) return v;
|
||||
await W(150);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const put = (box, value) => {
|
||||
box.value = value;
|
||||
box.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
box.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
};
|
||||
const NEW = { pipe: "관 본체X", "pipe.diameter": "관 지름X", "pipe.diameter=800": "Ø800X" };
|
||||
const out = { mode: MODE };
|
||||
|
||||
if (MODE === "m02") {
|
||||
const getDef = async () =>
|
||||
(await fetch("/api/m02/structure-definition", { credentials: "include" })).json();
|
||||
const first = await getDef();
|
||||
out.판 = first.판;
|
||||
const row = await until(() => $("[data-options-row]"));
|
||||
if (!row) return JSON.stringify({ error: "정의 줄 없음" });
|
||||
row.click();
|
||||
if (!(await until(() => $$("[data-sod-kind]").length)))
|
||||
return JSON.stringify({ error: "종류 목록 없음" });
|
||||
// 명세 없는 종류 — 옛 편집기 그대로
|
||||
$('[data-sod-kind="retaining_wall"]').click();
|
||||
await W(200);
|
||||
out.other = { base: !!$("[data-sod-base]"), spec: !!$("[data-sod-spec-block]") };
|
||||
// 배관 — 명세 이름 나무
|
||||
$('[data-sod-kind="pipe"]').click();
|
||||
await W(200);
|
||||
const block = $("[data-sod-spec-block]");
|
||||
const detail = $("[data-sod-detail]");
|
||||
const rows = $$("[data-sod-spec]", block);
|
||||
out.spec = {
|
||||
rows: rows.length,
|
||||
containers: $$(".is-level-0", block).length,
|
||||
inputs: $$("input, select, textarea", block).filter(
|
||||
(n) => !n.closest("[data-sod-spec-preview]"),
|
||||
).length,
|
||||
nameInputs: $$("[data-sod-spec-name]", block).length,
|
||||
keysReadOnly: rows.every((r) => r.querySelector("small.m02-opts__key")),
|
||||
};
|
||||
out.old = {
|
||||
base: !!$("[data-sod-base]", detail),
|
||||
blocks: detail.children.length, // 머리 칸 + 명세 칸 = 2
|
||||
};
|
||||
const save = $("[data-sod-save]");
|
||||
out.saveBefore = save.disabled;
|
||||
const box = (k) => $(`[data-sod-spec="${k}"] [data-sod-spec-name]`);
|
||||
const orig = Object.fromEntries(Object.keys(NEW).map((k) => [k, box(k).value]));
|
||||
for (const [k, v] of Object.entries(NEW)) {
|
||||
put(box(k), v);
|
||||
await W(150);
|
||||
}
|
||||
const preview = $("[data-sod-spec-preview]");
|
||||
const pipeBox = $('[data-spec-container="pipe"]', preview);
|
||||
out.preview = {
|
||||
title: pipeBox.querySelector("legend").textContent.includes(NEW.pipe),
|
||||
item: pipeBox
|
||||
.querySelector('[data-spec-item="pipe.diameter"]')
|
||||
.textContent.includes(NEW["pipe.diameter"]),
|
||||
choice: [...pipeBox.querySelectorAll('[data-spec-item="pipe.diameter"] option')].some(
|
||||
(o) => o.textContent === NEW["pipe.diameter=800"],
|
||||
),
|
||||
};
|
||||
out.saveAfterEdit = $("[data-sod-save]").disabled;
|
||||
// 빈 이름 → 저장 막힘
|
||||
put(box("pipe.diameter"), "");
|
||||
await W(150);
|
||||
out.blank = {
|
||||
issue: $("[data-sod-issues]").textContent.includes("명세 이름"),
|
||||
save: $("[data-sod-save]").disabled,
|
||||
};
|
||||
// 되돌림
|
||||
for (const [k, v] of Object.entries(orig)) {
|
||||
put(box(k), v);
|
||||
await W(150);
|
||||
}
|
||||
out.restored = Object.entries(orig).every(([k, v]) => box(k).value === v);
|
||||
out.판After = (await getDef()).판;
|
||||
out.ok =
|
||||
out.other.base &&
|
||||
!out.other.spec &&
|
||||
out.spec.containers === 5 &&
|
||||
out.spec.inputs === out.spec.rows &&
|
||||
out.spec.nameInputs === out.spec.rows &&
|
||||
out.spec.keysReadOnly &&
|
||||
!out.old.base &&
|
||||
out.old.blocks === 2 &&
|
||||
out.preview.title &&
|
||||
out.preview.item &&
|
||||
out.preview.choice &&
|
||||
!out.saveAfterEdit &&
|
||||
out.blank.issue &&
|
||||
out.blank.save &&
|
||||
out.restored &&
|
||||
out.판After === out.판;
|
||||
} else {
|
||||
const sec = $(".b05-structure-section");
|
||||
if (!sec) return JSON.stringify({ error: "구조물 배치 칸 없음" });
|
||||
const url = performance
|
||||
.getEntriesByType("resource")
|
||||
.map((e) => e.name)
|
||||
.filter((n) => n.includes("/B05_Profile/B05_Profile_Api_Structures.ts"))
|
||||
.pop();
|
||||
const api = await import(url);
|
||||
const pipe = (await api.fetchStructureTypes(true)).find((t) => t.type_id === "pipe");
|
||||
pipe.spec_names = { ...(pipe.spec_names ?? {}), ...NEW };
|
||||
await until(() => $$(".b05-route__irregular-list li").length);
|
||||
const li = $$(".b05-route__irregular-list li").find((l) =>
|
||||
/배관 1$/.test(l.textContent.trim()),
|
||||
);
|
||||
const spec = () => $(".b05-structure__spec", sec);
|
||||
(li.querySelector("button") ?? li).click();
|
||||
await W(800);
|
||||
if (!(spec() && spec().offsetParent !== null)) (li.querySelector("button") ?? li).click();
|
||||
await until(() => spec() && spec().offsetParent !== null);
|
||||
const pipeBox = $('[data-spec-container="pipe"]', spec());
|
||||
out.named = {
|
||||
title: pipeBox.querySelector("legend").textContent.includes(NEW.pipe),
|
||||
item: pipeBox
|
||||
.querySelector('[data-spec-item="pipe.diameter"]')
|
||||
.textContent.includes(NEW["pipe.diameter"]),
|
||||
choice: [...pipeBox.querySelectorAll('[data-spec-item="pipe.diameter"] option')].some(
|
||||
(o) => o.textContent === NEW["pipe.diameter=800"],
|
||||
),
|
||||
};
|
||||
(li.querySelector("button") ?? li).click(); // 재선택 = 해제
|
||||
out.ok = Object.values(out.named).every(Boolean);
|
||||
}
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
@@ -0,0 +1,181 @@
|
||||
/* 52-4 — B05 · B06 좌측 「구조물 배치」 세월교 = 사용자 구조물 표 04 명세(컨테이너 셋)
|
||||
* 시작 화면: B05 종단설계(#/b05-profile) · 검증 프로젝트(sub6_검증) · 목록에 「세월교 1」
|
||||
* 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m524 = "단계";` 를 붙여
|
||||
* orca eval --expression "window.__m524='edit'; $(cat 이 파일)" → 고치고 [저장] (원래 관 지점은 sessionStorage 에 둠)
|
||||
* (orca reload 로 다시 열기)
|
||||
* orca eval --expression "window.__m524='verify'; $(cat 이 파일)" → 다시 연 값 · B06 같은 그리기
|
||||
* orca eval --expression "window.__m524='restore'; $(cat 이 파일)" → 원래 관 지점으로 되돌림(PUT) · 초안 비움
|
||||
* 쓰는 것: 검증 프로젝트 pipe_points.json(되돌림 단계가 원래 값으로 다시 씀)
|
||||
* 기대 수치(edit): 컨테이너 3(세월교 · 유입 구조 · 유출 구조 · 모두 필수) · 관경 800/1000/1200/1500 ·
|
||||
* 구조 없음/날개벽/흙막이 · 옛 관종 · 날개벽 칸 안 보임 · 폭 8 → 높이 = 필요 수심(설계유량 있으면) ·
|
||||
* 낮게 적으면 필요 수심으로 되돌림 · 수량 3 · 잇기 세월교 · 유입 지정 · 유출 지정 뒤 풂
|
||||
* 기대 수치(verify): 저장 값 그대로 · 같은 그리기(B05 = B06)
|
||||
*/
|
||||
(async () => {
|
||||
const MODE = window.__m524 || "edit";
|
||||
const NAME = /세월교 1$/;
|
||||
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 spec = () =>
|
||||
[...document.querySelectorAll(".b05-structure__spec")].find((n) => n.offsetParent !== null);
|
||||
const box = (c) => spec().querySelector(`[data-spec-container="${c}"]`);
|
||||
const ctl = (c, item) =>
|
||||
box(c)?.querySelector(`[data-spec-item="${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(250);
|
||||
};
|
||||
const labels = (c, item) => [...ctl(c, item).options].filter((o) => o.value).map((o) => o.text);
|
||||
const pickFord = async () => {
|
||||
const find = () =>
|
||||
[...document.querySelectorAll(".b05-route__irregular-list li")].find(
|
||||
(l) => l.offsetParent !== null && NAME.test(l.textContent.trim()),
|
||||
);
|
||||
await until(() => !!find());
|
||||
const li = find();
|
||||
(li.querySelector("button") ?? li).click();
|
||||
await w(800);
|
||||
if (!spec()) (li.querySelector("button") ?? li).click();
|
||||
await until(() => !!spec());
|
||||
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,
|
||||
n.querySelector("select, input").value,
|
||||
]),
|
||||
link: b.querySelector(".b05-structure__summary-name")?.textContent,
|
||||
}));
|
||||
const assigned = (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 visibleKeys = () =>
|
||||
[...document.querySelectorAll(".b05-drainage__facility [data-option-keys]")]
|
||||
.filter((n) => n.offsetParent !== null)
|
||||
.map((n) => n.dataset.optionKeys);
|
||||
const out = { mode: MODE };
|
||||
|
||||
if (MODE === "edit") {
|
||||
const before = await fetchApi.fetchDetailPipePoints(projectId);
|
||||
sessionStorage.setItem("__524_original", JSON.stringify(before.pipe_points));
|
||||
await pickFord();
|
||||
const s0 = shape();
|
||||
out.containers = s0.map((b) => [b.c, b.legend]);
|
||||
out.oldFieldsVisible = visibleKeys(); // 옛 관종 · 월류 · 날개벽 칸 — 빈 목록이어야
|
||||
out.diameters = labels("ford", "diameter");
|
||||
out.inletChoices = labels("inlet", "structure");
|
||||
out.outletChoices = labels("outlet", "structure");
|
||||
out.start = s0.map((b) => b.items);
|
||||
await set("ford", "diameter", "1200");
|
||||
await set("ford", "width", "8");
|
||||
out.heightAfterWidth8 = ctl("ford", "height").value;
|
||||
out.heightHint = ctl("ford", "height").placeholder;
|
||||
await set("ford", "width", "4");
|
||||
out.heightAfterWidth4 = ctl("ford", "height").value;
|
||||
await set("ford", "height", "0.01");
|
||||
out.heightLowBounced = ctl("ford", "height").value;
|
||||
const raised = (Number(out.heightAfterWidth4 || 0) + 0.2).toFixed(2);
|
||||
await set("ford", "height", raised);
|
||||
out.heightRaisedKept = ctl("ford", "height").value === raised;
|
||||
await set("ford", "count", "3");
|
||||
await set("inlet", "structure", "흙막이");
|
||||
await set("outlet", "structure", "날개벽");
|
||||
await pickLink("ford");
|
||||
await pickLink("inlet");
|
||||
await pickLink("outlet");
|
||||
out.linksPicked = ["ford", "inlet", "outlet"].map(assigned);
|
||||
box("outlet").querySelector('[data-b05-summary="clear"]').click();
|
||||
await w(300);
|
||||
out.linkOutletCleared = !assigned("outlet");
|
||||
out.edited = shape().map((b) => b.items);
|
||||
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((x) => x.chainage_m > 170 && x.chainage_m < 176); // 세월교 1 = 8+13
|
||||
out.file = p && { options: p.options, group_items: Object.keys(p.group_items ?? {}) };
|
||||
}
|
||||
|
||||
if (MODE === "verify") {
|
||||
await pickFord();
|
||||
const b05 = shape();
|
||||
out.reopened = b05.map((b) => b.items);
|
||||
out.links = ["ford", "inlet", "outlet"].map(assigned);
|
||||
location.hash = "#/b06-section";
|
||||
await until(() => location.hash.includes("b06"), 5000);
|
||||
await w(6000);
|
||||
await pickFord();
|
||||
const b06 = shape();
|
||||
out.sameDrawing = JSON.stringify(b05) === JSON.stringify(b06);
|
||||
if (!out.sameDrawing) out.diff = { b05, b06 };
|
||||
out.b06WingSlots = ["inlet", "outlet"].map(
|
||||
(c) => box(c).querySelector(".b05-structure__adjust-slot")?.childElementCount ?? -1,
|
||||
);
|
||||
}
|
||||
|
||||
if (MODE === "restore") {
|
||||
const original = JSON.parse(sessionStorage.getItem("__524_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 === "__524_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);
|
||||
}
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
@@ -0,0 +1,158 @@
|
||||
/* 52-5 — B05 · B06 좌측 「구조물 배치」 노출형 횡단수로 · 개거(겉도랑) = 사용자 구조물 표 05
|
||||
* 시작 화면: B05 종단설계(#/b05-profile) · 검증 프로젝트(랩탑_보조) · 폼 비어 있음 · 로그인 상태
|
||||
* 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m525 = "단계";` 를 붙여
|
||||
* orca eval --expression "window.__m525='edit'; $(cat 이 파일)" → 두 종류 모양 · 추가 · 잇기 · [저장]
|
||||
* (orca reload 로 다시 열기)
|
||||
* orca eval --expression "window.__m525='verify'; $(cat 이 파일)" → 저장 값 그대로 · 다시 고름
|
||||
* (B06 으로 옮겨 — 같은 모양)
|
||||
* orca eval --expression "window.__m525='verify'; $(cat 이 파일)" → B06 에서도 같은 값 · 같은 모양
|
||||
* orca eval --expression "window.__m525='restore'; $(cat 이 파일)" → 추가한 둘 삭제 · [저장]
|
||||
* 쓰는 것: 검증 프로젝트 structures.json(되돌림 단계가 지움)
|
||||
* 기대 수치:
|
||||
* shape(종류마다) — optionInputs 0 · containers 0 · summaryRows 1 · summaryVisible true · summaryRightAfterPosition true ·
|
||||
* anchor true · stale false
|
||||
* edit — added 둘 · 잇기 이름 값 있음 · 저장 뒤 목록 2 늘어남
|
||||
* verify — 같은 모양 · 잇기 이름 그대로 · 옵션 값 없음
|
||||
*/
|
||||
(async () => {
|
||||
const MODE = window.__m525 || "edit";
|
||||
const w = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const until = async (fn, ms = 15000) => {
|
||||
for (let t = 0; t < ms; t += 200) {
|
||||
if (fn()) return true;
|
||||
await w(200);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const vis = (n) => !!n && n.offsetParent !== null;
|
||||
const sec = () => document.querySelector(".b05-structure-section");
|
||||
const body = () => sec().querySelector(".b05-route__panel-body");
|
||||
const selects = () => sec().querySelectorAll(".b05-structure__grid select");
|
||||
const fire = (el, value) => {
|
||||
el.value = value;
|
||||
el.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
};
|
||||
const listItems = () => [...document.querySelectorAll(".b05-route__irregular-list li")];
|
||||
const NAMES = { cross_drain_exposed: "노출형 횡단수로", open_ditch: "개거(겉도랑)" };
|
||||
const AT = { cross_drain_exposed: [2, 7], open_ditch: [6, 3] };
|
||||
|
||||
const shape = (typeId) => {
|
||||
const kids = [...body().children];
|
||||
const position = kids.find((c) => c.classList.contains("b05-structure__position-row"));
|
||||
const summary = kids.find((c) => c.classList.contains("b05-structure__summary"));
|
||||
const optionRow = kids.filter((c) => c.classList.contains("b05-structure__grid"))[1];
|
||||
const after = kids.slice(kids.indexOf(position) + 1).find((c) => vis(c));
|
||||
const anchorWrap = position.querySelectorAll(":scope > label")[2];
|
||||
return {
|
||||
typeId,
|
||||
optionInputs: vis(optionRow) ? optionRow.querySelectorAll("input, select").length : 0,
|
||||
containers: [...sec().querySelectorAll("[data-spec-container]")].filter(vis).length,
|
||||
summaryRows: [...sec().querySelectorAll("[data-b05-summary=row]")].filter(vis).length,
|
||||
summaryVisible: vis(summary),
|
||||
summaryRightAfterPosition: after === summary,
|
||||
anchor: vis(anchorWrap),
|
||||
stale: vis(sec().querySelector(".b05-structure__stale")),
|
||||
linked: summary.querySelector(".b05-structure__summary-name")?.textContent ?? "",
|
||||
};
|
||||
};
|
||||
const pickType = async (typeId) => {
|
||||
const [group, type] = selects();
|
||||
fire(group, "A");
|
||||
await w(300);
|
||||
fire(type, typeId);
|
||||
await w(500);
|
||||
};
|
||||
const linkFirstLeaf = async () => {
|
||||
sec().querySelector("[data-b05-summary=pick]").click();
|
||||
await until(() => document.querySelector(".ui-item-picker"));
|
||||
await w(600);
|
||||
const modal = document.querySelector(".ui-item-picker");
|
||||
// 끝 항목 = ▸ · ▾ 로 안 시작하고 모달 조작 단추가 아닌 첫 단추
|
||||
const ctl = ["닫기", "개인용으로 복사", "관리 화면에서 고치기", "해제", "적용"];
|
||||
const leaf = [...modal.querySelectorAll("button")].find((b) => {
|
||||
const t = b.textContent.trim();
|
||||
return t && !/^[▸▾]/.test(t) && !ctl.includes(t);
|
||||
});
|
||||
leaf?.click();
|
||||
await w(300);
|
||||
[...modal.querySelectorAll("button")].find((b) => b.textContent.trim() === "적용")?.click();
|
||||
await w(500);
|
||||
return !!leaf;
|
||||
};
|
||||
const clickBtn = async (text) => {
|
||||
[...document.querySelectorAll("button")]
|
||||
.find((b) => b.textContent.trim() === text && vis(b))
|
||||
?.click();
|
||||
await w(1500);
|
||||
};
|
||||
const out = { mode: MODE, items: [] };
|
||||
|
||||
if (MODE === "edit") {
|
||||
await until(() => listItems().length > 0);
|
||||
out.before = listItems().length;
|
||||
for (const typeId of Object.keys(NAMES)) {
|
||||
await pickType(typeId);
|
||||
const row = { ...shape(typeId), phase: "edit" };
|
||||
const [no, dist] = sec()
|
||||
.querySelectorAll(".b05-structure__position-row > label")[2]
|
||||
.querySelectorAll("input");
|
||||
fire(no, String(AT[typeId][0]));
|
||||
fire(dist, String(AT[typeId][1]));
|
||||
await w(200);
|
||||
row.picked = await linkFirstLeaf();
|
||||
row.linked = shape(typeId).linked;
|
||||
sec().querySelector(".is-primary").click(); // 추가
|
||||
await w(800);
|
||||
out.items.push(row);
|
||||
}
|
||||
out.after = listItems().length;
|
||||
await clickBtn("저장");
|
||||
out.listed = listItems()
|
||||
.map((l) => l.textContent.trim())
|
||||
.filter((t) => /횡단수로|개거/.test(t));
|
||||
} else if (MODE === "verify") {
|
||||
await until(() => listItems().length > 0);
|
||||
for (const typeId of Object.keys(NAMES)) {
|
||||
const li = listItems().find((l) => l.textContent.includes(NAMES[typeId]));
|
||||
if (!li) {
|
||||
out.items.push({ typeId, missing: true });
|
||||
continue;
|
||||
}
|
||||
(li.querySelector("button") ?? li).click();
|
||||
await w(800);
|
||||
// B06 은 다른 항목이 골라져 있으면 첫 눌림이 풀기만 함 — 안 불러와졌으면 한 번 더
|
||||
if (selects()[1].value !== typeId) {
|
||||
(li.querySelector("button") ?? li).click();
|
||||
await w(800);
|
||||
}
|
||||
out.items.push({ ...shape(typeId), phase: "verify", title: li.title || "" });
|
||||
(li.querySelector("button") ?? li).click(); // 재선택 = 해제
|
||||
await w(300);
|
||||
}
|
||||
} else if (MODE === "restore") {
|
||||
await until(() => listItems().length > 0);
|
||||
for (const typeId of Object.keys(NAMES)) {
|
||||
const li = listItems().find((l) => l.textContent.includes(NAMES[typeId]));
|
||||
if (!li) continue;
|
||||
(li.querySelector("button") ?? li).click();
|
||||
await w(600);
|
||||
await clickBtn("삭제");
|
||||
}
|
||||
await clickBtn("저장");
|
||||
out.left = listItems().length;
|
||||
}
|
||||
const rows = out.items.filter((r) => !r.missing);
|
||||
out.shapeOk =
|
||||
rows.length === 2 &&
|
||||
rows.every(
|
||||
(r) =>
|
||||
r.optionInputs === 0 &&
|
||||
r.containers === 0 &&
|
||||
r.summaryRows === 1 &&
|
||||
r.summaryVisible &&
|
||||
r.summaryRightAfterPosition &&
|
||||
r.anchor &&
|
||||
!r.stale,
|
||||
);
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
@@ -0,0 +1,217 @@
|
||||
/* 52-6 — 횡단배수 › 맹암거(표 06 · 새 종류 `cross_underdrain`) · B05 · B06 「구조물 배치」
|
||||
* 시작 화면: B05 종단설계(#/b05-profile) · 아무것도 안 고름 · 측점 3+7 이 비어 있음
|
||||
* 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m526 = "단계";` 를 붙여
|
||||
* orca eval --expression "window.__m526='edit'; $(cat 이 파일)" → 종류 목록 · 컨테이너 · 값 · 잇기 · [추가] · 다시 열기
|
||||
* orca eval --expression "window.__m526='b06'; $(cat 이 파일)" → B06 에서 같은 항목 다시 열기 · B05 와 같은 그리기
|
||||
* orca eval --expression "window.__m526='save'; $(cat 이 파일)" → [저장] 요청 가로챔(몸통만 봄)
|
||||
* orca eval --expression "window.__m526='restore'; $(cat 이 파일)" → sessionStorage 를 처음 그대로 · 뒤에 orca reload
|
||||
* 쓰는 것: 없음 — [저장] 은 fetch 를 가로채 서버로 안 보냄(공유 storage) · 서버 저장 왕복은 test_52_6_cross_underdrain
|
||||
* 기대 수치(edit): b05Types 에 cross_underdrain:맹암거 · underdrain 없음 · box 맹암거 · keys 높이 · 경사 ·
|
||||
* 잇기 줄 1(맹암거 상자 안만 · 종류 단위 줄 가림 · 52-6b) · pending 높이 0.6 · 경사 2.5 · 잇기 gcrossud 지정 · reopened 같음
|
||||
* 기대 수치(b06): b06Types A 에 맹암거 · B 에 종단배수 맹암거 따로 · sameDrawing · 값 같음
|
||||
* 기대 수치(save): saved = PUT 몸통에 같은 값(67 m · 0.6 · 2.5 · gcrossud)
|
||||
*/
|
||||
(async () => {
|
||||
const MODE = window.__m526 || "edit";
|
||||
const KEY = "cross_underdrain";
|
||||
const GROUP = "gcrossud";
|
||||
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 vis = (n) => n && n.offsetParent !== null;
|
||||
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 sec = () => document.querySelector(".b05-structure-section");
|
||||
const sels = () => sec().querySelectorAll(".b05-structure__grid select");
|
||||
const choose = async (sel, value) => {
|
||||
sel.value = value;
|
||||
sel.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await w(300);
|
||||
};
|
||||
const typesOf = async (group) => {
|
||||
await choose(sels()[0], group);
|
||||
return [...sels()[1].options].filter((o) => o.value).map((o) => `${o.value}:${o.text}`);
|
||||
};
|
||||
const box = () => sec().querySelector(`[data-b05-group="${GROUP}"]`);
|
||||
const input = (k) => box()?.querySelector(`[data-option-keys="${k}"] input`);
|
||||
const shape = () => {
|
||||
const b = box();
|
||||
return {
|
||||
legend: b?.querySelector("legend").textContent,
|
||||
keys: [...(b?.querySelectorAll("[data-option-keys]") ?? [])].map(
|
||||
(n) => `${n.dataset.optionKeys}|${n.querySelector("span").textContent}`,
|
||||
),
|
||||
values: ["culvert_height_m", "culvert_slope_pct"].map((k) => input(k)?.value),
|
||||
link: b?.querySelector(".b05-structure__summary-name").textContent,
|
||||
linked: b?.querySelector(".b05-structure__summary-name").classList.contains("is-assigned"),
|
||||
linkRows: [...sec().querySelectorAll("[data-b05-summary=row]")].filter(vis).length,
|
||||
};
|
||||
};
|
||||
const item = () =>
|
||||
[...document.querySelectorAll(".b05-route__irregular-list li")].find((l) =>
|
||||
/^3\+7.*맹암거/.test(l.textContent.trim()),
|
||||
);
|
||||
const openItem = async () => {
|
||||
await until(() => item());
|
||||
const li = item();
|
||||
(li.querySelector("button") ?? li).click();
|
||||
await w(600);
|
||||
// 이미 고른 채로 열렸으면 누르면 풀림 — 풀렸으면 한 번 더(재선택 = 해제)
|
||||
if (sels()[1].value !== KEY) (li.querySelector("button") ?? li).click();
|
||||
await until(() => vis(box()));
|
||||
await w(300);
|
||||
};
|
||||
const pickLink = async () => {
|
||||
box().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 pendingMine = () =>
|
||||
(api.readPendingStructures(projectId) ?? []).filter((s) => s.type_id === KEY);
|
||||
const out = { mode: MODE };
|
||||
|
||||
if (MODE === "edit") {
|
||||
sessionStorage.setItem("__526_session", JSON.stringify({ ...sessionStorage }));
|
||||
out.b05Types = await typesOf("A");
|
||||
out.b05HasLongUnderdrain = (await typesOf("B")).some((t) => t.startsWith("underdrain:"));
|
||||
await typesOf("A");
|
||||
await choose(sels()[1], KEY);
|
||||
out.boxBeforeStation = { shown: vis(box()), locked: !!box()?.querySelector(".is-locked") };
|
||||
const pos = [...sec().querySelectorAll(".b05-structure__position-row input")].filter(vis);
|
||||
for (const [el, v] of [
|
||||
[pos[0], "3"],
|
||||
[pos[1], "7"],
|
||||
]) {
|
||||
el.value = v;
|
||||
el.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
el.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
}
|
||||
await w(300);
|
||||
for (const [k, v] of [
|
||||
["culvert_height_m", "0.6"],
|
||||
["culvert_slope_pct", "2.5"],
|
||||
]) {
|
||||
input(k).value = v;
|
||||
input(k).dispatchEvent(new Event("input", { bubbles: true }));
|
||||
input(k).dispatchEvent(new Event("change", { bubbles: true }));
|
||||
}
|
||||
await w(200);
|
||||
await pickLink();
|
||||
out.form = shape();
|
||||
sec().querySelector(".is-primary").click(); // [추가]
|
||||
await w(800);
|
||||
const mine = pendingMine();
|
||||
out.pending = mine.map((s) => ({
|
||||
chainage_m: s.chainage_m,
|
||||
options: s.options,
|
||||
groups: Object.keys(s.group_items ?? {}),
|
||||
}));
|
||||
// 종단 알약 — 약자 · 색이 종단배수 맹암거(맹암거 · #1f7fb8)와 따로
|
||||
const types = await api.fetchStructureTypes(true);
|
||||
const ids = new Set(mine.map((s) => s.structure_id));
|
||||
if (!document.querySelector(".b05-structure__mark")) {
|
||||
[...document.querySelectorAll("button")]
|
||||
.find((b) => b.textContent.trim() === "▲종단")
|
||||
?.click();
|
||||
await until(() => document.querySelector(".b05-structure__mark"), 8000);
|
||||
}
|
||||
out.marks = [...document.querySelectorAll(".b05-structure__mark")]
|
||||
.filter((m) => ids.has(m.dataset.structureId))
|
||||
.map((m) => `${m.textContent}|${m.style.color}`);
|
||||
out.longStyle = types.find((t) => t.type_id === "underdrain")?.style;
|
||||
// 다시 열기 — 폼을 비우고 목록에서 다시 고름
|
||||
const li = item();
|
||||
(li.querySelector("button") ?? li).click(); // 재선택 = 해제
|
||||
await w(400);
|
||||
await openItem();
|
||||
out.reopened = shape();
|
||||
out.reopenSame = JSON.stringify(out.reopened) === JSON.stringify(out.form);
|
||||
}
|
||||
|
||||
if (MODE === "save") {
|
||||
// [저장] — 요청을 가로채 몸통만 봄(서버 · storage 안 감) · 뒤에 화면이 서버 목록을 다시 읽어 항목이 사라짐
|
||||
const real = window.fetch;
|
||||
const sent = [];
|
||||
window.fetch = async (url, init = {}) => {
|
||||
const method = (init.method || "GET").toUpperCase();
|
||||
if (method !== "GET" && String(url).includes(`/projects/${projectId}/`)) {
|
||||
sent.push({ url: String(url), method, body: init.body ? String(init.body) : "" });
|
||||
return new Response(JSON.stringify({ status: "success", revision: 0, count: 0 }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
return real(url, init);
|
||||
};
|
||||
try {
|
||||
document.querySelector('[data-dock-action="save"]').click();
|
||||
await w(3000);
|
||||
await until(() => !document.querySelector(".ui-overlay.is-active"), 20000);
|
||||
} finally {
|
||||
window.fetch = real;
|
||||
}
|
||||
const put = sent.find((r) => r.url.includes("/route/structures") && r.method === "PUT");
|
||||
const body = put ? JSON.parse(put.body) : null;
|
||||
const saved = body?.structures.filter((s) => s.type_id === KEY) ?? [];
|
||||
out.saved = saved.map((s) => ({
|
||||
chainage_m: s.chainage_m,
|
||||
options: s.options,
|
||||
groups: Object.keys(s.group_items ?? {}),
|
||||
}));
|
||||
out.sent = sent.map((r) => `${r.method} ${r.url.split("/api/")[1] ?? r.url}`);
|
||||
}
|
||||
|
||||
if (MODE === "b06") {
|
||||
await openItem();
|
||||
const b05 = shape();
|
||||
location.hash = "#/b06-section";
|
||||
await until(() => location.hash.includes("b06"), 5000);
|
||||
await w(6000);
|
||||
await openItem();
|
||||
const b06 = shape();
|
||||
out.sameDrawing = JSON.stringify(b05) === JSON.stringify(b06);
|
||||
out.b06 = b06;
|
||||
sels()[1].value = "";
|
||||
const li = item();
|
||||
(li.querySelector("button") ?? li).click(); // 고름 풂
|
||||
await w(300);
|
||||
out.b06TypesA = await typesOf("A");
|
||||
out.b06TypesB = await typesOf("B");
|
||||
}
|
||||
|
||||
if (MODE === "restore") {
|
||||
const raw = sessionStorage.getItem("__526_session");
|
||||
if (!raw) return JSON.stringify({ error: "처음 sessionStorage 없음" });
|
||||
const before = JSON.parse(raw);
|
||||
sessionStorage.clear();
|
||||
for (const [k, v] of Object.entries(before)) sessionStorage.setItem(k, v);
|
||||
out.restored = JSON.stringify({ ...sessionStorage }) === JSON.stringify(before);
|
||||
out.mine = pendingMine().length;
|
||||
}
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
@@ -0,0 +1,99 @@
|
||||
/* 52-8 — B05 · B06 좌측 「구조물 배치」 종단배수(B) · 계류사방(D) 고르기 목록에서 숨김
|
||||
* 시작 화면: B05 종단설계(#/b05-profile) · 검증 프로젝트(sub2_검증)
|
||||
* 부르는 법: 앞에 `window.__m528 = "단계";` 를 붙여
|
||||
* orca eval --expression "window.__m528='seed'; $(cat 이 파일)" → 옛 값 둘(골막이 D · 측구 B)을 세션 초안에만 둠
|
||||
* (orca reload)
|
||||
* orca eval --expression "window.__m528='verify'; $(cat 이 파일)" → 군 목록 · 우클릭 메뉴 · 옛 값 열기 (B05)
|
||||
* (#/b06-section 으로 가서 reload 후 verify 한 번 더)
|
||||
* orca eval --expression "window.__m528='clear'; $(cat 이 파일)" → 세션 초안 비움(서버 저장 안 함)
|
||||
* 기대 수치(verify): visibleGroups 에 B · D 없음(0) · hiddenGroups 가림만 · menuHasBD 0 ·
|
||||
* 옛 값 목록 둘 그대로 · 옛 값을 열면 군 = D / B 로 열림(editGroup) · 잇기 칸 pickLinks 골막이(D) 1 · 측구(B) 0
|
||||
*/
|
||||
(async () => {
|
||||
const MODE = window.__m528 || "verify";
|
||||
const w = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
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 out = { mode: MODE, hash: location.hash };
|
||||
if (MODE === "seed") {
|
||||
const base = {
|
||||
placement_source: "manual",
|
||||
status: "draft",
|
||||
revision: 0,
|
||||
geometry: null,
|
||||
memo: "",
|
||||
options: {},
|
||||
};
|
||||
api.writePendingStructures(projectId, [
|
||||
{
|
||||
...base,
|
||||
structure_id: "m528d",
|
||||
type_id: "erosion_check",
|
||||
placement: "point",
|
||||
chainage_m: 40,
|
||||
},
|
||||
{
|
||||
...base,
|
||||
structure_id: "m528b",
|
||||
type_id: "ditch_side",
|
||||
placement: "interval",
|
||||
start_m: 60,
|
||||
end_m: 80,
|
||||
},
|
||||
]);
|
||||
out.seeded = api.readPendingStructures(projectId).length;
|
||||
return JSON.stringify(out);
|
||||
}
|
||||
if (MODE === "clear") {
|
||||
api.writePendingStructures(projectId, null);
|
||||
out.cleared = api.readPendingStructures(projectId) === null;
|
||||
return JSON.stringify(out);
|
||||
}
|
||||
const groupSel = [...document.querySelectorAll("select")].find((s) =>
|
||||
[...s.options].some((o) => o.text.includes("A 횡단배수")),
|
||||
);
|
||||
const isBD = (o) => /^(B|D) /.test(o.text);
|
||||
out.visibleGroups = [...groupSel.options].filter((o) => !o.hidden && o.value).map((o) => o.text);
|
||||
out.visibleBD = [...groupSel.options].filter((o) => !o.hidden && isBD(o)).length;
|
||||
out.hiddenGroups = [...groupSel.options].filter((o) => o.hidden).map((o) => o.text);
|
||||
// 우클릭 메뉴 목록(공용 만들기) — 종류 전부를 줘도 B · D 군이 안 나옴
|
||||
const menuMod = await import(modUrl("/B05_Profile/B05_Profile_UI_Profile_Structures.ts"));
|
||||
const types = (await api.fetchStructureTypes(true)).map((t) => ({
|
||||
type_id: t.type_id,
|
||||
group: t.group,
|
||||
name: t.name,
|
||||
}));
|
||||
const menu = menuMod.structureGroupMenuItems(types, () => {});
|
||||
out.menuGroups = menu.map((m) => m.label);
|
||||
out.menuHasBD = menu.filter((m) => /^(B|D) /.test(m.label)).length;
|
||||
// 옛 값 — 목록에 그대로 · 열면 그 군으로 열림
|
||||
const rows = [...document.querySelectorAll(".b05-route__irregular-list li")];
|
||||
out.rows = rows.map((r) => r.textContent.trim().replace(/\s+/g, " ").slice(0, 40));
|
||||
const old = rows.find((r) =>
|
||||
(window.__m528row ? new RegExp(window.__m528row) : /골막이/).test(r.textContent),
|
||||
);
|
||||
if (old) {
|
||||
(old.querySelector("button") ?? old).click();
|
||||
await w(1500);
|
||||
const sels = [...document.querySelectorAll("select")].filter((s) =>
|
||||
[...s.options].some((o) => o.text.includes("A 횡단배수")),
|
||||
);
|
||||
// 다시 열면 고른 것이 복원돼 있음 — 누르면 풀리므로 풀렸으면 한 번 더
|
||||
if (!sels[0]?.value) {
|
||||
(old.querySelector("button") ?? old).click();
|
||||
await w(1500);
|
||||
}
|
||||
out.editGroup = sels[0]?.value;
|
||||
// 종단배수(B)는 조합 잇기 칸이 없음(0) · 계류사방(D)은 옛 그대로(1)
|
||||
out.pickLinks = [...document.querySelectorAll('[data-b05-summary="pick"]')].filter(
|
||||
(n) => n.offsetParent,
|
||||
).length;
|
||||
}
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
Reference in New Issue
Block a user