diff --git a/A00_Common/design/design_structure_pipe_calc.ts b/A00_Common/design/design_structure_pipe_calc.ts index 91e21143e..54b6bb940 100644 --- a/A00_Common/design/design_structure_pipe_calc.ts +++ b/A00_Common/design/design_structure_pipe_calc.ts @@ -20,6 +20,9 @@ export function setPipeSectionsSource(source: () => readonly CrossSection[] | nu sectionsSource = source; } +/** 횡단 측점 목록 읽기 — 노면공 확폭 구간 상자(87-3)가 확폭을 읽음 · 자료 없으면 null. */ +export const pipeSections = (): readonly CrossSection[] | null => sectionsSource?.() ?? null; + /** 유출 단 수를 대는 곳 — 횡단 보기의 세션 값 우선(`countFor`) · 없으면 정본 patch. */ let extraCountSource: ((section: CrossSection) => number) | null = null; export function setPipeExtraCountSource(source: (section: CrossSection) => number): void { diff --git a/A00_Common/design/design_structure_widening.ts b/A00_Common/design/design_structure_widening.ts new file mode 100644 index 000000000..6b841ea38 --- /dev/null +++ b/A00_Common/design/design_structure_widening.ts @@ -0,0 +1,114 @@ +/* ============================================================================= + * design_structure_widening.ts + * 노면공 › 확폭 구간(87-3) — 범위 안에서 확폭(좌 + 우) > 0 인 횡단 측점을 이어진 구간으로 묶고, + * 끈 구간을 `options.widening_off = "시작-끝,시작-끝"`(측점 m · 빈 글 = 모두 켬)으로 읽고 씀. + * 켬 = 확폭 포함 · 끔 = 규격 폭만. 노선이 바뀌어 구간 끝이 안 맞는 끈 값은 무시(= 켬). + * 횡단 형상 쪽(88장)도 이 키를 같은 글로 읽음 — 측점이 범위 ±0.02 m 안. + * import 없음 — 시험이 이 파일만 옮겨 돌림. + * ========================================================================== */ + +/** 구조물 범위 · 구간 끝 맞춤 허용치(m). */ +export const WIDENING_TOL_M = 0.02; + +/** 횡단 측점에서 읽는 몫만 — 횡단 설계 확폭(좌 · 우) · 설계 없으면 저장된 곡선 확폭(바깥쪽 한 방향). */ +export interface WideningStation { + chainage_m: number; + curve_widening_m?: number | null; + curve_outer_side?: "left" | "right" | null; + design?: { widening_left_m?: number; widening_right_m?: number } | null; +} + +export interface WideningRun { + start_m: number; + end_m: number; + /** 구간 안 확폭(좌 + 우) 최댓값(m). */ + widening_m: number; +} + +/** 한 측점의 확폭(좌 + 우, m) — 없으면 0. */ +export function wideningOf(station: WideningStation): number { + const sum = station.design + ? (station.design.widening_left_m ?? 0) + (station.design.widening_right_m ?? 0) + : station.curve_outer_side + ? (station.curve_widening_m ?? 0) + : 0; + return Number.isFinite(sum) && sum > 0 ? sum : 0; +} + +/** 범위 `[start, end]` 안 확폭 > 0 측점을 이어진 구간(범위 안 측점 목록에서 이웃)으로 묶음. */ +export function wideningRuns( + stations: readonly WideningStation[], + start: number, + end: number, +): WideningRun[] { + const inside = stations + .filter((s) => s.chainage_m >= start - WIDENING_TOL_M && s.chainage_m <= end + WIDENING_TOL_M) + .sort((a, b) => a.chainage_m - b.chainage_m); + const runs: WideningRun[] = []; + let open: WideningRun | null = null; + for (const station of inside) { + const width = wideningOf(station); + if (width <= 0) { + open = null; + continue; + } + if (open) { + open.end_m = station.chainage_m; + open.widening_m = Math.max(open.widening_m, width); + } else { + open = { start_m: station.chainage_m, end_m: station.chainage_m, widening_m: width }; + runs.push(open); + } + } + return runs; +} + +const round = (value: number): number => Number(value.toFixed(2)); + +/** 저장 글 → 끈 구간 쌍. 틀린 조각은 버림. */ +export function parseOff(text: unknown): Array<[number, number]> { + if (typeof text !== "string") return []; + const pairs: Array<[number, number]> = []; + for (const part of text.split(",")) { + const [a, b] = part.split("-").map((token) => Number(token.trim())); + if (part.includes("-") && Number.isFinite(a) && Number.isFinite(b) && b >= a) + pairs.push([a, b]); + } + return pairs; +} + +export const formatOff = (pairs: ReadonlyArray): string => + pairs.map(([a, b]) => `${round(a)}-${round(b)}`).join(","); + +/** 끈 쌍이 이 구간과 맞는가(시작 · 끝 각각 허용치 안). */ +export const pairMatches = (pair: readonly [number, number], run: WideningRun): boolean => + Math.abs(pair[0] - run.start_m) <= WIDENING_TOL_M && + Math.abs(pair[1] - run.end_m) <= WIDENING_TOL_M; + +/** 지금 구간들 중 끈 것만 남긴 글(안 맞는 옛 값은 빠짐). */ +export function offForRuns(runs: readonly WideningRun[], text: unknown): string { + const pairs = parseOff(text); + return formatOff( + runs + .filter((run) => pairs.some((p) => pairMatches(p, run))) + .map((run) => [run.start_m, run.end_m] as const), + ); +} + +/** 구간 하나의 켬/끔을 뒤집은 새 글(맞는 구간 쌍만 남김). */ +export function toggleRun( + runs: readonly WideningRun[], + text: unknown, + target: WideningRun, +): string { + const pairs = parseOff(text); + const isOff = (run: WideningRun): boolean => pairs.some((p) => pairMatches(p, run)); + return formatOff( + runs + .filter((run) => (run === target ? !isOff(run) : isOff(run))) + .map((run) => [run.start_m, run.end_m] as const), + ); +} + +export const isRunOff = (run: WideningRun, text: unknown): boolean => + parseOff(text).some((p) => pairMatches(p, run)); diff --git a/B05_Profile/B05_Profile_UI_Structures_Form.ts b/B05_Profile/B05_Profile_UI_Structures_Form.ts index 7a7c33571..1cbb4d8cc 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Form.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Form.ts @@ -29,6 +29,7 @@ import { type FacilityDefinition, } from "./B05_Profile_UI_Structures_Define_Form"; import { createStructureSpecForm } from "./B05_Profile_UI_Structures_Spec"; +import { createWideningBox } from "./B05_Profile_UI_Structures_Widening"; export interface StructuresFormElements { root: HTMLElement; @@ -156,6 +157,16 @@ export function buildStructuresForm(options: { set: groupBoxes.setOne, }); groupBoxes.watch(facilityOptions.paintLinks); + // 노면공 시작~종료 읽기(87-1 길이 출력 · 87-3 확폭 구간 상자가 같이 씀) — 못 읽으면 null + const rangeNow = (): { start: number | null; end: number | null } => { + const step = options.getInterval(); + return { start: startFields.read(step, false), end: endFields.read(step, false) }; + }; + const widening = createWideningBox({ + range: rangeNow, + interval: options.getInterval, + onChange: options.onFacilityChange, + }); // 일반 구조물 명세 컨테이너(PLAN 52-10 · 노면공 등) — 잇기 · 반영 길은 배관 명세와 같음 const structureSpec = createStructureSpecForm( optionRow, @@ -163,19 +174,20 @@ export function buildStructuresForm(options: { options.onFacilityChange, // 노면공 길이 칸(87-1) = 시작~종료 출력 — 못 셈이면 빈 글(「—」) (_c, item) => { - if (item.id !== "length") return ""; - const step = options.getInterval(); - const start = startFields.read(step, false); - const end = endFields.read(step, false); - return start !== null && end !== null && end > start + const { start, end } = rangeNow(); + return item.id === "length" && start !== null && end !== null && end > start ? String(Number((end - start).toFixed(2))) : ""; }, + widening, ); - // 시작 · 종료를 고치면 길이 출력을 다시 그림 + // 시작 · 종료를 고치면 길이 출력 · 확폭 구간을 다시 그림 [startFields, endFields].forEach((fields) => [fields.station, fields.remainder].forEach((input) => - input.addEventListener("change", () => structureSpec.refresh()), + input.addEventListener("change", () => { + structureSpec.refresh(); + widening.refresh(); + }), ), ); groupBoxes.watch(structureSpec.paintLinks); @@ -190,6 +202,7 @@ export function buildStructuresForm(options: { positionDivider, optionRow, structureSpec.root, + widening.root, // 노면공 「확폭 구간」 — 상세 내용 아래(87-3) facilityOptions.root, staleBox, groupBoxes.root, diff --git a/B05_Profile/B05_Profile_UI_Structures_Pavement.ts b/B05_Profile/B05_Profile_UI_Structures_Pavement.ts index a2c435550..ed3a7805d 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Pavement.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Pavement.ts @@ -54,7 +54,7 @@ export async function splitPavementRange( await showConfirmDialog( `지정한 포장 구간(${format(start)}~${format(end)})이 물넘이포장` + `(${format(partial.start)}~${format(partial.end)})의 끝에 걸칩니다.\n` + - "물넘이는 그 자체가 포장이라 겹칠 수 없습니다 — 기준 측점·길이·전후 값을 고쳐 주세요.", + "물넘이는 그 자체가 포장이라 겹칠 수 없습니다 — 시작·종료 측점을 고쳐 주세요.", "확인", ); return null; diff --git a/B05_Profile/B05_Profile_UI_Structures_Spec.ts b/B05_Profile/B05_Profile_UI_Structures_Spec.ts index e5cb9320d..ff2ad8b7e 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Spec.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Spec.ts @@ -18,8 +18,11 @@ import { import { STRUCTURE_SPECS } from "../A00_Common/design/design_structure_pipe"; import { specKeys, type StructureSpec } from "../A00_Common/design/design_structure_spec"; import { optionDefaults } from "./B05_Profile_UI_Structures_Fields"; +import type { WideningBox } from "./B05_Profile_UI_Structures_Widening"; const forms = new WeakMap(); +/** 범위형 명세(노면공 · 87-3)의 「확폭 구간」 상자 — 옵션 칸 짝. */ +const widenings = new WeakMap(); /** 일반 구조물 명세 — 계곡 통과 시설(관 지점 정본)은 시설 폼 몫이라 뺌. */ export const structureSpecOf = (type: StructureType | null): StructureSpec | null => @@ -31,14 +34,19 @@ export function createStructureSpecForm( links: SpecLinks, onChange: () => void, auto?: SpecFormHooks["auto"], + widening?: WideningBox, ): SpecForm { const form = createSpecForm(links, { onChange, auto }); form.root.hidden = true; forms.set(optionRow, form); + if (widening) widenings.set(optionRow, widening); const lock = (): void => { const locked = optionRow.classList.contains("is-locked"); - form.root.inert = locked; - form.root.classList.toggle("is-locked", locked); + for (const root of [form.root, widening?.root]) { + if (!root) continue; + root.inert = locked; + root.classList.toggle("is-locked", locked); + } }; if (typeof MutationObserver !== "undefined") new MutationObserver(lock).observe(optionRow, { @@ -51,7 +59,16 @@ export function createStructureSpecForm( /** 정의 옵션 칸으로 안 그리는 키 — 명세 키 · 명세가 뺀 옛 키. */ export function specOwnedKeys(type: StructureType | null): Set { const spec = structureSpecOf(type); - return new Set(spec ? [...specKeys(spec).keys, ...spec.retired, ...(spec.dropped ?? [])] : []); + return new Set( + spec + ? [ + ...specKeys(spec).keys, + ...spec.retired, + ...(spec.dropped ?? []), + ...(spec.byRange ? ["widening_off"] : []), + ] + : [], + ); } /** 종류 · 값으로 명세 폼을 다시 그림(명세 없는 종류면 가림). */ @@ -62,6 +79,7 @@ export function paintStructureSpec( ): void { const form = forms.get(optionRow); const spec = structureSpecOf(type); + widenings.get(optionRow)?.paint(!!spec?.byRange, values.widening_off); if (!form) return; form.root.hidden = !spec; if (!spec) return; @@ -75,7 +93,9 @@ export function readStructureSpec( type: StructureType | null, ): Record { const form = forms.get(optionRow); - return form && structureSpecOf(type) ? form.read() : {}; + const spec = structureSpecOf(type); + if (!form || !spec) return {}; + return { ...form.read(), ...(spec.byRange ? widenings.get(optionRow)?.read() : {}) }; } /** [수정] 병합에서 폼이 다시 쓰는 키 — 명세 키 + 옛 값 중 선택 컨테이너 머리 키(빼면 지워져야 함). */ @@ -87,5 +107,5 @@ export function specRenderedKeys( if (!spec) return []; const { keys, prefixes } = specKeys(spec); const own = Object.keys(previous ?? {}).filter((key) => prefixes.some((p) => key.startsWith(p))); - return [...keys, ...own, ...(spec.dropped ?? [])]; + return [...keys, ...own, ...(spec.dropped ?? []), ...(spec.byRange ? ["widening_off"] : [])]; } diff --git a/B05_Profile/B05_Profile_UI_Structures_Widening.ts b/B05_Profile/B05_Profile_UI_Structures_Widening.ts new file mode 100644 index 000000000..252ce54f9 --- /dev/null +++ b/B05_Profile/B05_Profile_UI_Structures_Widening.ts @@ -0,0 +1,102 @@ +/* ============================================================================= + * B05_Profile_UI_Structures_Widening.ts + * 노면공 「확폭 구간」 상자(PLAN 87-3) — 상세 내용 아래. 범위 안 확폭 > 0 측점을 이어진 구간으로 묶어 + * 구간마다 단추(켬 = 확폭 포함 · 끔 = 규격 폭만 · 기본 켬). 끈 구간 = `options.widening_off`. + * 규칙(구간 묶기 · 글 읽고 쓰기)은 `design_structure_widening` · 횡단 측점 읽기는 `pipeSections`. + * ========================================================================== */ + +import { pipeSections } from "../A00_Common/design/design_structure_pipe_calc"; +import { + isRunOff, + offForRuns, + toggleRun, + wideningRuns, + type WideningRun, +} from "../A00_Common/design/design_structure_widening"; +import { stationLabel } from "../common_util/common_util_station"; +import { group } from "./B05_Profile_UI_Drainage_Facility_Fields"; + +export interface WideningBox { + root: HTMLElement; + /** 종류 · 저장 값으로 다시 그림(범위형이 아니면 가림). */ + paint: (visible: boolean, stored: string | number | undefined) => void; + /** 저장할 값 — 끈 구간이 없으면 빈 묶음. 횡단 자료가 없으면 저장 값을 그대로 지킴. */ + read: () => Record; + /** 범위 · 횡단 자료가 바뀌었을 때 다시 그림. */ + refresh: () => void; +} + +export function createWideningBox(hooks: { + range: () => { start: number | null; end: number | null }; + interval: () => number; + onChange: () => void; +}): WideningBox { + const box = group("확폭 구간"); + box.root.classList.add("b05-structure__widening"); + box.root.hidden = true; + let off = ""; + + /** 지금 범위의 구간들 — 횡단 자료 · 범위가 없으면 null(+ 안내 글). */ + const runs = (): { list: WideningRun[] | null; note: string } => { + const sections = pipeSections(); + if (!sections?.length) return { list: null, note: "횡단 자료 없음" }; + const { start, end } = hooks.range(); + if (start === null || end === null || end <= start) + return { list: null, note: "시작 · 종료 측점을 넣으면 보임" }; + const list = wideningRuns(sections, start, end); + return { list, note: list.length ? "" : "범위 안 확폭 없음" }; + }; + + const paintBody = (): void => { + const { list, note } = runs(); + if (!list?.length) { + const text = document.createElement("span"); + text.className = "b05-structure__widening-note"; + text.textContent = note; + box.body.replaceChildren(text); + return; + } + const step = hooks.interval(); + box.body.replaceChildren( + ...list.map((run) => { + const on = !isRunOff(run, off); + const button = document.createElement("button"); + button.type = "button"; + button.className = `b05-route__irregular-btn${on ? " is-primary" : ""}`; + button.setAttribute("aria-pressed", String(on)); + button.dataset.wideningRun = `${run.start_m}-${run.end_m}`; + button.textContent = `${stationLabel(run.start_m, step)} ~ ${stationLabel(run.end_m, step)} · 확폭 ${Number(run.widening_m.toFixed(2))} m`; + button.title = on ? "확폭 포함 — 누르면 규격 폭만" : "규격 폭만 — 누르면 확폭 포함"; + button.addEventListener("click", () => { + off = toggleRun(list, off, run); + paintBody(); + hooks.onChange(); + }); + return button; + }), + ); + }; + + // 횡단 자료가 늦게 오면 상자 위에 올 때 다시 그림 + box.root.addEventListener("pointerenter", () => { + if (!box.root.hidden) paintBody(); + }); + + return { + root: box.root, + paint(visible, stored) { + box.root.hidden = !visible; + off = typeof stored === "string" ? stored : ""; + if (visible) paintBody(); + }, + read() { + const { list } = runs(); + const text = list ? offForRuns(list, off) : off; + const out: Record = text ? { widening_off: text } : {}; + return out; + }, + refresh() { + if (!box.root.hidden) paintBody(); + }, + }; +} diff --git a/B05_Profile/B05_Profile_UI_Style_Structures.css b/B05_Profile/B05_Profile_UI_Style_Structures.css index e0cadecda..9a1311990 100644 --- a/B05_Profile/B05_Profile_UI_Style_Structures.css +++ b/B05_Profile/B05_Profile_UI_Style_Structures.css @@ -570,3 +570,25 @@ font-size: var(--text-caption); line-height: 1; } + +/* 노면공 「확폭 구간」 상자(PLAN 87-3) — 구간마다 단추 한 줄 */ +.b05-structure__widening[hidden] { + display: none; +} + +.b05-structure__widening .b05-structure__group-body { + display: flex; + flex-direction: column; + gap: 4px; +} + +.b05-structure__widening .b05-structure__group-body button { + align-self: stretch; + text-align: left; + white-space: normal; +} + +.b05-structure__widening-note { + color: var(--text-muted, inherit); + font-size: var(--text-caption); +} diff --git a/resources/tester/helper_87_widening.cjs b/resources/tester/helper_87_widening.cjs new file mode 100644 index 000000000..4aea3aeda --- /dev/null +++ b/resources/tester/helper_87_widening.cjs @@ -0,0 +1,54 @@ +/* 노면공 확폭 구간 규칙(PLAN 87-3) — `design_structure_widening.ts` 를 그 자리에서 트랜스파일해 Node 로 돌림. */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); + +const file = path.join( + __dirname, + "..", + "..", + "A00_Common", + "design", + "design_structure_widening.ts", +); +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", js)(box.exports, box); +const w = box.exports; + +// 곡선 둘(앞 No.1+0~No.3+0 · 뒤 No.5+0~No.6+0) 사이에 직선 측점 하나 — 확폭 좌우 합 +const at = (chainage_m, left = 0, right = 0) => ({ + chainage_m, + design: { widening_left_m: left, widening_right_m: right }, +}); +const stations = [ + at(0), + at(20, 0.3, 0), + at(40, 0.75, 0), + at(60, 0.4, 0), + at(80), + at(100, 0, 0.5), + at(120, 0, 0.6), + at(140), +]; +const runs = w.wideningRuns(stations, 0, 140); +const inner = w.wideningRuns(stations, 30, 110); // 범위가 곡선을 가로지름 +const off = w.formatOff([[20, 60]]); +const out = { + runs, + inner, + none: w.wideningRuns(stations, 0, 10), + fallback: w.wideningOf({ chainage_m: 1, curve_widening_m: 0.9, curve_outer_side: "left" }), + fallbackStraight: w.wideningOf({ chainage_m: 1, curve_widening_m: 0.9, curve_outer_side: null }), + off, + parsed: w.parseOff("20-60, 100-120,bad,5"), + isOff: runs.map((r) => w.isRunOff(r, off)), + toggledOff: w.toggleRun(runs, "", runs[1]), + toggledBoth: w.toggleRun(runs, off, runs[1]), + toggledOn: w.toggleRun(runs, off, runs[0]), + stale: w.offForRuns(runs, "20-60,0-5,100-130"), + empty: w.offForRuns([], "20-60"), +}; +process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/test_87_widening_runs.py b/resources/tester/test_87_widening_runs.py new file mode 100644 index 000000000..05a91b85d --- /dev/null +++ b/resources/tester/test_87_widening_runs.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +"""노면공 확폭 구간(PLAN 87-3) — 구간 묶기 · `widening_off` 글 읽고 쓰기 · 안 맞는 값 무시. + +규칙은 Node 헬퍼(`helper_87_widening.cjs`)가 `design_structure_widening.ts` 를 그대로 돌림. +""" + +import json +import os +import subprocess + +import pytest + +HERE = os.path.dirname(os.path.abspath(__file__)) + + +@pytest.fixture(scope="module") +def out(): + proc = subprocess.run( + ["node", os.path.join(HERE, "helper_87_widening.cjs")], + capture_output=True, + text=True, + encoding="utf-8", + timeout=60, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +def test_곡선_둘은_구간_둘_확폭은_구간_최대(out): + assert out["runs"] == [ + {"start_m": 20, "end_m": 60, "widening_m": 0.75}, + {"start_m": 100, "end_m": 120, "widening_m": 0.6}, + ] + + +def test_범위_안_측점만_묶음(out): + assert out["inner"] == [ + {"start_m": 40, "end_m": 60, "widening_m": 0.75}, + {"start_m": 100, "end_m": 100, "widening_m": 0.5}, + ] + assert out["none"] == [] + + +def test_설계_없으면_곡선_확폭_바깥쪽만(out): + assert out["fallback"] == 0.9 + assert out["fallbackStraight"] == 0 + + +def test_끈_구간_글_왕복(out): + assert out["off"] == "20-60" + assert out["parsed"] == [[20, 60], [100, 120]] # 틀린 조각은 버림 + assert out["isOff"] == [True, False] + assert out["toggledOff"] == "100-120" + assert out["toggledBoth"] == "20-60,100-120" + assert out["toggledOn"] == "" + + +def test_안_맞는_끈_값은_무시_저장_때_빠짐(out): + assert out["stale"] == "20-60" + assert out["empty"] == "" diff --git a/resources/tester/ui_checks/52-10_노면공_기타.js b/resources/tester/ui_checks/52-10_노면공_기타.js index 10ca498ab..efce4d6ff 100644 --- a/resources/tester/ui_checks/52-10_노면공_기타.js +++ b/resources/tester/ui_checks/52-10_노면공_기타.js @@ -8,8 +8,9 @@ * orca eval --expression "window.__m5210='restore'; $(cat 이 파일)" → 추가한 둘 삭제 · [저장] * 쓰는 것: 검증 프로젝트 structures.json(되돌림 단계가 지움) * 기대 수치: - * 포장 — containers ["detail"] · 상세 내용 · 포장사양 선택지 ["콘크리트"] · 높이 0.35 · 잇기 지정 · - * 종류 단위 잇기 줄 가림 · 옛 상세 칸(두께 · 줄눈 …) 안 보임 · 길이 · 전 · 후 칸 보임 + * 포장 — containers ["detail"] · 상세 내용 · 포장사양 선택지 [콘크리트 · 아스콘 · 쇄석] · 칸 차례 포장사양 → 길이(출력 20) → + * 높이 0.35 · 잇기 지정 · 종류 단위 잇기 줄 가림 · 옛 상세 칸(두께 · 줄눈 …) 안 보임 · + * 기준 측점 칸 없음 · 시작 · 종료 칸 보임 · 길이 · 전 · 후 입력 칸 없음 * 기타 — 「상세 종류 이름」 글 칸 = 「52-10 시험 기타」 · 목록 이름도 그 글 · * 선택 컨테이너 「상세 내용」(52-14) 하나만(52-17) — 추가 → [추가] 없음 · 빼기 → 다시 보임 · * 다시 추가 · 비고 「52-14 비고」(길이 상한 100) · 잇기 · 하나 남음 · 종류 단위 잇기 줄 가림 @@ -37,7 +38,7 @@ }; const listItems = () => [...document.querySelectorAll(".b05-route__irregular-list li")]; const ITEMS = [ - { typeId: "pavement_concrete", group: "G", at: [4, 6], list: /콘크리트 포장/ }, + { typeId: "pavement_concrete", group: "G", at: [4, 6], to: [5, 6], list: /콘크리트 포장/ }, { typeId: "etc", group: "호환", at: [5, 2], list: new RegExp(NAME) }, ]; const typeSummary = () => @@ -76,6 +77,16 @@ : null, paveSpec: specCtl("spec")?.value ?? null, height: specCtl("height")?.value ?? null, + itemOrder: boxes[0] + ? [...boxes[0].querySelectorAll("[data-spec-item]")].map((n) => n.dataset.specItem) + : [], + lengthOut: + spec()?.querySelector('[data-spec-item="detail.length"] output')?.textContent ?? null, + anchorHidden: !!sec().querySelectorAll(".b05-structure__position-row > label")[2]?.hidden, + startEndShown: [1, 3].every( + (n) => !sec().querySelectorAll(".b05-structure__position-row > label")[n]?.hidden, + ), + wideningShown: vis(sec().querySelector(".b05-structure__widening")), containerLink: !!boxes[0]?.querySelector(".b05-structure__summary-name.is-assigned"), containerLinks: boxes.map( (b) => !!b.querySelector(".b05-structure__summary-name.is-assigned"), @@ -139,11 +150,20 @@ for (const item of ITEMS) { await pickType(item.group, item.typeId); const row = { lockedBeforePosition: shape(item.typeId).locked }; - const [no, dist] = sec() - .querySelectorAll(".b05-structure__position-row > label")[2] - .querySelectorAll("input"); - fire(no, String(item.at[0])); - fire(dist, String(item.at[1])); + const labels = sec().querySelectorAll(".b05-structure__position-row > label"); + if (item.to) { + // 노면공 = 시작 · 종료 측점만(87-1) — 기준 측점 칸 없음 + const [s0, s1] = labels[1].querySelectorAll("input"); + const [e0, e1] = labels[3].querySelectorAll("input"); + fire(s0, String(item.at[0])); + fire(s1, String(item.at[1])); + fire(e0, String(item.to[0])); + fire(e1, String(item.to[1])); + } else { + const [no, dist] = labels[2].querySelectorAll("input"); + fire(no, String(item.at[0])); + fire(dist, String(item.at[1])); + } await w(300); if (item.typeId === "pavement_concrete") { fire(specCtl("spec"), "콘크리트"); @@ -226,13 +246,17 @@ !pave.missing && JSON.stringify(pave.containers) === '["detail"]' && pave.legend[0] === "상세 내용" && - JSON.stringify(pave.paveChoices) === '["콘크리트"]' && + JSON.stringify(pave.paveChoices) === '["콘크리트","아스콘","쇄석"]' && + JSON.stringify(pave.itemOrder) === '["detail.spec","detail.length","detail.height"]' && + Number(pave.lengthOut) === 20 && + pave.anchorHidden && + pave.startEndShown && pave.paveSpec === "콘크리트" && Number(pave.height) === 0.35 && pave.containerLink && !pave.typeSummaryVisible && !pave.options.some((l) => /두께|줄눈|철망|비닐|포장 폭|확폭/.test(l)) && - ["길이", "기준측점 전", "기준측점 후"].every((l) => pave.options.some((o) => o.startsWith(l))); + !pave.options.some((l) => /^(길이|기준측점 전|기준측점 후)/.test(l)); // 87-1 입력 칸 없음 out.etcOk = !!etc && !etc.missing && diff --git a/resources/tester/ui_checks/52-15_같은측점_고르기.js b/resources/tester/ui_checks/52-15_같은측점_고르기.js index 605392815..4f41b9f7c 100644 --- a/resources/tester/ui_checks/52-15_같은측점_고르기.js +++ b/resources/tester/ui_checks/52-15_같은측점_고르기.js @@ -13,7 +13,7 @@ (async () => { const MODE = window.__m5215 || "check"; const PIPE = /^22\+0 배관/; - const PAVE = /^21\+15~22\+5 콘크리트 포장/; // 기준 22+0 · 길이 10 m + const PAVE = /^21\+15~22\+5 콘크리트 포장/; // 시작 21+15 · 종료 22+5 (10 m) const OTHER = /^4\+5 배관/; const w = (ms) => new Promise((r) => setTimeout(r, ms)); const until = async (fn, ms = 15000) => { @@ -85,13 +85,16 @@ await w(300); fire(t, "pavement_concrete"); await w(1500); - const anchor = () => - sec().querySelectorAll(".b05-structure__position-row > label")[2].querySelectorAll("input"); - fire(anchor()[0], "22"); + // 노면공 = 시작 · 종료 측점만(87-1) — 시작 21+15 · 종료 22+5(10 m) + const pos = (n) => + sec().querySelectorAll(".b05-structure__position-row > label")[n].querySelectorAll("input"); + fire(pos(1)[0], "21"); + fire(pos(1)[1], "15"); + fire(pos(3)[0], "22"); await w(300); - fire(anchor()[1], "0"); + fire(pos(3)[1], "5"); await w(500); - out.anchor = [...anchor()].map((i) => i.value); + out.anchor = [...pos(1), ...pos(3)].map((i) => i.value); fire(specCtl("spec"), "콘크리트"); await w(200); fire(specCtl("height"), "0.35");