Merge remote-tracking branch 'origin/sub_laptop_6' into main_laptop_1
This commit is contained in:
@@ -59,6 +59,14 @@ export interface SpecFormHooks {
|
||||
* [추가] · [빼기] 없음 · 줄면 끝에서부터 가림(값은 남음 · 54-10) · null = 모름(저장된 것 그대로) ·
|
||||
* undefined = 손 [추가]. */
|
||||
count?: (c: SpecContainer) => number | null | undefined;
|
||||
/** 그림이 쓰는 값(54-13) — 고르기 칸이 비었거나 자동 규칙일 때 그 값 · 꼬리(「기본」 · 「자동」) ·
|
||||
* null = 없음. 보여 주기만(값 · 저장은 사용자가 고를 때만). `values` = 보이는 값(위 칸 반영). */
|
||||
shown?: (
|
||||
c: SpecContainer,
|
||||
item: SpecItem,
|
||||
values: Record<string, string>,
|
||||
index: number,
|
||||
) => { value: string; tag: string } | null;
|
||||
}
|
||||
|
||||
export interface SpecForm {
|
||||
@@ -105,18 +113,27 @@ export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = {
|
||||
return slot;
|
||||
};
|
||||
|
||||
const control = (c: SpecContainer, item: SpecItem, values: Instance): HTMLElement => {
|
||||
const control = (
|
||||
c: SpecContainer,
|
||||
item: SpecItem,
|
||||
values: Instance,
|
||||
tag?: string,
|
||||
): HTMLElement => {
|
||||
if (item.kind === "select") {
|
||||
const select = document.createElement("select");
|
||||
const choices = choicesOf(item, values);
|
||||
const now = values[item.id] ?? "";
|
||||
const drawn = choices.find((ch) => ch.value === now);
|
||||
// 그림이 쓰는 값은 빈 보기 자리에 꼬리를 달아 보임 — 고른 값과 구별(54-13)
|
||||
const name = drawn ? choiceLabel(c, item, drawn, names) : now;
|
||||
const blank = !tag ? "— 안 정함 —" : name ? `${name} (${tag})` : tag;
|
||||
select.replaceChildren(
|
||||
new Option("— 안 정함 —", ""),
|
||||
new Option(blank, ""),
|
||||
...choices.map((ch) => new Option(choiceLabel(c, item, ch, names), ch.value)),
|
||||
);
|
||||
const now = values[item.id] ?? "";
|
||||
// 선택지 밖 옛 값은 그 값만 남겨 보임(지운 값을 몰래 바꾸지 않음)
|
||||
if (now && !choices.some((ch) => ch.value === now)) select.add(new Option(now, now));
|
||||
select.value = now;
|
||||
if (!tag && now && !drawn) select.add(new Option(now, now));
|
||||
select.value = tag ? "" : now;
|
||||
return select;
|
||||
}
|
||||
if (item.kind === "text") {
|
||||
@@ -139,7 +156,19 @@ export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = {
|
||||
return stepper(input, item.step ?? 0.1);
|
||||
};
|
||||
|
||||
const onEdit = (c: SpecContainer, item: SpecItem, values: Instance, raw: string): void => {
|
||||
const onEdit = (
|
||||
c: SpecContainer,
|
||||
item: SpecItem,
|
||||
values: Instance,
|
||||
raw: string,
|
||||
view: Instance,
|
||||
): void => {
|
||||
// 그림 값을 보이던 위 칸 아래를 고침 = 그 위 값을 고른 것 — 다른 갈래 값은 비움(54-13)
|
||||
const parent = item.by?.item;
|
||||
if (parent && view[parent] !== values[parent]) {
|
||||
values[parent] = view[parent];
|
||||
for (const child of c.items) if (child.by?.item === parent) values[child.id] = "";
|
||||
}
|
||||
values[item.id] = raw;
|
||||
if (item.split) {
|
||||
const order = [item.split, ...(recent.get(values) ?? []).filter((r) => r !== item.split)];
|
||||
@@ -170,15 +199,26 @@ export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = {
|
||||
const title = containerTitle(c, names, groupNames[c.link]);
|
||||
const box = group(c.required ? title : `${title} ${index + 1}`);
|
||||
box.root.dataset.specContainer = c.id;
|
||||
// 보이는 값 = 고른 값 + 그림이 쓰는 값(54-13) · 위 칸이 다른 갈래를 보이면 아래 칸은 빈 채로
|
||||
const view: Instance = { ...values };
|
||||
const tags = new Map<string, string>();
|
||||
for (const item of c.items) {
|
||||
if (item.by && view[item.by.item] !== values[item.by.item]) view[item.id] = "";
|
||||
const drawn = item.kind === "select" ? hooks.shown?.(c, item, view, index) : null;
|
||||
if (!drawn) continue;
|
||||
view[item.id] = drawn.value;
|
||||
tags.set(item.id, drawn.tag);
|
||||
}
|
||||
const fields = c.items
|
||||
.filter((item) => item.kind === "auto" || !item.by || branchOf(item, values))
|
||||
.filter((item) => item.kind === "auto" || !item.by || branchOf(item, view))
|
||||
.map((item) => {
|
||||
const input = control(c, item, values);
|
||||
const input = control(c, item, view, tags.get(item.id));
|
||||
const wrap = labeled(itemLabel(c, item, names), input);
|
||||
wrap.dataset.specItem = `${c.id}.${item.id}`;
|
||||
if (tags.has(item.id)) wrap.dataset.specShown = tags.get(item.id);
|
||||
const target = input.querySelector?.("input, select") ?? input;
|
||||
target.addEventListener("change", () =>
|
||||
onEdit(c, item, values, (target as HTMLInputElement | HTMLSelectElement).value),
|
||||
onEdit(c, item, values, (target as HTMLInputElement | HTMLSelectElement).value, view),
|
||||
);
|
||||
return wrap;
|
||||
});
|
||||
|
||||
@@ -25,6 +25,13 @@ export function setPipeExtraCountSource(source: (section: CrossSection) => numbe
|
||||
extraCountSource = source;
|
||||
}
|
||||
|
||||
/** 유입 형식을 대는 곳 — 횡단 보기의 세션 값 우선(`valueFor`) · 없으면 정본 patch(54-13). */
|
||||
type InletChoice = NonNullable<CrossSection["design"]>["inlet_structure"];
|
||||
let inletSource: ((section: CrossSection) => InletChoice) | null = null;
|
||||
export function setPipeInletSource(source: (section: CrossSection) => InletChoice): void {
|
||||
inletSource = source;
|
||||
}
|
||||
|
||||
/** 그 관의 주인 측점(관 자리에서 가장 가까운 측점) — 횡단 자료가 없으면 null. */
|
||||
function ownerSection(
|
||||
chainageM: number | null,
|
||||
@@ -64,6 +71,34 @@ export function pipeExtraCount(chainageM: number | null): number | null {
|
||||
return computeStoredLayouts(probe, sections)?.culvert?.extraWalls.length ?? outlet;
|
||||
}
|
||||
|
||||
/** 그림이 쓰는 유입 · 유출 값(54-13) — 집수정 모양(없으면 null = 흙막이 벽) · 벽 형태(유입 · 유출 ·
|
||||
* 흙막이 n). 폼 빈 · 자동 칸에 보여 주기만. 횡단 자료가 없으면 null(모름). */
|
||||
export function pipeDrawn(chainageM: number | null): {
|
||||
basin: string | null;
|
||||
forms: Record<string, string | null>;
|
||||
extras: Array<string | null>;
|
||||
} | null {
|
||||
const found = ownerSection(chainageM);
|
||||
const design = found?.owner.design;
|
||||
if (!found || !design) return null;
|
||||
const { owner, sections } = found;
|
||||
const outlet = extraCountSource?.(owner) ?? design.extra_wall_counts?.outlet ?? 0;
|
||||
const counts = { basin: 0, ...design.extra_wall_counts, outlet };
|
||||
const inlet = inletSource?.(owner) ?? design.inlet_structure;
|
||||
const probe = {
|
||||
...owner,
|
||||
design: { ...design, extra_wall_counts: counts, inlet_structure: inlet },
|
||||
};
|
||||
const layout = computeStoredLayouts(probe, sections)?.culvert;
|
||||
if (!layout) return null;
|
||||
const formOf = (role: string) => layout.walls.find((w) => w.role === role)?.form ?? null;
|
||||
return {
|
||||
basin: layout.basin?.shape ?? null,
|
||||
forms: { inlet: formOf("inlet"), outlet: formOf("outlet") },
|
||||
extras: layout.extraWalls.map((w) => w.form),
|
||||
};
|
||||
}
|
||||
|
||||
/** 흙막이 높이 빈 칸 안내 — 관경 기준 최소 높이(엔진이 빈 칸을 이 값으로 그림). */
|
||||
export function guardHeightHint(diameterMm: string): string {
|
||||
const d = Number(diameterMm || "1000") / 1000;
|
||||
|
||||
@@ -76,6 +76,7 @@ import type { DesignPageHost } from "./design_page";
|
||||
import {
|
||||
pipeExtraCount,
|
||||
setPipeExtraCountSource,
|
||||
setPipeInletSource,
|
||||
setPipeSectionsSource,
|
||||
} from "./design_structure_pipe_calc";
|
||||
import {
|
||||
@@ -296,6 +297,8 @@ export async function mountCrossView(
|
||||
});
|
||||
// 배관 흙막이 개수 = 유출 단 수(54-1) — 세션 값 우선
|
||||
setPipeExtraCountSource((section) => stationControls.extraWalls.countFor(section, "outlet"));
|
||||
// 폼이 보이는 유입 그림 값(54-13) — 세션 형식 우선
|
||||
setPipeInletSource((section) => stationControls.inletStructure.valueFor(section));
|
||||
|
||||
const sectionView = createSectionView(
|
||||
(chainageM, change) => {
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
WING_OUT_KEYS,
|
||||
} from "./B05_Profile_UI_Drainage_Facility_Fields";
|
||||
import { createSpecForm, type SpecLinks } from "../A00_Common/design/design_structure_form";
|
||||
import { branchOf } from "../A00_Common/design/design_structure_spec";
|
||||
import { PIPE_SPEC } from "../A00_Common/design/design_structure_pipe";
|
||||
import {
|
||||
BOX_SPEC,
|
||||
@@ -57,6 +58,7 @@ import { FORD_SPEC, withLegacyGuards } from "../A00_Common/design/design_structu
|
||||
import { fordHeightHint, fordHeightM } from "../A00_Common/design/design_structure_ford_calc";
|
||||
import {
|
||||
guardHeightHint,
|
||||
pipeDrawn,
|
||||
pipeExtraCount,
|
||||
pipeLengthM,
|
||||
} from "../A00_Common/design/design_structure_pipe_calc";
|
||||
@@ -212,6 +214,8 @@ export interface FacilityOptionsForm {
|
||||
definition?: {
|
||||
spec_names?: Record<string, string>;
|
||||
groups?: Array<{ id: string; name: string }>;
|
||||
/** 옵션 정의 기본값 — 빈 고르기 칸이 그림 값을 보임(54-13). */
|
||||
options?: Array<{ key: string; default: unknown }>;
|
||||
} | null,
|
||||
) => void;
|
||||
/** 배관 명세 컨테이너 「구조물 조합」 잇기 자리 — 폼이 패널 그룹 값에 이음(PLAN 52-1). */
|
||||
@@ -267,7 +271,52 @@ export function createFacilityOptionsForm(
|
||||
clear: (key) => linkSource?.clear(key),
|
||||
set: (key, item) => linkSource?.set(key, item),
|
||||
};
|
||||
// 그림이 쓰는 값(54-13) — 빈 고르기 칸 = 정의 기본값(엔진 · 횡단도가 그 값으로 그림) · 보여 주기만
|
||||
let defaults: Record<string, unknown> = {};
|
||||
const byDefault = (key: string | undefined): { value: string; tag: string } | null => {
|
||||
const value = key ? defaults[key] : null;
|
||||
return value === null || value === undefined || value === ""
|
||||
? null
|
||||
: { value: String(value), tag: "기본" };
|
||||
};
|
||||
/** 빈 날개벽 설치 = 정의 기본 설치(옛 「있음」 · 빈 값 = 날개벽 · 엔진 `_wing_spec`). */
|
||||
const wingDefault = (key: string): { value: string; tag: string } => {
|
||||
const value = String(defaults[key] ?? "");
|
||||
return { value: ["없음", "흙막이"].includes(value) ? value : "날개벽", tag: "기본" };
|
||||
};
|
||||
// 유입 「자동(규칙)」 = 기슭막이 + 유입 형식 auto(옛 값 · 정의 기본) — 그림은 지형 규칙이 집수정 · 흙막이를
|
||||
// 정함. 사용자가 유입 구조를 고를 때까지 저장도 그대로(흙막이 고름의 형식 revet 을 안 적음)
|
||||
let inletAuto: { structure?: string | number } | null = null;
|
||||
const inletIsAuto = (): boolean =>
|
||||
!!inletAuto && pipeForm.state().inlet?.[0]?.structure === "기슭막이";
|
||||
// 그림 계산은 한 번 그릴 동안 한 번만(칸마다 부름)
|
||||
let drawnMemo: ReturnType<typeof pipeDrawn> | undefined;
|
||||
const drawnNow = (): ReturnType<typeof pipeDrawn> => {
|
||||
if (drawnMemo === undefined) {
|
||||
drawnMemo = pipeDrawn(chainage);
|
||||
queueMicrotask(() => (drawnMemo = undefined));
|
||||
}
|
||||
return drawnMemo;
|
||||
};
|
||||
const pipeForm = createSpecForm(specLinks, {
|
||||
shown: (c, item, values, index) => {
|
||||
if (c.id === "inlet" && item.id === "structure" && (!values.structure || inletIsAuto())) {
|
||||
const drawn = drawnNow();
|
||||
return drawn
|
||||
? { value: drawn.basin ? "집수정" : "기슭막이", tag: "자동" }
|
||||
: { value: "", tag: "자동(지형 규칙)" };
|
||||
}
|
||||
if (values[item.id]) return null;
|
||||
if (item.id !== "form") return byDefault(branchOf(item, values)?.key);
|
||||
const basin = c.id === "inlet" && values.structure === "집수정";
|
||||
const drawn = drawnNow();
|
||||
const form = basin
|
||||
? drawn?.basin
|
||||
: c.id === "soil_add"
|
||||
? drawn?.extras[index]
|
||||
: drawn?.forms[c.id];
|
||||
return form ? { value: form, tag: basin ? "자동" : "기본" } : null;
|
||||
},
|
||||
auto: (_c, item) => {
|
||||
if (item.id !== "length") return "";
|
||||
const length = pipeLengthM(chainage);
|
||||
@@ -276,13 +325,21 @@ export function createFacilityOptionsForm(
|
||||
// 흙막이 개수 = 유출 「추가 기슭막이(단)」(54-1)
|
||||
count: (c) => (c.id === "soil_add" ? pipeExtraCount(chainage) : undefined),
|
||||
hint: (c, item, state) => {
|
||||
const soil = c.id !== "inlet" || state.inlet?.[0]?.structure === "기슭막이";
|
||||
const soil =
|
||||
c.id !== "inlet" ||
|
||||
(state.inlet?.[0]?.structure === "기슭막이" && !(inletIsAuto() && drawnNow()?.basin));
|
||||
return item.id === "height" && soil ? guardHeightHint(state.pipe?.[0]?.diameter ?? "") : "";
|
||||
},
|
||||
onChange: (c, item) => {
|
||||
if (c.id === "inlet" && item.id === "structure") inletAuto = null;
|
||||
if (c.id === "inlet" && (item.id === "structure" || item.id === "form"))
|
||||
inletStructureHandler?.(inletStructureOf());
|
||||
emit();
|
||||
// 고른 뒤 그림이 바뀜 — 자동 표시를 새 그림으로 다시 그림(54-13)
|
||||
if (c.id === "inlet") {
|
||||
drawnMemo = undefined;
|
||||
pipeForm.refresh();
|
||||
}
|
||||
},
|
||||
});
|
||||
// 조정창에서 옮겨 오는 행(단 수·옵션)이 붙을 자리 — 유입 · 유출 컨테이너 끝(2026-08-29 지시 4)
|
||||
@@ -303,6 +360,11 @@ export function createFacilityOptionsForm(
|
||||
const fordNeed = (): number | null =>
|
||||
fordHeightM(fordFlow, Number(fordForm.state().ford?.[0]?.width ?? ""));
|
||||
const fordForm = createSpecForm(specLinks, {
|
||||
shown: (c, item, values) => {
|
||||
if (values[item.id]) return null;
|
||||
if (c.id !== "ford") return wingDefault(c.id === "inlet" ? "wing_in" : "wing_out");
|
||||
return item.id === "diameter" ? byDefault("pipe_diameter_mm") : null;
|
||||
},
|
||||
hint: (c, item, state) =>
|
||||
c.id === "ford" && item.id === "height"
|
||||
? fordHeightHint(fordFlow, Number(state.ford?.[0]?.width ?? ""))
|
||||
@@ -368,7 +430,18 @@ export function createFacilityOptionsForm(
|
||||
};
|
||||
|
||||
// ── BOX암거 = 사용자 구조물 표 02 명세(PLAN 52-2) — 본체 · 유입 구조 · 유출 구조 ──
|
||||
const boxForm = createSpecForm(specLinks, { onChange: () => emit() });
|
||||
const boxForm = createSpecForm(specLinks, {
|
||||
onChange: () => emit(),
|
||||
shown: (c, item, values) => {
|
||||
if (values[item.id]) return null;
|
||||
if (c.id !== "body") return wingDefault(c.id === "inlet" ? "wing_in" : "wing_out");
|
||||
const size = bodyOf({
|
||||
body_width_m: Number(defaults.body_width_m ?? 2),
|
||||
body_height_m: Number(defaults.body_height_m ?? 2),
|
||||
}).size;
|
||||
return item.id === "size" && size ? { value: size, tag: "기본" } : null;
|
||||
},
|
||||
});
|
||||
// 날개벽 칸 — 세월교 · BOX 가 같은 조각을 쓰던 자리 · 조정 행은 명세 유입 · 유출 칸으로 옮겨 붙음
|
||||
const wingInFields = createWingFields("날개벽(유입)", WING_IN_KEYS);
|
||||
const wingOutFields = createWingFields("날개벽(유출)", WING_OUT_KEYS);
|
||||
@@ -460,8 +533,10 @@ export function createFacilityOptionsForm(
|
||||
inletStructure: inletStructureOf,
|
||||
setInletStructure(value) {
|
||||
// 조정창 형식 → 유입 보호공 구조 · 형태(자동 = 고른 구조는 지키고 형태만 비움)
|
||||
if (value === "revet") pipeForm.setValue("inlet", "structure", "기슭막이");
|
||||
else if (value !== "auto") pipeForm.setValue("inlet", "structure", "집수정");
|
||||
if (value === "revet") {
|
||||
inletAuto = null;
|
||||
pipeForm.setValue("inlet", "structure", "기슭막이");
|
||||
} else if (value !== "auto") pipeForm.setValue("inlet", "structure", "집수정");
|
||||
if (value !== "revet" && pipeForm.state().inlet?.[0]?.structure === "집수정")
|
||||
pipeForm.setValue("inlet", "form", value === "auto" ? "" : value);
|
||||
syncVisibility();
|
||||
@@ -505,9 +580,16 @@ export function createFacilityOptionsForm(
|
||||
// 받은 값만 칸에 — 없으면 「안 정함」(새 시설은 부르는 쪽이 등록부 기본값을 실어 줌 · 46-7).
|
||||
const groups = Object.fromEntries((definition?.groups ?? []).map((g) => [g.id, g.name]));
|
||||
const names = definition?.spec_names ?? {};
|
||||
defaults = Object.fromEntries((definition?.options ?? []).map((o) => [o.key, o.default]));
|
||||
if (facility === "pipe") {
|
||||
// 정의 파일의 옛 이름 「흙막이 추가」 — 명세 이름 「흙막이」가 앞섬(54-1)
|
||||
const { soil_add: _old, ...pipeNames } = names;
|
||||
const kind = options.inlet_structure;
|
||||
inletAuto =
|
||||
options.inlet_type === "기슭막이" &&
|
||||
!["revet", "I", "L", "U"].includes(String(kind ?? ""))
|
||||
? { structure: kind }
|
||||
: null;
|
||||
pipeForm.write(PIPE_SPEC, options, pipeNames, groups);
|
||||
}
|
||||
if (facility === "box_culvert") {
|
||||
@@ -537,6 +619,11 @@ export function createFacilityOptionsForm(
|
||||
// 고른 칸만 싣는다 — 「안 정함」은 빈 값(기본값을 몰래 확정으로 안 바꿈).
|
||||
if (current === "pipe") {
|
||||
Object.assign(options, pipeForm.read()); // 명세 컨테이너 값 = 옛 키 그대로(PLAN 52-1)
|
||||
// 자동(규칙)은 고를 때까지 그대로 — 보여 주기만으로 파일이 안 바뀜(54-13)
|
||||
if (inletIsAuto()) {
|
||||
if (inletAuto!.structure === undefined) delete options.inlet_structure;
|
||||
else options.inlet_structure = inletAuto!.structure;
|
||||
}
|
||||
} else if (current === "box_culvert") {
|
||||
// 정본은 폭 · 높이 두 칸 — 규격이면 규격 수 · 사용자 입력이면 명세 숫자 칸(PLAN 52-2)
|
||||
Object.assign(options, boxForm.read(), bodyOptions(boxForm.state().body?.[0]));
|
||||
|
||||
@@ -65,7 +65,14 @@ export function optionControl(
|
||||
// 툴팁 — 표가 아직 안 읽는 칸이면 그 사유가 먼저(넣어도 안 바뀜을 알아야 함), 아니면 비울 때의 뜻.
|
||||
const tip = option.not_in_table ?? option.empty_means ?? "";
|
||||
if (option.input === "select") {
|
||||
const blank = option.required ? "— 선택 —" : "— 안 정함 —";
|
||||
// 빈 칸이 쓰는 정의 기본값은 빈 보기에 꼬리를 달아 보임 — 값은 안 고름(54-13)
|
||||
const fallback = option.default === null || option.default === "" ? null : option.default;
|
||||
const blank =
|
||||
fallback !== null && fallback !== undefined
|
||||
? `${label(String(fallback))} (기본)`
|
||||
: option.required
|
||||
? "— 선택 —"
|
||||
: "— 안 정함 —";
|
||||
// 정의에서 뺀 선택지(48)를 고른 옛 값 — 그 값만 남겨 보임(값을 잃지 않음)
|
||||
const kept = value && !option.choices.includes(value) ? [value] : [];
|
||||
const element = select([
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
/* 54-13 — 좌측 폼 고르기 칸이 그림이 쓰는 값(기본값 · 자동 규칙 결과)을 보이는가 · 보여 주기만으로 값이 안 바뀌는가
|
||||
* 시작 화면: B06 횡단설계(#/b06-section) 또는 B05 종단설계(#/b05-profile) · 검증 프로젝트(sub6_검증) · 로그인
|
||||
* 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m5413 = "단계";` · 결과 `sessionStorage.getItem("__5413_out_{단계}")`
|
||||
* check → 목록의 배관 · 세월교 · BOX 전부를 열어 폼 글 ↔ 그림(B06 카드 SVG 글 · 벽 툴팁 · 상세 세트) 대조 ·
|
||||
* B05 에서 부르면 B06 에서 읽은 폼 글과 같은지 · 초안(관 지점 세션) 안 생김
|
||||
* old → 4+5 배관을 옛 「자동(규칙)」(유입 기슭막이 · 형식 없음)으로 초안에 넣고 새로 고침
|
||||
* oldpick→ (새로 고친 뒤) 폼 = 그림 · 관경만 고르면 유입 형식 안 굳음 · 흙막이 고르면 revet · 그림 벽 ·
|
||||
* 12+18 배관 집수정 높이 고침 = 집수정을 고른 것
|
||||
* box → 17+12 세월교(빈 칸) 대조 · 세션에서만 BOX 로 바꾸고 칸을 비워 규격 · 유입 · 유출 = 기본 · 저장 칸 없음
|
||||
* restore→ 초안 지우고 새로 고침 · 서버 관 지점 = 처음 사진
|
||||
* 쓰는 것: sessionStorage 만(서버 저장 없음)
|
||||
* 기대 수치: 모든 단계 bad = []
|
||||
*/
|
||||
(async () => {
|
||||
const MODE = window.__m5413 || "check";
|
||||
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 pid = localStorage.getItem("frd_current_project_id");
|
||||
const DRAFT = `aislo:draft:pipes:${pid}`;
|
||||
const B06 = location.hash.includes("b06");
|
||||
const txt = (el) => el.innerText.replace(/\s+/g, " ").trim();
|
||||
const flat = (s) => String(s ?? "").replace(/\s+/g, "");
|
||||
const out = { mode: MODE, page: B06 ? "B06" : "B05", bad: [] };
|
||||
const bad = (what) => out.bad.push(what);
|
||||
const done = () => sessionStorage.setItem(`__5413_out_${MODE}`, JSON.stringify(out));
|
||||
const api = async () =>
|
||||
(await (await fetch(`/api/projects/${pid}/drainage/pipe-points`)).json()).pipe_points;
|
||||
const rows = () => [...document.querySelectorAll(".b05-route__irregular-list li")];
|
||||
const rowOf = (head) => rows().find((li) => txt(li).startsWith(head));
|
||||
const draft = (at) => {
|
||||
const raw = sessionStorage.getItem(DRAFT);
|
||||
const list = raw ? (JSON.parse(raw).value ?? JSON.parse(raw)) : null;
|
||||
return list?.find((p) => Math.abs(p.chainage_m - at) < 0.51) ?? null;
|
||||
};
|
||||
const spec = () =>
|
||||
[...document.querySelectorAll(".b05-structure__spec")].find((s) => s.offsetParent);
|
||||
/** 폼 고르기 칸 `{항목: 보이는 글}` · 꼬리 표시(data-spec-shown) 칸 목록. */
|
||||
const formOf = () => {
|
||||
const fields = {};
|
||||
const tagged = [];
|
||||
for (const wrap of spec()?.querySelectorAll("[data-spec-item]") ?? []) {
|
||||
const sel = wrap.querySelector("select");
|
||||
if (!sel) continue;
|
||||
fields[wrap.dataset.specItem] = sel.options[sel.selectedIndex]?.text ?? "";
|
||||
if (wrap.dataset.specShown) tagged.push(wrap.dataset.specItem);
|
||||
}
|
||||
return { fields, tagged };
|
||||
};
|
||||
const detailAt = (at) => {
|
||||
const key = Object.keys(sessionStorage).find((k) => k.includes("section-detail"));
|
||||
const raw = key ? JSON.parse(sessionStorage.getItem(key)) : null;
|
||||
const list = (raw?.value ?? raw)?.cross_sections ?? [];
|
||||
return list.reduce(
|
||||
(a, b) => (Math.abs(b.chainage_m - at) < Math.abs((a?.chainage_m ?? 1e9) - at) ? b : a),
|
||||
null,
|
||||
);
|
||||
};
|
||||
/** 그림 — 고른 카드 SVG 글 · 벽 툴팁(역할 + 형태). */
|
||||
const drawn = () => {
|
||||
const svg = document.querySelector(".b06-cross-card--selected svg");
|
||||
if (!svg) return null;
|
||||
return {
|
||||
labels: [...svg.querySelectorAll(".b06-chart__culvert-label")].map((t) => t.textContent),
|
||||
walls: [...svg.querySelectorAll(".b06-chart__culvert-revet title")].map(
|
||||
(t) => t.textContent.split(" H=")[0],
|
||||
),
|
||||
};
|
||||
};
|
||||
const open = async (head) => {
|
||||
const row = await until(() => rowOf(head)).then(() => rowOf(head));
|
||||
if (!row) return (bad(`${head}: 목록에 없음`), false);
|
||||
row.click();
|
||||
await until(() => spec() && (!B06 || document.querySelector(".b06-cross-card--selected svg")));
|
||||
await w(1200);
|
||||
return true;
|
||||
};
|
||||
const fire = (sel, value) => {
|
||||
sel.value = value;
|
||||
sel.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
};
|
||||
const selectOf = (item) => spec()?.querySelector(`[data-spec-item="${item}"] select`);
|
||||
const SHAPE = { I형: "I형", L형: "ㄴ형", ㄷ형: "ㄷ형" };
|
||||
/** 폼 글 ↔ 그림 — 종류마다 그림이 쓰는 값을 따로 읽어 대조. */
|
||||
const compare = (head, at, kind) => {
|
||||
const { fields, tagged } = formOf();
|
||||
const pic = B06 ? drawn() : null;
|
||||
const set = detailAt(at);
|
||||
const got = { head, fields, tagged, pic };
|
||||
const blank = Object.entries(fields).filter(
|
||||
([k, v]) => v === "— 안 정함 —" && !/^(soil_add|chute_add)/.test(k),
|
||||
);
|
||||
if (blank.length) bad(`${head}: 빈 칸 글 ${blank.map(([k]) => k)}`);
|
||||
if (!pic) return got; // B05 — 글 대조는 B06 사진과
|
||||
const head0 = (v) => flat(v).replace(/\((기본|자동)\)$/, "");
|
||||
if (kind === "pipe") {
|
||||
const basin = pic.labels.find((l) => l.startsWith("집수정"));
|
||||
const inlet = head0(fields["inlet.structure"]);
|
||||
if ((inlet === "집수정") !== !!basin) bad(`${head}: 유입 ${inlet} ↔ 그림 ${basin ?? "벽"}`);
|
||||
const form = head0(fields["inlet.form"]);
|
||||
if (basin && !basin.includes(SHAPE[form] ?? "?")) bad(`${head}: 모양 ${form} ↔ ${basin}`);
|
||||
for (const [side, role] of [
|
||||
["inlet", "유입"],
|
||||
["outlet", "유출"],
|
||||
]) {
|
||||
const wall = pic.walls.find((t) => t.startsWith(`${role} 기슭막이`));
|
||||
if (side === "inlet" && basin) continue;
|
||||
const f = head0(fields[`${side}.form`]);
|
||||
if (!wall || flat(wall) !== flat(`${role} 기슭막이 ${f}`))
|
||||
bad(`${head}: ${side} 형태 ${f} ↔ ${wall}`);
|
||||
}
|
||||
const d = Math.round((set?.culvert?.diameter_m ?? 0) * 1000);
|
||||
if (tagged.includes("pipe.diameter") && head0(fields["pipe.diameter"]) !== String(d))
|
||||
bad(`${head}: 관경 ${fields["pipe.diameter"]} ↔ ${d}`);
|
||||
}
|
||||
if (kind === "ford") {
|
||||
const label = pic.labels.find((l) => l.includes("Ø")) ?? "";
|
||||
if (!label.includes(`Ø${head0(fields["ford.diameter"])}`))
|
||||
bad(`${head}: 관경 ${fields["ford.diameter"]} ↔ ${label}`);
|
||||
for (const side of ["in", "out"]) {
|
||||
const wing = set?.ford?.[`wing_${side}`]?.installed;
|
||||
const text = head0(fields[`${side === "in" ? "inlet" : "outlet"}.structure`]);
|
||||
if (wing !== undefined && (text === "날개벽") !== wing)
|
||||
bad(`${head}: ${side} ${text} ↔ 설치 ${wing}`);
|
||||
}
|
||||
}
|
||||
if (kind === "box") {
|
||||
const label = pic.labels.find((l) => l.startsWith("BOX")) ?? "";
|
||||
if (flat(label) !== `BOX${head0(fields["body.size"])}`)
|
||||
bad(`${head}: 규격 ${fields["body.size"]} ↔ ${label}`);
|
||||
}
|
||||
return got;
|
||||
};
|
||||
const KIND = { 배관: "pipe", 세월교: "ford", BOX암거: "box" };
|
||||
|
||||
try {
|
||||
if (MODE === "check") {
|
||||
if (!sessionStorage.getItem("__5413_orig"))
|
||||
sessionStorage.setItem("__5413_orig", JSON.stringify(await api()));
|
||||
const before = sessionStorage.getItem(DRAFT);
|
||||
const heads = rows()
|
||||
.map(txt)
|
||||
.filter((t) => /^\d+\+\d+ (배관|세월교|BOX암거)/.test(t));
|
||||
const b06 = JSON.parse(sessionStorage.getItem("__5413_b06") ?? "{}");
|
||||
out.rows = [];
|
||||
for (const t of heads) {
|
||||
const head = t.split(" ").slice(0, 2).join(" ");
|
||||
if (!(await open(head))) continue;
|
||||
const station = head.split(" ")[0].split("+").map(Number);
|
||||
const got = compare(head, station[0] * 20 + station[1], KIND[t.split(" ")[1]]);
|
||||
out.rows.push(`${head} ${JSON.stringify(got.fields)} | ${JSON.stringify(got.pic)}`);
|
||||
if (B06) b06[t] = got.fields;
|
||||
else if (b06[t] && JSON.stringify(b06[t]) !== JSON.stringify(got.fields))
|
||||
bad(`${head}: B05 ${JSON.stringify(got.fields)} ≠ B06 ${JSON.stringify(b06[t])}`);
|
||||
else if (!b06[t]) bad(`${head}: B06 사진 없음 — B06 에서 check 먼저`);
|
||||
}
|
||||
if (B06) sessionStorage.setItem("__5413_b06", JSON.stringify(b06));
|
||||
if (sessionStorage.getItem(DRAFT) !== before) bad("열기만 했는데 관 초안이 바뀜");
|
||||
out.count = heads.length;
|
||||
} else if (MODE === "old") {
|
||||
const list = (await api()).map(({ lonlat, ...rest }) => rest);
|
||||
const pipe = list.find((p) => Math.abs(p.chainage_m - 85.05) < 0.51);
|
||||
pipe.options = { ...pipe.options, inlet_type: "기슭막이" };
|
||||
delete pipe.options.inlet_structure;
|
||||
sessionStorage.setItem(DRAFT, JSON.stringify(list));
|
||||
out.note = "초안 넣음 — 새로 고친 뒤 oldpick";
|
||||
done();
|
||||
location.reload();
|
||||
return;
|
||||
} else if (MODE === "oldpick") {
|
||||
await open("4+5 배관");
|
||||
out.start = compare("4+5 옛 자동", 85.05, "pipe");
|
||||
if (!flat(out.start.fields["inlet.structure"]).endsWith("(자동)"))
|
||||
bad(`옛 자동 유입 글 ${out.start.fields["inlet.structure"]}`);
|
||||
// 관경만 고름 — 유입 형식은 그대로(자동 유지)
|
||||
fire(selectOf("pipe.diameter"), "1200");
|
||||
await w(2500);
|
||||
out.afterDiameter = draft(85.05)?.options;
|
||||
if (out.afterDiameter?.inlet_structure !== undefined)
|
||||
bad(`관경만 골랐는데 형식 ${out.afterDiameter.inlet_structure}`);
|
||||
if (out.afterDiameter?.inlet_type !== "기슭막이")
|
||||
bad(`유입 저장 값 바뀜 ${out.afterDiameter?.inlet_type}`);
|
||||
out.stillAuto = formOf().fields["inlet.structure"];
|
||||
// 흙막이를 고름 = 형식 revet · 그림 벽
|
||||
fire(selectOf("inlet.structure"), "기슭막이");
|
||||
await w(3500);
|
||||
out.afterPick = draft(85.05)?.options;
|
||||
if (out.afterPick?.inlet_structure !== "revet")
|
||||
bad(`흙막이 골랐는데 형식 ${out.afterPick?.inlet_structure}`);
|
||||
out.picked = compare("4+5 흙막이 고름", 85.05, "pipe");
|
||||
if (out.picked.fields["inlet.structure"] !== "흙막이")
|
||||
bad(`고른 글 ${out.picked.fields["inlet.structure"]}`);
|
||||
// 다른 관(자동 집수정) — 집수정 높이 고침 = 집수정을 고른 것
|
||||
await open("12+18 배관");
|
||||
const shown = formOf().fields["inlet.structure"];
|
||||
out.other = shown;
|
||||
if (flat(shown) === "집수정(자동)") {
|
||||
const h = spec().querySelector('[data-spec-item="inlet.height"] input');
|
||||
h.value = "1.5";
|
||||
h.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await w(3000);
|
||||
const o = draft(258.12)?.options;
|
||||
out.basinEdit = o;
|
||||
if (o?.inlet_type !== "집수정" || o?.inlet_basin_height_m !== 1.5)
|
||||
bad(`집수정 높이 고침 저장 ${JSON.stringify(o)}`);
|
||||
if (formOf().fields["inlet.structure"] !== "집수정")
|
||||
bad(`높이 고친 뒤 글 ${formOf().fields["inlet.structure"]}`);
|
||||
}
|
||||
} else if (MODE === "box") {
|
||||
await open("17+12 세월교");
|
||||
out.ford = compare("17+12 세월교", 352.14, "ford");
|
||||
const type = document.querySelectorAll(
|
||||
".b05-structure-section .b05-structure__grid select",
|
||||
)[1];
|
||||
fire(type, "box_culvert");
|
||||
await until(() => rowOf("17+12 BOX암거"));
|
||||
await w(3000);
|
||||
// 새 종류는 정의 기본값이 값으로 실림(46-7) — 칸을 비워 옛 빈 저장과 같게
|
||||
for (const k of ["body.size", "inlet.structure", "outlet.structure"]) {
|
||||
fire(selectOf(k), "");
|
||||
await w(1500);
|
||||
}
|
||||
await w(2000);
|
||||
out.box = compare("17+12 BOX", 352.14, "box");
|
||||
const f = out.box.fields;
|
||||
if (flat(f["body.size"]) !== "2.0×2.0(기본)") bad(`BOX 규격 ${f["body.size"]}`);
|
||||
for (const k of ["inlet.structure", "outlet.structure"])
|
||||
if (flat(f[k]) !== "날개벽(기본)") bad(`BOX ${k} ${f[k]}`);
|
||||
const o = draft(352.14)?.options ?? {};
|
||||
if (["body_width_m", "wing_in", "wing_out"].some((k) => k in o))
|
||||
bad(`보여 주기만 했는데 BOX 저장 칸 ${JSON.stringify(o)}`);
|
||||
} else if (MODE === "restore") {
|
||||
sessionStorage.removeItem(DRAFT);
|
||||
// 고르기 단계가 남긴 B06 유입 형식 세션 · 그걸 얹은 상세 캐시(다시 받음)
|
||||
for (const key of Object.keys(sessionStorage))
|
||||
if (key.includes(`inletstruct:${pid}`) || key.includes(`section-detail:${pid}`))
|
||||
sessionStorage.removeItem(key);
|
||||
const orig = JSON.parse(sessionStorage.getItem("__5413_orig") ?? "null");
|
||||
const now = await api();
|
||||
out.same = JSON.stringify(now) === JSON.stringify(orig);
|
||||
if (!out.same) bad("서버 관 지점이 처음 사진과 다름");
|
||||
done();
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
bad(String(error?.stack ?? error));
|
||||
}
|
||||
done();
|
||||
})();
|
||||
Reference in New Issue
Block a user