Merge remote-tracking branch 'origin/sub_laptop_6' into sub_laptop_7
This commit is contained in:
@@ -215,3 +215,25 @@ export function withDefinition<
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** 대피로(표 07 · PLAN 52-7) — 옛 대피 종류는 고르기 목록에서 빼고 대피로 + 상세로 읽음.
|
||||
* 옛 기록은 고치기 전까지 옛 종류 그대로(도면 · 집계 그대로) · 고치면 대피로로 저장 · 옛 칸 값은 남음. */
|
||||
export const ROAD_TYPE = "evac_road";
|
||||
export const ROAD_DETAILS: Readonly<Record<string, string>> = {
|
||||
refuge: "대피소",
|
||||
work_yard: "정차/작업장",
|
||||
turnaround: "차돌림곳",
|
||||
};
|
||||
|
||||
/** 고르기 목록에 낼 종류인지 — 옛 대피 종류만 뺌. */
|
||||
export const pickable = (type: { type_id: string }): boolean => !ROAD_DETAILS[type.type_id];
|
||||
|
||||
/** 옛 대피 종류 기록 → 대피로 + 상세(옛 값 그대로 · 상세만 채움). 다른 기록은 그대로. */
|
||||
export function asRoad<T extends { type_id: string; options?: Record<string, string | number> }>(
|
||||
record: T,
|
||||
): T {
|
||||
const detail = ROAD_DETAILS[record.type_id];
|
||||
return detail
|
||||
? { ...record, type_id: ROAD_TYPE, options: { detail, ...record.options } }
|
||||
: record;
|
||||
}
|
||||
|
||||
@@ -48,7 +48,8 @@ export function createFacilityDefinition(root: HTMLElement): FacilityDefinition
|
||||
// 정의에 새로 든 선택지는 보기를 더하고, 정의 · 규칙 밖 보기는 가림(빈 보기 · 지금 값은 그대로)
|
||||
const have = new Set([...control.options].map((item) => item.value));
|
||||
for (const choice of state.choices)
|
||||
if (!have.has(choice)) control.add(new Option(choice, choice));
|
||||
if (!have.has(choice))
|
||||
control.add(new Option(type.spec_names?.[`${option.key}=${choice}`] ?? choice, choice));
|
||||
for (const item of control.options) {
|
||||
item.hidden =
|
||||
!!item.value && !state.choices.includes(item.value) && item.value !== control.value;
|
||||
|
||||
@@ -68,6 +68,8 @@ interface OptionShape {
|
||||
export function optionControl(
|
||||
option: OptionShape,
|
||||
stored: string | number | undefined,
|
||||
/** 선택지 보일 글(정의 이름 표 · 52-12) — 값은 그대로 */
|
||||
label: (choice: string) => string = (c) => c,
|
||||
): HTMLInputElement | HTMLSelectElement {
|
||||
const value = stored === undefined || stored === null ? "" : String(stored);
|
||||
// 툴팁 — 표가 아직 안 읽는 칸이면 그 사유가 먼저(넣어도 안 바뀜을 알아야 함), 아니면 비울 때의 뜻.
|
||||
@@ -78,7 +80,7 @@ export function optionControl(
|
||||
const kept = value && !option.choices.includes(value) ? [value] : [];
|
||||
const element = select([
|
||||
["", blank],
|
||||
...[...option.choices, ...kept].map((c) => [c, c] as [string, string]),
|
||||
...[...option.choices, ...kept].map((c) => [c, label(c)] as [string, string]),
|
||||
]);
|
||||
element.value = value;
|
||||
if (tip) element.title = tip;
|
||||
|
||||
@@ -136,9 +136,12 @@ export function createGroupBoxes(): GroupBoxes {
|
||||
for (const scope of scopes) {
|
||||
const marked = [...scope.querySelectorAll<HTMLElement>("[data-option-keys]")];
|
||||
// 지금 숨은 칸(다른 시설 몫 · 안 고른 갈래)은 그 자리에 둠 — 같은 키 칸이 두 벌 옮겨 오지 않게
|
||||
// 자리 자체가 가려졌으면(안 쓰는 시설 폼) 그 칸도 숨은 것 — 같은 키 칸이 딸려 오지 않게(52-7)
|
||||
const shown = (node: HTMLElement): boolean => {
|
||||
for (let at: HTMLElement | null = node; at && at !== scope; at = at.parentElement)
|
||||
for (let at: HTMLElement | null = node; at; at = at.parentElement) {
|
||||
if (at.hidden) return false;
|
||||
if (at === scope) break;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const fits = marked.filter(
|
||||
|
||||
@@ -42,7 +42,7 @@ import {
|
||||
loadPipeForm as loadPipeFormInto,
|
||||
} from "./B05_Profile_UI_Structures_Panel_Commit";
|
||||
import { renderStructureList } from "./B05_Profile_UI_Structures_List";
|
||||
import { showsAnchor } from "./B05_Profile_UI_Structures_Define";
|
||||
import { pickable, showsAnchor } from "./B05_Profile_UI_Structures_Define";
|
||||
import { hiddenOptionKeys } from "./B05_Profile_UI_Structures_Define_Form";
|
||||
import { stationLabel as formatStation } from "../common_util/common_util_station";
|
||||
import { PIPE_MATCH_M, resolveSelection } from "../A00_Common/design/design_selection";
|
||||
@@ -345,7 +345,7 @@ export function createStructuresSection(
|
||||
* 초기·리셋 상태를 "안 고름"으로 드러낸다. */
|
||||
function syncTypeOptions(keepTypeId?: string): void {
|
||||
const group = groupSelect.value;
|
||||
const candidates = group ? types.filter((type) => type.group === group) : [];
|
||||
const candidates = group ? types.filter((t) => t.group === group && pickable(t)) : [];
|
||||
typeSelect.replaceChildren(
|
||||
new Option("— 선택 —", ""),
|
||||
...candidates.map((type) => new Option(type.name, type.type_id)),
|
||||
|
||||
@@ -19,6 +19,7 @@ import type { StationFields } from "./B05_Profile_UI_Structures_Fields";
|
||||
import { splitPavementRange } from "./B05_Profile_UI_Structures_Pavement";
|
||||
import { mergeStructureOptions } from "./B05_Profile_UI_Structures_Summary";
|
||||
import { readStructureSpec, specRenderedKeys } from "./B05_Profile_UI_Structures_Spec";
|
||||
import { asRoad } from "./B05_Profile_UI_Structures_Define";
|
||||
import type { GroupBoxes } from "./B05_Profile_UI_Structures_Groups";
|
||||
import {
|
||||
readGroupItems,
|
||||
@@ -98,16 +99,17 @@ export function loadForm(ctx: StructuresCommitContext, target: StructureInstance
|
||||
ctx.setPositionConfirmed(target !== null);
|
||||
const step = ctx.interval();
|
||||
if (target) {
|
||||
const type = ctx.typeMap().get(target.type_id);
|
||||
const shown = asRoad(target); // 옛 대피 종류 = 대피로 + 상세(52-7)
|
||||
const type = ctx.typeMap().get(shown.type_id);
|
||||
if (type) {
|
||||
ctx.groupSelect.value = type.group;
|
||||
ctx.syncTypeOptions(target.type_id);
|
||||
ctx.syncTypeOptions(shown.type_id);
|
||||
}
|
||||
ctx.anchorFields.write(structureAnchorM(target), step);
|
||||
ctx.startFields.write(target.start_m ?? null, step);
|
||||
ctx.endFields.write(target.end_m ?? null, step);
|
||||
ctx.memoField.value = target.memo ?? "";
|
||||
ctx.renderOptionFields(target.options);
|
||||
ctx.renderOptionFields(shown.options);
|
||||
ctx.setSummary(readSummaryItem(target.summary_item));
|
||||
ctx.groupBoxes.set(readGroupItems(target.group_items));
|
||||
ctx.groupBoxes.setShapes(target.options);
|
||||
@@ -131,7 +133,7 @@ export function loadForm(ctx: StructuresCommitContext, target: StructureInstance
|
||||
afterEntry.input.value = after.toFixed(1);
|
||||
}
|
||||
}
|
||||
ctx.syncFacilityForm(target.options);
|
||||
ctx.syncFacilityForm(shown.options);
|
||||
} else {
|
||||
ctx.anchorFields.write(null, step);
|
||||
ctx.startFields.write(null, step);
|
||||
@@ -370,7 +372,7 @@ export async function commit(ctx: StructuresCommitContext, live = false): Promis
|
||||
...previous,
|
||||
...records[0],
|
||||
options: mergeStructureOptions(
|
||||
previous,
|
||||
asRoad(previous), // 옛 대피 종류를 대피로로 고쳐도 옛 칸 값은 남김(52-7)
|
||||
type.type_id,
|
||||
[
|
||||
...ctx.optionInputs.map((entry) => entry.key),
|
||||
|
||||
@@ -21,14 +21,22 @@ import {
|
||||
} from "./B05_Profile_UI_Structures_Define";
|
||||
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";
|
||||
import { paintStructureSpec, specOwnedKeys } from "./B05_Profile_UI_Structures_Spec";
|
||||
import { solveSplit, specKeys, type SplitRole } from "../A00_Common/design/design_structure_spec";
|
||||
|
||||
/** 종류 단위 「구조물 조합」 줄을 가릴 종류인지 — 구조물 명세 컨테이너 · 정의 그룹 상자(그룹마다 잇기 줄)로
|
||||
* 그리는 종류(PLAN 52-1 · 52-6b) · 종단배수(B군 · 조합 잇기 없음 · 표 07). */
|
||||
* 그리는 종류(PLAN 52-1 · 52-6b · 52-7) · 종단배수(B군 · 조합 잇기 없음 · 표 07). */
|
||||
export const hidesTypeSummary = (type: StructureType): boolean =>
|
||||
!!STRUCTURE_SPECS[type.type_id]?.containers.length || !!type.groups?.length || type.group === "B";
|
||||
|
||||
/** 길이 = 전 + 후 를 배관 맞물림(`solveSplit` · 마지막에 고친 둘을 지킴)으로 푸는 종류(표 07 가드레일). */
|
||||
const SOLVE_SPLIT = new Set(["guardrail"]);
|
||||
const SPLIT_ROLE: Record<string, SplitRole> = {
|
||||
length_m: "length",
|
||||
before_m: "before",
|
||||
after_m: "after",
|
||||
};
|
||||
|
||||
/** 패널이 쥔 값 중 옵션 그리기에 필요한 것들. 이름은 분리 전 지역변수와 같다. */
|
||||
export interface OptionRenderContext {
|
||||
optionInputs: Array<{
|
||||
@@ -86,7 +94,8 @@ export function renderOptionFields(
|
||||
shown.forEach((option) => {
|
||||
// 받은 값만 칸에 — 새 구조물은 부르는 쪽(패널)이 등록부 기본값을 `values` 로 실어 줌(PLAN 46-7).
|
||||
// 보기는 정의 전부로 세워 옛 값을 잃지 않고, 숨김 · 규칙 선택지는 `applyRules` 가 좁힘.
|
||||
const input = optionControl(option, values[option.key]);
|
||||
const name = (c: string): string => type.spec_names?.[`${option.key}=${c}`] ?? c;
|
||||
const input = optionControl(option, values[option.key], name);
|
||||
const label = `${option.unit ? `${option.label} (${option.unit})` : option.label}${option.not_in_table ? " · 표에 안 쓰임" : ""}`;
|
||||
const wrap = field(label, input, option.default_basis);
|
||||
wrap.dataset.optionKeys = option.key; // 옵션 그룹 상자가 이 표식으로 칸을 옮김(PLAN 46-3)
|
||||
@@ -134,12 +143,31 @@ export function renderOptionFields(
|
||||
"length" | "before_m" | "after_m";
|
||||
const refresh = (normalize: boolean): void => {
|
||||
input.classList.remove("is-invalid");
|
||||
ctx.syncSplitInputs(source, normalize);
|
||||
if (SOLVE_SPLIT.has(type!.type_id)) {
|
||||
solveInto(option.key); // 타이핑 중에도 — change 의 실시간 반영이 푼 값을 읽게
|
||||
ctx.syncRangeDisplay();
|
||||
} else ctx.syncSplitInputs(source, normalize);
|
||||
};
|
||||
input.addEventListener("input", () => refresh(false));
|
||||
input.addEventListener("change", () => refresh(true));
|
||||
}
|
||||
});
|
||||
/** 길이 · 전 · 후 맞물림 — 고친 차례(최근이 앞)를 쥐고 나머지 하나를 셈. */
|
||||
const recent: SplitRole[] = [];
|
||||
function solveInto(key: string): void {
|
||||
const role = SPLIT_ROLE[key];
|
||||
recent.splice(0, recent.length, role, ...recent.filter((r) => r !== role));
|
||||
const inputOf = (r: SplitRole) =>
|
||||
all.find(({ entry }) => SPLIT_ROLE[entry.key] === r)?.entry.input;
|
||||
const values = Object.fromEntries(
|
||||
(["length", "before", "after"] as SplitRole[]).map((r) => [r, inputOf(r)?.value ?? ""]),
|
||||
);
|
||||
const solved = solveSplit(values, recent);
|
||||
for (const r of ["length", "before", "after"] as SplitRole[]) {
|
||||
const input = inputOf(r);
|
||||
if (input) input.value = solved[r] ?? "";
|
||||
}
|
||||
}
|
||||
/** 규칙 걸기 — `parent` 가 바뀐 칸이면 그 칸에 매인 기본값부터 넣음. */
|
||||
function applyRules(parent: string | null): void {
|
||||
const now = (): Record<string, string> =>
|
||||
@@ -158,7 +186,11 @@ export function renderOptionFields(
|
||||
wrap.classList.toggle("is-rule-hidden", off); // 그룹 상자로 옮겨 가도 같은 표식
|
||||
if (!off) optionInputs.push(entry);
|
||||
if (state && entry.input.tagName.toLowerCase() === "select")
|
||||
syncChoices(entry.input as HTMLSelectElement, state.choices);
|
||||
syncChoices(
|
||||
entry.input as HTMLSelectElement,
|
||||
state.choices,
|
||||
(c) => type!.spec_names?.[`${entry.key}=${c}`] ?? c,
|
||||
);
|
||||
}
|
||||
}
|
||||
applyRules(null);
|
||||
@@ -167,14 +199,18 @@ export function renderOptionFields(
|
||||
}
|
||||
|
||||
/** 고르기 칸 보기를 규칙 선택지로 — 빈 보기는 남기고, 지금 값이 목록 밖이면 그 값만 남겨 보임. */
|
||||
function syncChoices(element: HTMLSelectElement, choices: string[]): void {
|
||||
function syncChoices(
|
||||
element: HTMLSelectElement,
|
||||
choices: string[],
|
||||
label: (choice: string) => string,
|
||||
): void {
|
||||
const now = [...element.children].slice(1).map((o) => (o as HTMLOptionElement).value);
|
||||
const keep = element.value && !choices.includes(element.value) ? [element.value] : [];
|
||||
const want = [...choices, ...keep];
|
||||
if (now.length === want.length && now.every((v, i) => v === want[i])) return;
|
||||
const value = element.value;
|
||||
const blank = element.children[0];
|
||||
element.replaceChildren(blank, ...want.map((c) => new Option(c, c)));
|
||||
element.replaceChildren(blank, ...want.map((c) => new Option(label(c), c)));
|
||||
element.value = value;
|
||||
}
|
||||
|
||||
|
||||
@@ -513,11 +513,6 @@ def fill_document(
|
||||
missing = sum(1 for item in items if item["type_id"] == "pipe" and not item.get("관연장"))
|
||||
if missing:
|
||||
notes.append(f"관 연장 없음 {missing}곳 — B06 횡단 설계 전이면 빈칸")
|
||||
loose = sum(
|
||||
1 for item in items if item["type_id"] == "guardrail" and not item["options"].get("kind")
|
||||
)
|
||||
if loose:
|
||||
notes.append(f"가드레일·경계석·위험표지 종류 미정 {loose}건 — 표에 안 셈")
|
||||
|
||||
# 손 값 지키기 — 같은 줄 id 의 손 열 · 전구간 줄 · 사용자가 더한 줄
|
||||
hand = _hand_ids(columns)
|
||||
|
||||
@@ -32,6 +32,9 @@ import { checkBox, textBox } from "./M02_SO_Def_Ui";
|
||||
import { buildGroupBlock } from "./M02_SO_Groups";
|
||||
import { to } from "./M02_SO_Text";
|
||||
|
||||
/** 옛 편집기(옵션 · 배치 · 그룹 · 규칙 · 기준 측점) — 모든 종류에서 숨김(52-12 · 정의 화면 = 이름만) */
|
||||
const OLD_EDITOR = false;
|
||||
|
||||
export interface DefScreenOptions {
|
||||
/** 시스템 관리자 — 아니면 읽기만 */
|
||||
isAdmin: boolean;
|
||||
@@ -184,7 +187,7 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
|
||||
}
|
||||
const edit = canEdit();
|
||||
const extra = (): NonNullable<StructDef["종류"][string]> => (d.종류[type.type_id] ??= {});
|
||||
// 명세가 있는 종류(PLAN 52) = 이름만 고침 — 옛 편집기(옵션 · 배치 · 그룹 · 규칙 · 기준 측점)는 숨김
|
||||
// 이름만 고침(PLAN 52-11 · 52-12) — 명세 종류 = 명세 나무 · 나머지 = 정의 그룹 나무
|
||||
const spec = STRUCTURE_SPECS[type.type_id];
|
||||
detail.replaceChildren(
|
||||
el("div", {
|
||||
@@ -200,7 +203,7 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
|
||||
edit,
|
||||
attr: "data-sod-kind-off",
|
||||
}),
|
||||
...(spec
|
||||
...(!OLD_EDITOR
|
||||
? []
|
||||
: [
|
||||
checkBox(
|
||||
@@ -221,7 +224,7 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
|
||||
}),
|
||||
],
|
||||
}),
|
||||
...(spec
|
||||
...(!OLD_EDITOR
|
||||
? [buildSpecBlock({ def: d, type, spec, edit, changed })]
|
||||
: [
|
||||
buildOptionsBlock({ def: d, type, edit, open, changed }),
|
||||
|
||||
@@ -3,14 +3,23 @@
|
||||
* 구조물 명세가 있는 종류(PLAN 52 · `STRUCTURE_SPECS`)의 이름 고치기 칸(52-11) — 나무 꼴
|
||||
* 컨테이너 제목 › 항목 이름 › 값 이름 · 이름만 고침 · 키 · 필수/선택 · 꼴은 읽기만 ·
|
||||
* 아래 미리보기 = B05 · B06 이 그리는 같은 명세 폼(`createSpecForm`)에 고친 이름을 얹음.
|
||||
* 명세 없는 종류(정의 그룹 꼴 · 52-12) = 그룹 제목 › 칸 이름 › 선택지 이름 · 미리보기 = B05 칸 부품
|
||||
* (`field` · `optionControl` · 그룹 상자)으로 같은 차례.
|
||||
* 구조물군 · 종류 이름은 왼쪽 목록 · 머리 칸이 그대로 고침.
|
||||
* ========================================================================== */
|
||||
|
||||
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 { group } from "../../B05_Profile/B05_Profile_UI_Drainage_Facility_Fields";
|
||||
import { field, optionControl } from "../../B05_Profile/B05_Profile_UI_Structures_Fields";
|
||||
import type { StructDef, TypeDef } from "./M02_SO_Def_Model";
|
||||
import { setSpecName, specNameRows, type SpecNameRow } from "./M02_SO_Def_Spec_Model";
|
||||
import {
|
||||
groupNameRows,
|
||||
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";
|
||||
|
||||
@@ -20,47 +29,90 @@ const TAG: Record<NonNullable<SpecNameRow["tag"]>, OptionsTextKey> = {
|
||||
select: "FormSelect",
|
||||
number: "FormNumber",
|
||||
auto: "FormAuto",
|
||||
text: "FormText",
|
||||
bool: "FormBool",
|
||||
};
|
||||
|
||||
/** 정의 그룹 꼴 미리보기 — 그룹 밖 칸 · 그룹 상자(제목 = 그룹 이름) · 선택지 = 이름 표 글(값 그대로). */
|
||||
function groupPreview(def: StructDef, type: TypeDef): HTMLElement {
|
||||
const names = def.종류[type.type_id]?.이름 ?? {};
|
||||
const options = type.options.filter((o) => o.enabled !== false);
|
||||
const cell = (o: (typeof options)[number]): HTMLElement =>
|
||||
field(
|
||||
o.unit ? `${o.label} (${o.unit})` : o.label,
|
||||
optionControl(
|
||||
{
|
||||
input: o.input === "select" || o.input === "number" ? o.input : "text",
|
||||
choices: o.choices ?? [],
|
||||
default: null,
|
||||
required: !!o.required,
|
||||
},
|
||||
undefined,
|
||||
(c) => names[`${o.key}=${c}`] ?? c,
|
||||
),
|
||||
);
|
||||
const groups = def.그룹[type.type_id] ?? [];
|
||||
const inGroup = new Set(groups.flatMap((g) => g.옵션));
|
||||
const grid = (cells: HTMLElement[]): HTMLElement =>
|
||||
el("div", { className: "b05-structure__grid", children: cells });
|
||||
return el("div", {
|
||||
children: [
|
||||
grid(options.filter((o) => !inGroup.has(o.key)).map(cell)),
|
||||
...groups.map((g) => {
|
||||
const box = group(g.이름);
|
||||
box.body.append(grid(options.filter((o) => g.옵션.includes(o.key)).map(cell)));
|
||||
return box.root;
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
export function buildSpecBlock(opt: {
|
||||
def: StructDef;
|
||||
type: TypeDef;
|
||||
spec: StructureSpec;
|
||||
/** 없으면 정의 그룹 꼴(52-12) */
|
||||
spec: StructureSpec | undefined;
|
||||
edit: boolean;
|
||||
changed: () => void;
|
||||
}): HTMLElement {
|
||||
const { def, type, spec, edit, changed } = opt;
|
||||
const rows = specNameRows(def, spec).map((row) =>
|
||||
const rows = (spec ? specNameRows(def, spec) : groupNameRows(def, type)).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",
|
||||
}),
|
||||
row.nameKey
|
||||
? textBox(row.label, (v) => setSpecName(def, type.type_id, row, v), changed, {
|
||||
edit,
|
||||
attr: "data-sod-spec-name",
|
||||
cls: "m02-sod__name",
|
||||
})
|
||||
: el("span", { text: to("SpecLoose") }),
|
||||
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;
|
||||
let preview: HTMLElement;
|
||||
if (spec) {
|
||||
const form = createSpecForm(null);
|
||||
const groups = Object.fromEntries((def.그룹[type.type_id] ?? []).map((g) => [g.id, g.이름]));
|
||||
form.write(spec, {}, def.종류[type.type_id]?.이름 ?? {}, groups);
|
||||
preview = form.root;
|
||||
} else preview = groupPreview(def, type);
|
||||
preview.inert = true;
|
||||
return el("div", {
|
||||
className: "m02-sod__block",
|
||||
attrs: { "data-sod-spec-block": "" },
|
||||
children: [
|
||||
...head(to("SpecHead"), to("SpecNote")),
|
||||
...rows,
|
||||
...(rows.length ? rows : [el("p", { className: "m02-side__empty", text: to("SpecNone") })]),
|
||||
...head(to("SpecPreview")),
|
||||
el("div", {
|
||||
className: "m02-sod__specpreview",
|
||||
attrs: { "data-sod-spec-preview": "" },
|
||||
children: [preview.root],
|
||||
children: [preview],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -6,25 +6,29 @@
|
||||
* · 이름 자리 = 정의 `종류.{종류}.이름`(키 `{컨테이너}` · `{컨테이너}.{항목}` · `{컨테이너}.{항목}={값}`)
|
||||
* · 필수 컨테이너 제목 = 이름 표에 없으면 정의 그룹(`그룹.{종류}` 의 `id = link`) 이름 — B05 그리기
|
||||
* (`containerTitle`)와 같은 차례로 읽고 그 자리에 씀
|
||||
* · 명세 없는 종류(정의 그룹 꼴 · 52-12) = 그룹 제목(`그룹.이름`) › 칸 이름(옵션 `label`) ›
|
||||
* 선택지 이름(이름 표 키 `{옵션}={값}` · 값은 그대로) — 그룹 밖 칸은 맨 앞 머리 줄 아래
|
||||
* import 는 꼴만 — 시험이 이 파일만 옮겨 돌림.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { StructureSpec } from "../../A00_Common/design/design_structure_spec";
|
||||
import type { StructDef } from "./M02_SO_Def_Model";
|
||||
import type { StructDef, TypeDef } 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;
|
||||
tag: "required" | "optional" | "select" | "number" | "auto" | "text" | "bool" | null;
|
||||
/** 필수 컨테이너 제목이 정의 그룹 이름에서 옴 — 고치면 그 그룹 이름에 씀 */
|
||||
group: string | null;
|
||||
/** 정의 옵션 이름(`label`)에서 옴 — 고치면 그 옵션 이름에 씀 */
|
||||
option: string | null;
|
||||
}
|
||||
|
||||
/** 명세 차례대로 이름 줄 — 같은 이름 키(갈래마다 겹친 값)는 한 줄. */
|
||||
@@ -48,6 +52,7 @@ export function specNameRows(def: StructDef, spec: StructureSpec): SpecNameRow[]
|
||||
label: names[c.id] ?? group?.이름 ?? c.title,
|
||||
tag: c.required ? "required" : "optional",
|
||||
group: group ? group.id : null,
|
||||
option: null,
|
||||
});
|
||||
for (const item of c.items) {
|
||||
const itemKey = `${c.id}.${item.id}`;
|
||||
@@ -58,6 +63,7 @@ export function specNameRows(def: StructDef, spec: StructureSpec): SpecNameRow[]
|
||||
label: names[itemKey] ?? item.label,
|
||||
tag: item.kind,
|
||||
group: null,
|
||||
option: null,
|
||||
});
|
||||
const choices = [
|
||||
...(item.choices ?? []),
|
||||
@@ -72,6 +78,7 @@ export function specNameRows(def: StructDef, spec: StructureSpec): SpecNameRow[]
|
||||
label: names[choiceKey] ?? ch.label,
|
||||
tag: null,
|
||||
group: null,
|
||||
option: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -79,8 +86,67 @@ export function specNameRows(def: StructDef, spec: StructureSpec): SpecNameRow[]
|
||||
return rows;
|
||||
}
|
||||
|
||||
/** 이름 하나를 사본에 씀 — 그룹에서 온 제목은 그 그룹 이름 · 나머지는 이름 표. */
|
||||
/** 정의 그룹 꼴 종류(명세 없음) 이름 줄 — 그룹 밖 칸 · 그룹마다 제목 › 칸 › 선택지(B05 그리기 차례). */
|
||||
export function groupNameRows(def: StructDef, type: TypeDef): SpecNameRow[] {
|
||||
const names = def.종류[type.type_id]?.이름 ?? {};
|
||||
const options = type.options.filter((o) => o.enabled !== false);
|
||||
const groups = def.그룹[type.type_id] ?? [];
|
||||
const inGroup = new Set(groups.flatMap((g) => g.옵션));
|
||||
const rows: SpecNameRow[] = [];
|
||||
const optionRows = (keys: readonly string[]): void => {
|
||||
for (const o of options.filter((x) => keys.includes(x.key))) {
|
||||
rows.push({
|
||||
level: 1,
|
||||
nameKey: o.key,
|
||||
key: o.key,
|
||||
label: o.label,
|
||||
tag: o.input,
|
||||
group: null,
|
||||
option: o.key,
|
||||
});
|
||||
for (const ch of o.input === "select" ? (o.choices ?? []) : []) {
|
||||
const nameKey = `${o.key}=${ch}`;
|
||||
rows.push({
|
||||
level: 2,
|
||||
nameKey,
|
||||
key: ch,
|
||||
label: names[nameKey] ?? ch,
|
||||
tag: null,
|
||||
group: null,
|
||||
option: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
const loose = options.filter((o) => !inGroup.has(o.key)).map((o) => o.key);
|
||||
if (loose.length) {
|
||||
rows.push({ level: 0, nameKey: "", key: "", label: "", tag: null, group: null, option: null });
|
||||
optionRows(loose);
|
||||
}
|
||||
for (const g of groups) {
|
||||
rows.push({
|
||||
level: 0,
|
||||
nameKey: `#${g.id}`,
|
||||
key: g.id,
|
||||
label: g.이름,
|
||||
tag: null,
|
||||
group: g.id,
|
||||
option: null,
|
||||
});
|
||||
optionRows(g.옵션);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/** 이름 하나를 사본에 씀 — 그룹에서 온 제목은 그 그룹 이름 · 옵션 칸은 옵션 이름 · 나머지는 이름 표. */
|
||||
export function setSpecName(def: StructDef, typeId: string, row: SpecNameRow, value: string): void {
|
||||
const option = row.option
|
||||
? def.types.find((t) => t.type_id === typeId)?.options.find((o) => o.key === row.option)
|
||||
: undefined;
|
||||
if (option) {
|
||||
option.label = value;
|
||||
return;
|
||||
}
|
||||
const group = row.group ? def.그룹[typeId]?.find((g) => g.id === row.group) : undefined;
|
||||
if (group) {
|
||||
group.이름 = value;
|
||||
|
||||
@@ -48,9 +48,11 @@ const TEXT = {
|
||||
FormAuto: ["자동", "Auto"],
|
||||
SpecHead: ["구조물 명세 이름", "Structure spec names"],
|
||||
SpecNote: [
|
||||
"컨테이너 제목 › 항목 이름 › 값 이름만 고침 — 키(작은 글) · 필수/선택 · 꼴 · 구조는 구조물 명세가 정함 · [저장]하면 B05 · B06 이 이 이름으로 그림",
|
||||
"Rename containers › items › values only — keys, required/optional, form and structure come from the spec",
|
||||
"제목 › 칸 이름 › 선택지 이름만 고침 — 키(작은 글) · 필수/선택 · 꼴 · 칸 더하기 · 빼기는 읽기만 · [저장]하면 B05 · B06 이 이 이름으로 그림",
|
||||
"Rename titles › fields › choices only — keys, required/optional, form and adding/removing fields are read-only",
|
||||
],
|
||||
SpecLoose: ["그룹 밖 칸", "Fields outside groups"],
|
||||
SpecNone: ["고칠 이름 없음 — 칸이 없는 종류", "No names to edit — this kind has no fields"],
|
||||
SpecRequired: ["필수", "Required"],
|
||||
SpecOptional: ["선택", "Optional"],
|
||||
SpecPreview: ["미리보기 — B05 · B06 폼", "Preview — B05 · B06 form"],
|
||||
|
||||
@@ -2646,6 +2646,36 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type_id": "evac_road",
|
||||
"group": "E",
|
||||
"name": "대피로",
|
||||
"placement": "point",
|
||||
"style": {
|
||||
"color": "#2fa84f",
|
||||
"abbr": "대피로"
|
||||
},
|
||||
"drawing_views": [
|
||||
"plan",
|
||||
"quantity"
|
||||
],
|
||||
"options": [
|
||||
{
|
||||
"key": "detail",
|
||||
"label": "상세",
|
||||
"input": "select",
|
||||
"choices": [
|
||||
"대피소",
|
||||
"정차/작업장",
|
||||
"차돌림곳"
|
||||
],
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "b05",
|
||||
"empty_means": "비우면 상세 미정 — 대피로 자리만 잡음(대피소 · 정차/작업장 · 차돌림곳은 뒤에 고름)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type_id": "refuge",
|
||||
"group": "E",
|
||||
@@ -2745,26 +2775,40 @@
|
||||
"drawing_views": [
|
||||
"plan"
|
||||
],
|
||||
"options": [
|
||||
{
|
||||
"key": "facing",
|
||||
"label": "방향",
|
||||
"input": "select",
|
||||
"choices": [
|
||||
"좌",
|
||||
"우",
|
||||
"양면"
|
||||
],
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
}
|
||||
]
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"type_id": "curb",
|
||||
"group": "E",
|
||||
"name": "경계석",
|
||||
"placement": "point",
|
||||
"style": {
|
||||
"color": "#4fae5c",
|
||||
"abbr": "경계석"
|
||||
},
|
||||
"drawing_views": [
|
||||
"plan"
|
||||
],
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"type_id": "hazard_sign",
|
||||
"group": "E",
|
||||
"name": "위험표시",
|
||||
"placement": "point",
|
||||
"style": {
|
||||
"color": "#5fb83a",
|
||||
"abbr": "위험"
|
||||
},
|
||||
"drawing_views": [
|
||||
"plan"
|
||||
],
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"type_id": "guardrail",
|
||||
"group": "E",
|
||||
"name": "가드레일·경계석·위험표지",
|
||||
"name": "가드레일",
|
||||
"placement": "interval",
|
||||
"style": {
|
||||
"color": "#1f9e46",
|
||||
@@ -2777,17 +2821,35 @@
|
||||
],
|
||||
"options": [
|
||||
{
|
||||
"key": "kind",
|
||||
"label": "종류",
|
||||
"input": "select",
|
||||
"choices": [
|
||||
"가드레일",
|
||||
"경계석",
|
||||
"위험표지"
|
||||
],
|
||||
"key": "form",
|
||||
"label": "형태",
|
||||
"input": "text",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"key": "length_m",
|
||||
"label": "길이",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"empty_means": "비우면 범위를 못 셈 — 길이를 적어야 기준 측점 앞뒤로 구간이 섬"
|
||||
},
|
||||
{
|
||||
"key": "before_m",
|
||||
"label": "기준측점 전",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"empty_means": "비우면 0 — 기준 측점이 곧 시작(길이 = 전 + 후 맞물림)"
|
||||
},
|
||||
{
|
||||
"key": "after_m",
|
||||
"label": "기준측점 후",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"empty_means": "비우면 길이 − 전 으로 셈(길이 = 전 + 후 맞물림)"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -2806,6 +2868,20 @@
|
||||
],
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"type_id": "water_intake",
|
||||
"group": "E",
|
||||
"name": "취수정",
|
||||
"placement": "point",
|
||||
"style": {
|
||||
"color": "#3a9e78",
|
||||
"abbr": "취수정"
|
||||
},
|
||||
"drawing_views": [
|
||||
"plan"
|
||||
],
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"type_id": "barrier_gate",
|
||||
"group": "E",
|
||||
@@ -3335,6 +3411,29 @@
|
||||
"형상": {}
|
||||
}
|
||||
],
|
||||
"evac_road": [
|
||||
{
|
||||
"id": "gevacdetail",
|
||||
"이름": "상세 내용",
|
||||
"옵션": [
|
||||
"detail"
|
||||
],
|
||||
"형상": {}
|
||||
}
|
||||
],
|
||||
"guardrail": [
|
||||
{
|
||||
"id": "gguarddetail",
|
||||
"이름": "상세 내용",
|
||||
"옵션": [
|
||||
"form",
|
||||
"length_m",
|
||||
"before_m",
|
||||
"after_m"
|
||||
],
|
||||
"형상": {}
|
||||
}
|
||||
],
|
||||
"cross_underdrain": [
|
||||
{
|
||||
"id": "gcrossud",
|
||||
|
||||
@@ -6,12 +6,20 @@ const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
// 이웃 TS(`./…`)는 같은 길로 읽음 — 명세 파일이 서로 import 함
|
||||
const cache = {};
|
||||
const load = (...parts) => {
|
||||
const js = ts.transpileModule(fs.readFileSync(path.join(ROOT, ...parts), "utf8"), {
|
||||
const file = path.join(ROOT, ...parts);
|
||||
if (cache[file]) return cache[file].exports;
|
||||
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
new Function("exports", "module", "require", js)(box.exports, box, require);
|
||||
const box = (cache[file] = { exports: {} });
|
||||
const near = (id) =>
|
||||
id.startsWith(".")
|
||||
? load(path.relative(ROOT, path.resolve(path.dirname(file), `${id}.ts`)))
|
||||
: require(id);
|
||||
new Function("exports", "module", "require", js)(box.exports, box, near);
|
||||
return box.exports;
|
||||
};
|
||||
const m = load("M02_MasterTemplete", "StructOptions", "M02_SO_Def_Spec_Model.ts");
|
||||
@@ -61,4 +69,27 @@ m.setSpecName(
|
||||
);
|
||||
out.bareGroup = bare.그룹.pipe.find((g) => g.id === "gnbf9m9").이름;
|
||||
|
||||
// 정의 그룹 꼴(52-12) — 맹암거 = 그룹 제목 › 칸 · 옆도랑 = 그룹 밖 칸 › 선택지
|
||||
const g = real();
|
||||
const ud = g.types.find((t) => t.type_id === "cross_underdrain");
|
||||
const udRows = m.groupNameRows(g, ud);
|
||||
out.udRows = udRows.map((r) => [r.level, r.nameKey, r.label]);
|
||||
m.setSpecName(g, ud.type_id, udRows[0], "맹암거 본체");
|
||||
m.setSpecName(g, ud.type_id, udRows[1], "암거 높이 칸");
|
||||
out.udGroup = g.그룹.cross_underdrain[0].이름;
|
||||
out.udLabel = ud.options[0].label;
|
||||
const side = g.types.find((t) => t.type_id === "ditch_side");
|
||||
const sideRows = m.groupNameRows(g, side);
|
||||
out.sideHead = sideRows[0];
|
||||
const lshape = sideRows.find((r) => r.nameKey === "form=L형");
|
||||
out.sideChoice = lshape && [lshape.level, lshape.label];
|
||||
m.setSpecName(g, side.type_id, lshape, "L 모양");
|
||||
out.sideName = g.종류.ditch_side.이름["form=L형"];
|
||||
out.sideChoices = side.options.find((o) => o.key === "form").choices;
|
||||
out.sideReread = m.groupNameRows(g, side).find((r) => r.nameKey === "form=L형").label;
|
||||
out.bareKind = m.groupNameRows(
|
||||
g,
|
||||
g.types.find((t) => t.type_id === "open_ditch"),
|
||||
).length;
|
||||
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""안전부대용지(표 07) — PLAN 52-7.
|
||||
|
||||
정의(대피로 · 가드레일 컨테이너 · 점형 일곱) · 옛 대피 종류 읽기(`asRoad` · `pickable`) ·
|
||||
화면은 ORCA `ui_checks/52-7_안전부대용지.js`.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map
|
||||
from M02_MasterTemplete.StructOptions import M02_SO_Definition as definition
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(os.path.dirname(HERE))
|
||||
POINTS = [
|
||||
"mirror",
|
||||
"curb",
|
||||
"hazard_sign",
|
||||
"position_sign",
|
||||
"water_intake",
|
||||
"completion_sign",
|
||||
"barrier_gate",
|
||||
]
|
||||
|
||||
|
||||
def test_정의_검사_통과():
|
||||
assert definition.check(definition.read()) == []
|
||||
|
||||
|
||||
def test_컨테이너_없는_일곱은_점형_옵션_없음():
|
||||
types = structure_type_map()
|
||||
for tid in POINTS:
|
||||
t = types[tid]
|
||||
assert (t.group, t.placement, t.options) == ("E", "point", []), tid
|
||||
assert [types[t].name for t in ("curb", "hazard_sign", "water_intake")] == [
|
||||
"경계석",
|
||||
"위험표시",
|
||||
"취수정",
|
||||
]
|
||||
|
||||
|
||||
def test_대피로_가드레일_상세_내용_컨테이너():
|
||||
doc = definition.read()
|
||||
groups = doc["그룹"]
|
||||
assert groups["evac_road"] == [
|
||||
{"id": "gevacdetail", "이름": "상세 내용", "옵션": ["detail"], "형상": {}}
|
||||
]
|
||||
assert groups["guardrail"][0]["옵션"] == ["form", "length_m", "before_m", "after_m"]
|
||||
types = structure_type_map()
|
||||
road = types["evac_road"]
|
||||
assert road.placement == "point"
|
||||
assert road.options[0].choices == ["대피소", "정차/작업장", "차돌림곳"]
|
||||
guard = types["guardrail"]
|
||||
assert guard.name == "가드레일" and guard.placement == "interval"
|
||||
assert [(o.key, o.input) for o in guard.options] == [
|
||||
("form", "text"),
|
||||
("length_m", "number"),
|
||||
("before_m", "number"),
|
||||
("after_m", "number"),
|
||||
]
|
||||
# 옛 대피 종류는 정의에 그대로(옛 기록 도면 · 집계)
|
||||
assert {"refuge", "work_yard", "turnaround"} <= set(types)
|
||||
|
||||
|
||||
NODE = r"""
|
||||
const fs = require("fs"), path = require("path");
|
||||
const ts = require(path.join(process.argv[1], "config", "node_modules", "typescript"));
|
||||
const src = fs.readFileSync(
|
||||
path.join(process.argv[1], "B05_Profile", "B05_Profile_UI_Structures_Define.ts"), "utf8");
|
||||
const js = ts.transpileModule(src, {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS } }).outputText;
|
||||
const m = { exports: {} };
|
||||
new Function("exports", "module", js)(m.exports, m);
|
||||
const d = m.exports;
|
||||
process.stdout.write(JSON.stringify({
|
||||
old: d.asRoad({ type_id: "turnaround", options: { width_m: 10 } }),
|
||||
other: d.asRoad({ type_id: "mirror", options: { facing: "좌" } }),
|
||||
pick: ["refuge", "work_yard", "turnaround", "evac_road", "guardrail"].map(
|
||||
(type_id) => d.pickable({ type_id })),
|
||||
}));
|
||||
"""
|
||||
|
||||
|
||||
def test_옛_대피_종류는_대피로_상세로_읽고_목록에서_뺌():
|
||||
proc = subprocess.run(
|
||||
["node", "-e", NODE, ROOT],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
out = json.loads(proc.stdout)
|
||||
assert out["old"] == {
|
||||
"type_id": "evac_road",
|
||||
"options": {"detail": "차돌림곳", "width_m": 10},
|
||||
}
|
||||
assert out["other"] == {"type_id": "mirror", "options": {"facing": "좌"}}
|
||||
assert out["pick"] == [False, False, False, True, True]
|
||||
@@ -74,7 +74,7 @@ def test_정의_덧칸_얹기(out):
|
||||
|
||||
|
||||
def test_판1_정의는_지금_화면과_같음(out):
|
||||
assert out["v1Types"] == 38 # 52-6 횡단배수 맹암거 더함
|
||||
assert out["v1Types"] == 42 # 52-6 맹암거 · 52-7 대피로 · 경계석 · 위험표시 · 취수정
|
||||
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군 3(노출형·개거·맹암거) + C군 5 + D군 3 + E군 10 + G군 1(콘크리트 포장) + 기타 1.
|
||||
"""잔존 B05 선택지 = A군 3(노출형·개거·맹암거) + C군 5 + D군 3 + E군 14 + G군 1(콘크리트 포장) + 기타 1.
|
||||
|
||||
콘크리트 포장은 2026-08-28 사용자 지시로 되살렸다 — 포장 구간을 기준측점+길이+전/후로
|
||||
받는 정본이 됐다(종단경사 자동 포장 적용을 대체).
|
||||
@@ -201,8 +201,10 @@ def test_all_enabled_non_managed_types_are_quick_addable():
|
||||
소단 규격은 법령·교본마다 갈려 프로그램이 판정하지 않고 **사용자가 구간에 놓는다** —
|
||||
그래서 C군 구간형 시설과 같은 자리에 선다. (측구쪽 `ditch_berm` 은 우클릭 추가 대상이
|
||||
아니라 여기 수에 안 잡힌다.)
|
||||
|
||||
E군이 10에서 14로 늘었다 — 표 07(PLAN 52-7) 대피로 · 경계석 · 위험표시 · 취수정.
|
||||
"""
|
||||
assert len(_quick_add_types()) == 24
|
||||
assert len(_quick_add_types()) == 28
|
||||
|
||||
|
||||
def test_removed_types_are_absent():
|
||||
|
||||
@@ -8,6 +8,8 @@ pytest 는 그 결과(JSON)를 판정.
|
||||
- 필수 컨테이너 제목은 정의 그룹 이름에 씀 · 항목 · 값 · 선택 컨테이너는 이름 표에 씀.
|
||||
- 이름만 바뀜 — types(옵션 · 키) 그대로 · 이름 표에 없는 키를 새로 만들지 않음.
|
||||
- 이름 표가 빈 종류는 명세 글로 읽음.
|
||||
- 정의 그룹 꼴(52-12): 그룹 제목 › 칸 › 선택지 · 제목 = 그룹 이름 · 칸 = 옵션 이름 ·
|
||||
선택지 = 이름 표(값 그대로) · 칸 없는 종류는 줄 없음.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -56,3 +58,17 @@ def test_구조는_그대로(r) -> None:
|
||||
def test_이름_표_없는_종류(r) -> None:
|
||||
assert r["bareLabel"] == "흙막이"
|
||||
assert r["bareGroup"] == "유입"
|
||||
|
||||
|
||||
def test_정의_그룹_꼴(r) -> None:
|
||||
assert r["udRows"] == [
|
||||
[0, "#gcrossud", "맹암거"],
|
||||
[1, "culvert_height_m", "암거 높이"],
|
||||
[1, "culvert_slope_pct", "암거 경사"],
|
||||
]
|
||||
assert r["udGroup"] == "맹암거 본체" and r["udLabel"] == "암거 높이 칸"
|
||||
assert r["sideHead"]["nameKey"] == "" and r["sideHead"]["level"] == 0
|
||||
assert r["sideChoice"] == [2, "L형"]
|
||||
assert r["sideName"] == "L 모양" and r["sideReread"] == "L 모양"
|
||||
assert "L형" in r["sideChoices"]
|
||||
assert r["bareKind"] == 0
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
못박는 것
|
||||
- 판 1 = 옛 레지스트리 그대로 옮김(37종 · 옵션 235 · 키 그대로) · 검사 통과.
|
||||
- 읽기 = 로그인 누구나 · 쓰기 = 시스템 관리자만(403).
|
||||
- 저장마다 판 +1 · 앞 판은 `구조물정의_이력/판_N.json` · 낡은 판 409 · 틀린 정의 422(안 씀).
|
||||
- 저장마다 판 +1(지금 판에서) · 앞 판은 `구조물정의_이력/판_N.json` · 낡은 판 409 · 틀린 정의 422(안 씀).
|
||||
- 검사 = 키 겹침 · 꼴 · 규칙 대상 있음 · 그룹 형상.
|
||||
- 저장한 정의를 `/structure-types` 가 바로 읽음 · 프로젝트 저장에 정의 판 · 없는 키 값 보존.
|
||||
"""
|
||||
@@ -49,12 +49,16 @@ def _client(session: dict) -> TestClient:
|
||||
|
||||
|
||||
def test_판1_은_옛_레지스트리_그대로_검사_통과() -> None:
|
||||
doc = definition.read()
|
||||
doc = definition.read_version(1)
|
||||
assert doc["판"] == 1 and len(doc["types"]) == 37
|
||||
assert sum(len(t["options"]) for t in doc["types"]) == 235
|
||||
assert len(load_structure_types()) == 37
|
||||
assert sum(len(t.options) for t in load_structure_types()) == 235
|
||||
assert definition.check(doc) == []
|
||||
now = definition.read()
|
||||
assert now["판"] >= 1 and definition.check(now) == []
|
||||
assert len(load_structure_types()) == len(now["types"])
|
||||
assert sum(len(t.options) for t in load_structure_types()) == sum(
|
||||
len(t["options"]) for t in now["types"]
|
||||
)
|
||||
assert [g["이름"] for g in doc["그룹"]["pipe"]] == ["관", "관 보호공(유입)", "관 보호공(유출)"]
|
||||
assert {t["group"] for t in doc["types"]} <= set(doc["구조물군"])
|
||||
|
||||
@@ -62,26 +66,28 @@ def test_판1_은_옛_레지스트리_그대로_검사_통과() -> None:
|
||||
def test_읽기는_누구나_쓰기는_시스템_관리자만(copy) -> None:
|
||||
user = _client(USER)
|
||||
got = user.get(URL)
|
||||
assert got.status_code == 200 and got.json()["판"] == 1
|
||||
assert user.put(URL, json={"판": 1, "정의": got.json()}).status_code == 403
|
||||
assert json.loads(copy.read_text("utf-8"))["판"] == 1 # 안 씀
|
||||
v = json.loads(copy.read_text("utf-8"))["판"]
|
||||
assert got.status_code == 200 and got.json()["판"] == v
|
||||
assert user.put(URL, json={"판": v, "정의": got.json()}).status_code == 403
|
||||
assert json.loads(copy.read_text("utf-8"))["판"] == v # 안 씀
|
||||
|
||||
|
||||
def test_저장마다_판_더하기_1_앞_판은_이력_structure_types_가_바로_읽음(copy) -> None:
|
||||
admin = _client(ADMIN)
|
||||
doc = admin.get(URL).json()
|
||||
v = doc["판"]
|
||||
tid = doc["types"][0]["type_id"]
|
||||
doc["types"][0]["name"] = "이름 바꿈"
|
||||
res = admin.put(URL, json={"판": 1, "정의": doc})
|
||||
res = admin.put(URL, json={"판": v, "정의": doc})
|
||||
assert res.status_code == 200, res.text
|
||||
assert res.json()["판"] == 2
|
||||
old = json.loads((copy.parent / "구조물정의_이력" / "판_1.json").read_text("utf-8"))
|
||||
assert old["판"] == 1 and old["types"][0]["name"] != "이름 바꿈"
|
||||
assert res.json()["판"] == v + 1
|
||||
old = json.loads((copy.parent / "구조물정의_이력" / f"판_{v}.json").read_text("utf-8"))
|
||||
assert old["판"] == v and old["types"][0]["name"] != "이름 바꿈"
|
||||
seen = {t["type_id"]: t for t in admin.get(TYPES).json()["types"]}
|
||||
assert seen[tid]["name"] == "이름 바꿈" and len(seen) == 37
|
||||
assert admin.put(URL, json={"판": 1, "정의": doc}).status_code == 409 # 낡은 판
|
||||
assert admin.put(URL, json={"판": 2, "정의": res.json()}).json()["판"] == 3
|
||||
assert (copy.parent / "구조물정의_이력" / "판_2.json").is_file()
|
||||
assert seen[tid]["name"] == "이름 바꿈" and len(seen) == len(doc["types"])
|
||||
assert admin.put(URL, json={"판": v, "정의": doc}).status_code == 409 # 낡은 판
|
||||
assert admin.put(URL, json={"판": v + 1, "정의": res.json()}).json()["판"] == v + 2
|
||||
assert (copy.parent / "구조물정의_이력" / f"판_{v + 1}.json").is_file()
|
||||
|
||||
|
||||
def _rule(doc: dict) -> tuple[str, dict, dict]:
|
||||
@@ -96,6 +102,7 @@ def _rule(doc: dict) -> tuple[str, dict, dict]:
|
||||
def test_검사_키_겹침_꼴_규칙_대상_형상(copy) -> None:
|
||||
admin = _client(ADMIN)
|
||||
base = admin.get(URL).json()
|
||||
v = base["판"]
|
||||
tid, up, down = _rule(base)
|
||||
good = {
|
||||
"종류": tid,
|
||||
@@ -133,13 +140,13 @@ def test_검사_키_겹침_꼴_규칙_대상_형상(copy) -> None:
|
||||
bad(모르는칸=1),
|
||||
]
|
||||
for i, doc in enumerate(cases):
|
||||
res = admin.put(URL, json={"판": 1, "정의": doc})
|
||||
res = admin.put(URL, json={"판": v, "정의": doc})
|
||||
assert res.status_code == 422, (i, res.text)
|
||||
assert json.loads(copy.read_text("utf-8"))["판"] == 1 # 거절되면 안 씀
|
||||
assert json.loads(copy.read_text("utf-8"))["판"] == v # 거절되면 안 씀
|
||||
assert not (copy.parent / "구조물정의_이력").exists()
|
||||
shaped = bad(종류={tid: {"기준측점": False}})
|
||||
shaped["그룹"]["pipe"][0]["형상"] = {"길이": {"받기": True, "뜻": "관 길이"}}
|
||||
assert admin.put(URL, json={"판": 1, "정의": shaped}).status_code == 200
|
||||
assert admin.put(URL, json={"판": v, "정의": shaped}).status_code == 200
|
||||
|
||||
|
||||
def test_프로젝트_저장에_정의_판_없는_키_값은_남김(copy, tmp_path) -> None:
|
||||
@@ -154,5 +161,5 @@ def test_프로젝트_저장에_정의_판_없는_키_값은_남김(copy, tmp_pa
|
||||
)
|
||||
assert save_structures(root, [item], base_revision=0) == 1
|
||||
raw = json.loads(Path(root, "B05_Profile", "route", "structures.json").read_text("utf-8"))
|
||||
assert raw["definition_version"] == 1
|
||||
assert raw["definition_version"] == definition.read()["판"]
|
||||
assert load_structures(root)[1][0].options["지난정의키"] == "옛값"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* 52-11 — M02 「구조물 옵션 정의」 = 구조물 명세 이름 고치기(명세 있는 종류) · B05 가 그 이름으로 그림
|
||||
* 52-12 — 정의 그룹 꼴 종류(맹암거 · 개거 · 바닥막이 …)도 같은 이름 나무 · 옛 편집기는 모든 종류에서 숨김
|
||||
* 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m5211 = "단계";` 를 붙여
|
||||
* M02 마스터 층(#/m02-master-template?root=master) · 시스템 관리자:
|
||||
* orca eval --expression "window.__m5211='m02'; $(cat 이 파일)"
|
||||
@@ -7,11 +8,14 @@
|
||||
* 쓰는 것: 없음 — M02 는 화면 사본만 고치고 [저장] 안 누름 · 되돌린 뒤 판 그대로를 GET 으로 봄 ·
|
||||
* B05 는 받아 둔 종류 객체(메모리)만 고침 · 끝나면 orca reload 로 사본을 버림
|
||||
* 기대 수치(m02):
|
||||
* - spec = 배관 나무 줄(컨테이너 5) · old = 옛 편집기 0(옵션 · 배치 · 그룹 · 규칙 · 기준 측점) · other = 명세 없는 종류는 옛 편집기
|
||||
* - spec = 배관 나무 줄(컨테이너 5) · old = 옛 편집기 0(옵션 · 배치 · 그룹 · 규칙 · 기준 측점) · other = 명세 없는 종류도 이름 나무
|
||||
* - 맹암거 = 그룹 제목 › 칸 2 · 고치면 미리보기 그룹 상자 제목 · 칸 이름 따라감
|
||||
* - 바닥막이 = 선택지 이름 고치면 미리보기 보기 글 따라감(값 그대로) · 개거 = 고칠 이름 없음 · 종류 이름만(왼쪽 목록 따라감)
|
||||
* - 이름 칸만 고침: inputs = 줄 수 · 키 · 꼴은 글(입력 칸 아님)
|
||||
* - 컨테이너 제목 · 항목 이름 · 값 이름 고침 → 미리보기 legend · 칸 이름 · 선택지 글 따라감 · [저장] 켜짐
|
||||
* - 빈 이름 → issues 「명세 이름」 · [저장] 꺼짐 · 되돌리면 이름 그대로 · 판 그대로
|
||||
* 기대 수치(b05): 사본 이름(컨테이너 · 항목 · 값)이 배관 폼에 그대로 → named.* = true
|
||||
* group.* = 맹암거 그룹 제목 · 칸 이름 · 바닥막이 선택지 글이 새 구조물 폼에 그대로(값 그대로)
|
||||
*/
|
||||
(async () => {
|
||||
const MODE = window.__m5211 || "m02";
|
||||
@@ -44,10 +48,74 @@
|
||||
row.click();
|
||||
if (!(await until(() => $$("[data-sod-kind]").length)))
|
||||
return JSON.stringify({ error: "종류 목록 없음" });
|
||||
// 명세 없는 종류 — 옛 편집기 그대로
|
||||
// 명세 없는 종류 — 옛 편집기 숨김 · 이름 나무(52-12)
|
||||
$('[data-sod-kind="retaining_wall"]').click();
|
||||
await W(200);
|
||||
out.other = { base: !!$("[data-sod-base]"), spec: !!$("[data-sod-spec-block]") };
|
||||
out.other = {
|
||||
base: !!$("[data-sod-base]"),
|
||||
spec: !!$("[data-sod-spec-block]"),
|
||||
blocks: $("[data-sod-detail]").children.length,
|
||||
};
|
||||
const nameBox = (k) => $(`[data-sod-spec="${k}"] [data-sod-spec-name]`);
|
||||
const groupPreview = () => $("[data-sod-spec-preview]");
|
||||
// 맹암거 — 그룹 제목 › 칸
|
||||
$('[data-sod-kind="cross_underdrain"]').click();
|
||||
await W(200);
|
||||
{
|
||||
const keys = $$("[data-sod-spec]").map((r) => r.dataset.sodSpec);
|
||||
const g0 = nameBox("#gcrossud").value;
|
||||
const o0 = nameBox("culvert_height_m").value;
|
||||
put(nameBox("#gcrossud"), "맹암거X");
|
||||
await W(150);
|
||||
put(nameBox("culvert_height_m"), "암거 높이X");
|
||||
await W(150);
|
||||
const pv = groupPreview();
|
||||
out.ud = {
|
||||
keys,
|
||||
legend: [...pv.querySelectorAll("legend")].some((l) => l.textContent === "맹암거X"),
|
||||
label: pv.textContent.includes("암거 높이X (m)"),
|
||||
old: !!$("[data-sod-option]") || !!$("[data-sod-base]"),
|
||||
};
|
||||
put(nameBox("#gcrossud"), g0);
|
||||
await W(150);
|
||||
put(nameBox("culvert_height_m"), o0);
|
||||
await W(150);
|
||||
out.ud.restored =
|
||||
nameBox("#gcrossud").value === g0 && nameBox("culvert_height_m").value === o0;
|
||||
}
|
||||
// 바닥막이 — 선택지 이름(값 그대로)
|
||||
$('[data-sod-kind="bed_sill"]').click();
|
||||
await W(200);
|
||||
{
|
||||
const k = "form=돌붙임(메)";
|
||||
const c0 = nameBox(k).value;
|
||||
put(nameBox(k), "돌붙임 메X");
|
||||
await W(150);
|
||||
const opt = [...groupPreview().querySelectorAll("option")].find(
|
||||
(o) => o.textContent === "돌붙임 메X",
|
||||
);
|
||||
out.bed = { choice: !!opt, value: opt?.value };
|
||||
put(nameBox(k), c0);
|
||||
await W(150);
|
||||
out.bed.restored = nameBox(k).value === c0;
|
||||
}
|
||||
// 개거 — 칸 없음 · 종류 이름만
|
||||
$('[data-sod-kind="open_ditch"]').click();
|
||||
await W(200);
|
||||
{
|
||||
const kind = $("[data-sod-kind-name]");
|
||||
const n0 = kind.value;
|
||||
out.ditch = {
|
||||
rows: $$("[data-sod-spec]").length,
|
||||
none: !!$("[data-sod-spec-block] p.m02-side__empty"),
|
||||
};
|
||||
put(kind, "개거X");
|
||||
await W(200);
|
||||
out.ditch.list = $('[data-sod-kind="open_ditch"]').textContent.includes("개거X");
|
||||
put($("[data-sod-kind-name]"), n0);
|
||||
await W(200);
|
||||
out.ditch.restored = $('[data-sod-kind="open_ditch"]').textContent.includes(n0);
|
||||
}
|
||||
// 배관 — 명세 이름 나무
|
||||
$('[data-sod-kind="pipe"]').click();
|
||||
await W(200);
|
||||
@@ -102,8 +170,21 @@
|
||||
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.other.base &&
|
||||
out.other.spec &&
|
||||
out.other.blocks === 2 &&
|
||||
out.ud.keys.join() === "#gcrossud,culvert_height_m,culvert_slope_pct" &&
|
||||
out.ud.legend &&
|
||||
out.ud.label &&
|
||||
!out.ud.old &&
|
||||
out.ud.restored &&
|
||||
out.bed.choice &&
|
||||
out.bed.value === "돌붙임(메)" &&
|
||||
out.bed.restored &&
|
||||
out.ditch.rows === 0 &&
|
||||
out.ditch.none &&
|
||||
out.ditch.list &&
|
||||
out.ditch.restored &&
|
||||
out.spec.containers === 5 &&
|
||||
out.spec.inputs === out.spec.rows &&
|
||||
out.spec.nameInputs === out.spec.rows &&
|
||||
@@ -149,7 +230,41 @@
|
||||
),
|
||||
};
|
||||
(li.querySelector("button") ?? li).click(); // 재선택 = 해제
|
||||
out.ok = Object.values(out.named).every(Boolean);
|
||||
await W(500);
|
||||
// 정의 그룹 꼴(52-12) — 받아 둔 종류 객체만 고치고 새 구조물 폼(군 · 종류 고르기 · 놓지 않음)으로 봄
|
||||
const types = await api.fetchStructureTypes(true);
|
||||
const ud = types.find((t) => t.type_id === "cross_underdrain");
|
||||
ud.groups[0].name = "맹암거X";
|
||||
ud.options[0].label = "암거 높이X";
|
||||
const bed = types.find((t) => t.type_id === "bed_sill");
|
||||
bed.spec_names = { ...(bed.spec_names ?? {}), "form=돌붙임(메)": "돌붙임 메X" };
|
||||
const count = $$(".b05-route__irregular-list li").length;
|
||||
const [famSel, kindSel] = $$("select", sec);
|
||||
const choose = async (fam, kind) => {
|
||||
famSel.value = fam;
|
||||
famSel.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await W(300);
|
||||
kindSel.value = kind;
|
||||
kindSel.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await W(500);
|
||||
};
|
||||
await choose("A", "cross_underdrain");
|
||||
out.group = {
|
||||
legend: $$("legend", sec).some(
|
||||
(l) => l.offsetParent !== null && l.textContent.includes("맹암거X"),
|
||||
),
|
||||
label: $$(".b05-route__field > span", sec).some(
|
||||
(n) => n.offsetParent !== null && n.textContent === "암거 높이X (m)",
|
||||
),
|
||||
};
|
||||
await choose("D", "bed_sill");
|
||||
const opt = $$("option", sec).find((o) => o.textContent === "돌붙임 메X");
|
||||
out.group.choice = !!opt && opt.value === "돌붙임(메)";
|
||||
famSel.value = "";
|
||||
famSel.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await W(300);
|
||||
out.group.noWrite = $$(".b05-route__irregular-list li").length === count;
|
||||
out.ok = Object.values(out.named).every(Boolean) && Object.values(out.group).every(Boolean);
|
||||
}
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
/* 52-7 — B05 · B06 좌측 「구조물 배치」 안전부대용지 = 사용자 구조물 표 07
|
||||
* 시작 화면: B05 종단설계(#/b05-profile) · 검증 프로젝트(랩탑_보조)
|
||||
* 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m527 = "단계";` 를 붙여
|
||||
* seed → 원래 구조물을 sessionStorage 에 두고 옛 「대피소」 한 건(refuge · 너비 4 · 유효길이 15)을 더해 PUT
|
||||
* (orca reload)
|
||||
* edit → 종류 목록 · 일곱 종류 컨테이너 0 / 잇기 1 · 대피로 상세 3 · 옛 대피소 = 대피로 + 대피소 ·
|
||||
* 가드레일 길이 10 전 3 → 후 7 · 가드레일 · 대피로 [추가] → [저장]
|
||||
* (orca reload)
|
||||
* verify → 다시 연 값 · 옛 대피소 기록 그대로 · B05 = B06 같은 그리기
|
||||
* restore → 원래 구조물로 되돌림(PUT) · 초안 비움
|
||||
* 쓰는 것: 검증 프로젝트 structures.json(되돌림 단계가 원래 목록으로 다시 씀)
|
||||
*/
|
||||
(async () => {
|
||||
const MODE = window.__m527 || "edit";
|
||||
const w = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const until = async (fn, ms = 20000) => {
|
||||
for (let t = 0; t < ms; t += 200) {
|
||||
if (fn()) return true;
|
||||
await w(200);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const projectId = localStorage.getItem("frd_current_project_id");
|
||||
const modUrl = (part) =>
|
||||
performance
|
||||
.getEntriesByType("resource")
|
||||
.map((e) => e.name)
|
||||
.filter((n) => n.includes(part))
|
||||
.pop();
|
||||
const api = await import(modUrl("/B05_Profile/B05_Profile_Api_Structures.ts"));
|
||||
const section = () => document.querySelector(".b05-structure-section");
|
||||
const fieldOf = (label) =>
|
||||
[...section().querySelectorAll(".b05-route__field")].find(
|
||||
(f) => f.querySelector("span")?.textContent.trim() === label,
|
||||
);
|
||||
const groupSelect = () => fieldOf("구조물군").querySelector("select");
|
||||
const typeSelect = () => fieldOf("종류").querySelector("select");
|
||||
const choose = async (el, value) => {
|
||||
el.value = value;
|
||||
el.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await w(300);
|
||||
};
|
||||
const pickType = async (typeId) => {
|
||||
await choose(groupSelect(), "E");
|
||||
await choose(typeSelect(), typeId);
|
||||
};
|
||||
const anchor = () => fieldOf("기준 측점")?.querySelectorAll("input");
|
||||
const place = async (station, plus) => {
|
||||
const [s, r] = anchor();
|
||||
s.value = String(station);
|
||||
s.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
r.value = String(plus);
|
||||
r.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await w(400);
|
||||
};
|
||||
// 보이는 칸만 — 안 쓰는 시설 폼에도 같은 키 칸(포장 길이 등)이 숨어 있음
|
||||
const opt = (key) =>
|
||||
[...section().querySelectorAll(`[data-option-keys="${key}"]`)]
|
||||
.find((n) => n.offsetParent !== null)
|
||||
?.querySelector("input, select");
|
||||
const setOpt = async (key, value) => {
|
||||
const el = opt(key);
|
||||
el.value = value;
|
||||
el.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
el.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await w(300);
|
||||
};
|
||||
const shown = (el) => !!el && el.offsetParent !== null;
|
||||
// 종류 단위 잇기 = 기준 측점 아래 줄(컨테이너 밖) · 컨테이너 = 정의 그룹 상자
|
||||
const typeLinks = () =>
|
||||
[...section().querySelectorAll('[data-b05-summary="row"]')].filter(
|
||||
(r) => !r.closest("[data-b05-group]") && shown(r),
|
||||
).length;
|
||||
const containers = () =>
|
||||
[...section().querySelectorAll("[data-b05-group]")].filter(shown).map((box) => ({
|
||||
title: box.querySelector("legend").textContent,
|
||||
keys: [...box.querySelectorAll("[data-option-keys]")].map((n) => n.dataset.optionKeys),
|
||||
links: box.querySelectorAll('[data-b05-summary="row"]').length,
|
||||
}));
|
||||
const listItem = (name) =>
|
||||
[...document.querySelectorAll(".b05-route__irregular-list li")].find((l) =>
|
||||
l.textContent.includes(name),
|
||||
);
|
||||
const openItem = async (name) => {
|
||||
await until(() => listItem(name));
|
||||
const li = listItem(name);
|
||||
(li.querySelector("button") ?? li).click();
|
||||
await w(600);
|
||||
if (!typeSelect().value) {
|
||||
(listItem(name).querySelector("button") ?? listItem(name)).click(); // 재선택 = 해제였으면 다시
|
||||
await w(600);
|
||||
}
|
||||
};
|
||||
const stale = () =>
|
||||
[...section().querySelectorAll(".b05-structure__stale .b05-route__field")].map((f) => [
|
||||
f.querySelector("span").textContent,
|
||||
f.querySelector("input").value,
|
||||
]);
|
||||
const form = () => ({
|
||||
type: typeSelect().selectedOptions[0]?.text,
|
||||
containers: containers(),
|
||||
typeLinks: typeLinks(),
|
||||
values: Object.fromEntries(
|
||||
[...section().querySelectorAll("[data-option-keys]")]
|
||||
.filter(shown)
|
||||
.map((n) => [n.dataset.optionKeys, n.querySelector("input, select").value]),
|
||||
),
|
||||
stale: stale(),
|
||||
});
|
||||
const out = { mode: MODE };
|
||||
|
||||
if (MODE === "seed") {
|
||||
const now = await api.fetchStructures(projectId);
|
||||
sessionStorage.setItem("__527_original", JSON.stringify(now.structures));
|
||||
const legacy = {
|
||||
structure_id: null,
|
||||
type_id: "refuge",
|
||||
placement: "interval",
|
||||
chainage_m: 150,
|
||||
start_m: 145,
|
||||
end_m: 160,
|
||||
options: { width_m: 4, length_m: 15 },
|
||||
memo: "52-7 옛 대피소",
|
||||
placement_source: "manual",
|
||||
status: "draft",
|
||||
revision: 0,
|
||||
geometry: null,
|
||||
};
|
||||
await api.saveStructures(projectId, now.revision, [...now.structures, legacy]);
|
||||
api.writePendingStructures(projectId, null);
|
||||
const saved = await api.fetchStructures(projectId);
|
||||
out.seeded = saved.structures.filter((s) => s.type_id === "refuge").length;
|
||||
out.total = saved.structures.length;
|
||||
}
|
||||
|
||||
if (MODE === "edit") {
|
||||
await until(() => section() && groupSelect().options.length > 1);
|
||||
await choose(groupSelect(), "E");
|
||||
out.picker = [...typeSelect().options].filter((o) => o.value).map((o) => o.text);
|
||||
// 컨테이너 없는 일곱 — 기준 측점 아래 잇기 1 · 컨테이너 0
|
||||
out.seven = {};
|
||||
for (const id of [
|
||||
"mirror",
|
||||
"curb",
|
||||
"hazard_sign",
|
||||
"position_sign",
|
||||
"water_intake",
|
||||
"completion_sign",
|
||||
"barrier_gate",
|
||||
]) {
|
||||
await choose(typeSelect(), id);
|
||||
out.seven[typeSelect().selectedOptions[0].text] = [containers().length, typeLinks()];
|
||||
}
|
||||
// 대피로 — 상세 내용 컨테이너 · 상세 셋
|
||||
await pickType("evac_road");
|
||||
await place(7, 0);
|
||||
out.road = form();
|
||||
out.roadDetails = [...opt("detail").options].filter((o) => o.value).map((o) => o.text);
|
||||
await setOpt("detail", "정차/작업장");
|
||||
section().querySelector(".b05-route__irregular-btn.is-primary").click();
|
||||
await w(600);
|
||||
// 가드레일 — 형태 · 길이 10 · 전 3 → 후 7
|
||||
await pickType("guardrail");
|
||||
await place(5, 0);
|
||||
await setOpt("form", "W빔 가드레일");
|
||||
await setOpt("length_m", "10");
|
||||
await setOpt("before_m", "3");
|
||||
out.guard = form();
|
||||
out.range = section().querySelector(".b05-structure__range-value").textContent;
|
||||
section().querySelector(".b05-route__irregular-btn.is-primary").click();
|
||||
await w(600);
|
||||
// 옛 대피소 기록 = 대피로 + 상세 대피소 · 옛 칸 값은 지난 정의 값
|
||||
await openItem("대피소");
|
||||
out.legacy = form();
|
||||
section().querySelectorAll(".b05-route__irregular-btn")[2].click(); // [리셋] — 안 고치고 닫음
|
||||
await w(300);
|
||||
document.querySelector('[data-dock-action="save"]').click();
|
||||
await w(500);
|
||||
out.saved = await until(() => !document.querySelector(".ui-overlay.is-active"), 60000);
|
||||
const file = await api.fetchStructures(projectId);
|
||||
out.file = file.structures
|
||||
.filter((s) => ["evac_road", "guardrail", "refuge"].includes(s.type_id))
|
||||
.map((s) => [s.type_id, s.placement, s.chainage_m, s.start_m, s.end_m, s.options]);
|
||||
}
|
||||
|
||||
if (MODE === "verify") {
|
||||
await openItem("가드레일");
|
||||
const b05 = form();
|
||||
out.guard = b05;
|
||||
await openItem("대피로");
|
||||
out.road = form();
|
||||
await openItem("대피소");
|
||||
out.legacy = form();
|
||||
location.hash = "#/b06-section";
|
||||
await until(() => location.hash.includes("b06"), 5000);
|
||||
await w(6000);
|
||||
await openItem("가드레일");
|
||||
const b06 = form();
|
||||
out.sameDrawing = JSON.stringify(b05) === JSON.stringify(b06);
|
||||
if (!out.sameDrawing) out.diff = { b05, b06 };
|
||||
location.hash = "#/b05-profile";
|
||||
await w(3000);
|
||||
}
|
||||
|
||||
if (MODE === "restore") {
|
||||
const original = JSON.parse(sessionStorage.getItem("__527_original") || "null");
|
||||
if (!original) return JSON.stringify({ error: "원래 값 없음" });
|
||||
const now = await api.fetchStructures(projectId);
|
||||
await api.saveStructures(projectId, now.revision, original);
|
||||
api.writePendingStructures(projectId, null);
|
||||
sessionStorage.removeItem("__527_original");
|
||||
const after = await api.fetchStructures(projectId);
|
||||
const pick = (list) => JSON.stringify(list.map((s) => [s.type_id, s.chainage_m, s.options]));
|
||||
out.restored = pick(after.structures) === pick(original);
|
||||
out.count = after.structures.length;
|
||||
}
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
Reference in New Issue
Block a user