fix(505-3 B05 그림 · 글): 임시 횡단 창 = B06 카드(형상 얹은 단면) · 빈칸 칩 그래프 오른쪽 위 · 곡선 판 가둠 · 지반선 누름 통과 · STALE 글 · 창 단추 공용 꼴 — 요구 309-3 · 310
- 측점 뜨기(design_pipeline_probe)가 형상 · 이름 얹은 단면도 돌려줌 · 창은 「횡단」 판과 같은 routeCrossCard(서버 미리 뜬 것만 선 그림) - Cross_Card 판 재료 crossSourceOf 로 나눔(창 · 판 한 벌) · 카드 머리 걷기 전 경고 칩을 그래프 오른쪽 위로 - 곡선 판 가둠 = 편집 도구줄 아래 · 열린 횡단 판 왼쪽 · 뜬 종단 판 위 · 판 크기 바뀌면 다시 가둠 - 종단 지반선 · 계획선 pointer-events none · WF_State_Stale = 「다시 확인 필요 (앞 단계 변경)」 · ui-button → ui-btn - 시험 test_505_3_cross_card_text · test_505_3_cross_card_screen(5179) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
* `crossWithUphill` · `stationSection`)를 그 자리 측점 하나로 다시 부름 — [측점 추가] 뒤 묶음이 낼 측점과
|
||||
* 같은 틀 · 반경 · 확폭(기존 측점 + 그 자리로 셈). 파생 저장 · 캐시 · 묶음 상태는 안 건드림.
|
||||
* 지형 격자가 없거나 분석 지형 ≠ 설계 지형이면 null(부르는 쪽이 서버 미리 뜨기로).
|
||||
* 단면(`section`) = 그 측점에 지금 현황 형상 · 이름을 얹은 것 — 창이 「횡단」 판과 같은 B06 카드로 그림(505-3).
|
||||
* ========================================================================== */
|
||||
|
||||
import { pagePipeline } from "./design_pipeline_page";
|
||||
@@ -19,27 +20,31 @@ import { probeStation } from "./logic/design_pipeline_probe_station";
|
||||
import { groundStageDeps } from "../../B05_Profile/B05_Profile_Api_TerrainGrid";
|
||||
import { createGroundStage } from "../../B05_Profile/logic/B05_Profile_Pipeline_RouteGround";
|
||||
import {
|
||||
attachStatus,
|
||||
crossWithShapes,
|
||||
crossAtSeat,
|
||||
crossWithUphill,
|
||||
shapeStatus,
|
||||
stationSection,
|
||||
type CrossInput,
|
||||
} from "../../B05_Profile/logic/B05_Profile_Pipeline_Detail";
|
||||
import { fetchStructureTypes } from "../../B05_Profile/B05_Profile_Api_Structures";
|
||||
import { createCrossStage } from "../../B06_Section/logic/B06_Section_Cross_Stage";
|
||||
import { pipelineCrossAt } from "../../B05_Profile/B05_Profile_UI_RouteEdit_Cross";
|
||||
import type { CrossPreviewResponse } from "../../B05_Profile/B05_Profile_Api_Replan";
|
||||
import type { CrossSection } from "../../B06_Section/B06_Section_Api_Fetch";
|
||||
|
||||
const SAME_M = 1e-6;
|
||||
|
||||
/**
|
||||
* 지금 계획선(초안 포함)으로 그 누가거리 임시 횡단 — 묶음이 아직 안 셌거나 격자 없음 · 분석 지형 ≠ 설계 지형이면 null.
|
||||
* 지금 계획선(초안 포함)으로 그 누가거리 임시 횡단(선 그림 재료 · 형상 얹은 단면) — 묶음이 아직 안 셌거나
|
||||
* 격자 없음 · 분석 지형 ≠ 설계 지형이면 null.
|
||||
*/
|
||||
export async function pipelineCrossAtChainage(
|
||||
projectId: string,
|
||||
chainageM: number,
|
||||
intervalM: number,
|
||||
): Promise<CrossPreviewResponse | null> {
|
||||
): Promise<{ preview: CrossPreviewResponse; section: CrossSection } | null> {
|
||||
const pipe = pagePipeline(projectId);
|
||||
const route = pipe?.output("route");
|
||||
const profile = pipe?.output("profile");
|
||||
@@ -105,5 +110,9 @@ export async function pipelineCrossAtChainage(
|
||||
output: (name: StageName) =>
|
||||
name === "ground" ? { stations: [station] } : name === "cross" ? cross : null,
|
||||
} as unknown as Parameters<typeof pipelineCrossAt>[0];
|
||||
return pipelineCrossAt(one, chainageM, intervalM, undefined);
|
||||
const preview = pipelineCrossAt(one, chainageM, intervalM, undefined);
|
||||
if (!preview) return null;
|
||||
const section = stationSection(station, cross, {}, intervalM);
|
||||
attachStatus([section], status);
|
||||
return { preview, section };
|
||||
}
|
||||
|
||||
@@ -43,6 +43,14 @@
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
/* B06 카드 읽기 전용(505-3) — 칸 테두리 안 · 고름 강조 · 손가락 없음 */
|
||||
.b05-crossmodal__box > .b06-cross-card {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.b05-crossmodal__foot {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-caption);
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
* 열기 = 도구가 꺼진 때 원지반선 근처 누름(정수 m) · 추가 측점 세로선 누름. 창은 종단 칸 안에 겹침
|
||||
* (유토 배지처럼 · [창 분할]에 따라감 · 다시 그려도 남음). 내용 = 지금 계획선(초안 포함 · 묶음 같은 계산
|
||||
* `pipelineCrossAtChainage` · 368-4) — 격자 없음 · 분석 지형 다름이면 서버 미리 뜬 횡단(`preview-stations` ·
|
||||
* 저장 계획선 기준 · 글 한 줄) · 평면 우측 횡단과 같은 그림(`buildCrossSvg`) · 확대 · 이동 · 묶음 cross 차례마다 다시.
|
||||
* 저장 계획선 기준 · 글 한 줄) · 그림 = 평면 「횡단」 판과 같은 B06 카드(구조물 형상 · 포장 · 면적 · 505-3 ·
|
||||
* 서버 미리 뜬 것만 선 그림 `buildCrossSvg`) · 확대 · 이동 · 묶음 cross 차례마다 다시.
|
||||
* [측점 추가] = 추가 측점 초안(일반측점 · [저장] 한 번) · 같은 m 에 측점이 있으면 막음 · [삭제] = 추가
|
||||
* 측점일 때만 · [닫기]. 열리는 자리 가까이 = 원 · 세로 점선 · 측점 글(`mountCrossHint` · 490-1).
|
||||
* ========================================================================== */
|
||||
@@ -20,7 +21,7 @@ import {
|
||||
} from "../A00_Common/design/logic/design_profile_free";
|
||||
import { Y_WINDOW_ATTRS, Y_WINDOW_CLASS } from "../B06_Section/B06_Section_UI_Longitudinal";
|
||||
import { previewStructureSections } from "../B06_Section/B06_Section_Api_Fetch";
|
||||
import { pagePipeline } from "../A00_Common/design/design_pipeline_page";
|
||||
import { pagePipeline, pipelineDetail } from "../A00_Common/design/design_pipeline_page";
|
||||
import { pipelineCrossAtChainage } from "../A00_Common/design/design_pipeline_probe";
|
||||
import { stationLabel } from "../common_util/common_util_station";
|
||||
import { plusStationsOf, writePlusDraft } from "./B05_Profile_Api_PlusStations";
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
CROSS_PLOT_PAD,
|
||||
summarizeCross,
|
||||
} from "./B05_Profile_UI_RouteEdit_Cross_Draw";
|
||||
import { crossSourceOf, routeCrossCard } from "./B05_Profile_UI_RouteEdit_Cross_Card";
|
||||
import {
|
||||
crossPickAt,
|
||||
groundElevationAt,
|
||||
@@ -196,9 +198,9 @@ function createCrossModal(host: HTMLElement): CrossModal {
|
||||
<strong>임시 횡단</strong>
|
||||
<span class="b05-crossmodal__station"></span>
|
||||
<div class="b05-crossmodal__actions">
|
||||
<button type="button" class="ui-button" data-act="add">측점 추가</button>
|
||||
<button type="button" class="ui-button" data-act="remove">삭제</button>
|
||||
<button type="button" class="ui-button" data-act="close">닫기</button>
|
||||
<button type="button" class="ui-btn ui-btn--ghost ui-btn--sm" data-act="add">측점 추가</button>
|
||||
<button type="button" class="ui-btn ui-btn--ghost ui-btn--sm" data-act="remove">삭제</button>
|
||||
<button type="button" class="ui-btn ui-btn--ghost ui-btn--sm" data-act="close">닫기</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="b05-crossmodal__box"></div>
|
||||
@@ -241,7 +243,7 @@ function createCrossModal(host: HTMLElement): CrossModal {
|
||||
const now = await pipelineCrossAtChainage(projectId(), m, current?.intervalM ?? 20).catch(
|
||||
() => null,
|
||||
);
|
||||
const [cross] = now ? [now] : await previewStructureSections(projectId(), [m]);
|
||||
const [cross] = now ? [now.preview] : await previewStructureSections(projectId(), [m]);
|
||||
if (mine !== turn) return;
|
||||
if (!cross) {
|
||||
foot.textContent = "이 자리의 횡단을 뜨지 못했습니다.";
|
||||
@@ -249,21 +251,37 @@ function createCrossModal(host: HTMLElement): CrossModal {
|
||||
}
|
||||
const preview = cross as unknown as CrossPreviewResponse;
|
||||
const rect = box.getBoundingClientRect();
|
||||
const plot = buildCrossSvg(
|
||||
preview,
|
||||
Math.round(rect.width) || 320,
|
||||
Math.round(rect.height) || 180,
|
||||
);
|
||||
box.replaceChildren(plot.svg);
|
||||
attachSvgZoomPan({
|
||||
svg: plot.svg,
|
||||
layer: plot.layer,
|
||||
widthPx: plot.width,
|
||||
heightPx: plot.height,
|
||||
pad: CROSS_PLOT_PAD,
|
||||
content: plot.content,
|
||||
wheelZoom: true,
|
||||
});
|
||||
const [width, height] = [Math.round(rect.width) || 320, Math.round(rect.height) || 180];
|
||||
if (now) {
|
||||
// 「횡단」 판과 같은 B06 카드(505-3) — 그림 밖 높이만큼 줄여 한 번 더 지어 칸에 맞춤
|
||||
const detail = pipelineDetail(projectId());
|
||||
const source = crossSourceOf(
|
||||
projectId(),
|
||||
now.section,
|
||||
detail?.cross_sections ?? [now.section],
|
||||
detail?.longitudinal.design_profiles,
|
||||
"modal",
|
||||
);
|
||||
const build = (plotHeight: number): HTMLElement => {
|
||||
const card = routeCrossCard(source, current?.intervalM ?? 20, width, plotHeight);
|
||||
box.replaceChildren(card);
|
||||
return card;
|
||||
};
|
||||
const extra = build(height).offsetHeight - height;
|
||||
if (extra > 0) build(Math.max(120, height - extra));
|
||||
} else {
|
||||
const plot = buildCrossSvg(preview, width, height);
|
||||
box.replaceChildren(plot.svg);
|
||||
attachSvgZoomPan({
|
||||
svg: plot.svg,
|
||||
layer: plot.layer,
|
||||
widthPx: plot.width,
|
||||
heightPx: plot.height,
|
||||
pad: CROSS_PLOT_PAD,
|
||||
content: plot.content,
|
||||
wheelZoom: true,
|
||||
});
|
||||
}
|
||||
const planZ = Number(
|
||||
(preview.design as { design_elevation_m?: unknown } | null)?.design_elevation_m,
|
||||
);
|
||||
|
||||
@@ -15,7 +15,7 @@ import "../B06_Section/B06_Section_UI_Style_Cross_Controls.css";
|
||||
import { pipelineDetail } from "../A00_Common/design/design_pipeline_page";
|
||||
import type { DesignPipeline } from "../A00_Common/design/logic/design_pipeline";
|
||||
import { nearestWithin } from "../A00_Common/design/design_selection";
|
||||
import type { CrossSection } from "../B06_Section/B06_Section_Api_Fetch";
|
||||
import type { CrossSection, DesignProfile } from "../B06_Section/B06_Section_Api_Fetch";
|
||||
import { peekSectionDetail } from "../B06_Section/B06_Section_Section_Store";
|
||||
import { displayHalfWidth } from "../B06_Section/B06_Section_Display_Width";
|
||||
import { pageRouteId } from "../A00_Common/design/design_page_route";
|
||||
@@ -74,7 +74,17 @@ export function routeCrossSource(
|
||||
const sections = detail?.cross_sections ?? [];
|
||||
const found = nearestWithin(sections, chainageM, (entry) => entry.chainage_m);
|
||||
if (!detail || !found) return null;
|
||||
const profiles = detail.longitudinal.design_profiles;
|
||||
return crossSourceOf(projectId, found, sections, detail.longitudinal.design_profiles, pane);
|
||||
}
|
||||
|
||||
/** 단면 하나 → 판 재료(사본) — 이웃 단면 · 계획선은 연동 관 · 반폭 · 계획고 재료(종단 임시 횡단 창도 씀 · 505-3). */
|
||||
export function crossSourceOf(
|
||||
projectId: string,
|
||||
found: CrossSection,
|
||||
sections: readonly CrossSection[],
|
||||
profiles: DesignProfile[] | undefined,
|
||||
pane: string,
|
||||
): RouteCrossSource {
|
||||
const planZ = designElevationAt(profiles, found.chainage_m);
|
||||
return {
|
||||
section: {
|
||||
@@ -146,6 +156,14 @@ export function routeCrossCard(
|
||||
box,
|
||||
);
|
||||
// 그래프만(393-3) — 카드 머리 · 줌 단추 · 발(암 경계 · 요약) 걷음 · 벽 · 선 · 면적 고름 막음
|
||||
// 경고 칩(빈칸 · 기본값 · 사면 열림)만 그래프 오른쪽 위에 겹쳐 남김(505-3)
|
||||
const chips = card.querySelectorAll(":scope > header .b06-cross-card__warning");
|
||||
if (chips.length) {
|
||||
const box = document.createElement("div");
|
||||
box.className = "b05-cross-chips";
|
||||
box.append(...chips);
|
||||
card.append(box);
|
||||
}
|
||||
card
|
||||
.querySelectorAll(":scope > header, :scope > footer, .b06-cross-card__zoom")
|
||||
.forEach((el) => el.remove());
|
||||
|
||||
@@ -180,6 +180,13 @@ export function createCurveBar(params: CurveBarParams): CurveBar {
|
||||
);
|
||||
}
|
||||
|
||||
// 가둠 판(도구줄 · 횡단 판 · 종단 판)이 열리고 닫히고 크기가 바뀌면 다시 가둠(505-3) · 편집 창이 닫히면 끊음
|
||||
const watched = new Set<Element>();
|
||||
const refit = new ResizeObserver(() => {
|
||||
if (!params.canvas.isConnected) return refit.disconnect();
|
||||
sync();
|
||||
});
|
||||
|
||||
/** 고른 자리에 맞춰 라벨을 옮겨 그린다. 끝점은 곡선이 없어 **고정 단추만** 낸다(계획서 22-5). */
|
||||
function sync(): void {
|
||||
const {
|
||||
@@ -199,11 +206,25 @@ export function createCurveBar(params: CurveBarParams): CurveBar {
|
||||
return;
|
||||
}
|
||||
const rect = params.canvas.getBoundingClientRect();
|
||||
// 가둠 = 도구줄 아래 · 열린 횡단 판 왼쪽 · 뜬 종단 판 위(505-3 · 판 밑에 깔려 가리지 않게)
|
||||
const main = params.canvas.closest(".design-page__main") ?? params.canvas.parentElement;
|
||||
const box = (selector: string): DOMRect | null => {
|
||||
const element = main?.querySelector(selector);
|
||||
if (element && !watched.has(element)) {
|
||||
watched.add(element);
|
||||
refit.observe(element);
|
||||
}
|
||||
const found = element?.getBoundingClientRect();
|
||||
return found && found.width > 0 && found.height > 0 ? found : null;
|
||||
};
|
||||
const tools = box(".b05-routeedit__tools");
|
||||
const side = box(".b05-routeedit__side-wrap");
|
||||
const profile = box(".b05-route-profile");
|
||||
const bounds = {
|
||||
left: rect.left + 8,
|
||||
top: rect.top + 8,
|
||||
right: rect.right - 8,
|
||||
bottom: rect.bottom - 8,
|
||||
top: Math.max(rect.top, tools?.bottom ?? rect.top) + 8,
|
||||
right: Math.min(rect.right, side?.left ?? rect.right) - 8,
|
||||
bottom: Math.min(rect.bottom, profile?.top ?? rect.bottom) - 8,
|
||||
};
|
||||
const last = planned.length - 1;
|
||||
if (picked === 0 || picked === last) {
|
||||
|
||||
@@ -288,6 +288,32 @@
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* 카드 경고 칩(빈칸 · 기본값 · 사면 열림) — 머리를 걷어도 그래프 오른쪽 위에 겹쳐 남김(505-3) · 판 · 임시 횡단 창 같이 씀 */
|
||||
.b06-cross-card:has(> .b05-cross-chips) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.b05-cross-chips {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 2px;
|
||||
max-width: 70%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.b05-cross-chips > .b06-cross-card__warning {
|
||||
padding: 0 4px;
|
||||
border-radius: var(--radius-4, 4px);
|
||||
background: color-mix(in srgb, var(--color-surface-raised) 85%, transparent);
|
||||
font-size: var(--text-caption);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.b05-routeedit__cross-foot:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ export function stationSection(
|
||||
|
||||
/** 측점마다 그림 자료 · 카드 이름 = 지금 현황을 그 자리에 다시 얹음(캐시 `withDraftShapes` · 이름 = 측점 계획
|
||||
* 제목 · 457) — 옛 측점 그림을 옮기지 않음. */
|
||||
function attachStatus(sections: CrossSection[], status: ShapeStatus): void {
|
||||
export function attachStatus(sections: CrossSection[], status: ShapeStatus): void {
|
||||
const { pipes, structures, types } = status;
|
||||
attachShapes(sections as unknown as Array<Record<string, unknown>>, pipes, structures, types);
|
||||
const planned = planStations(pipes, structures, types, registryDefaults(types));
|
||||
|
||||
@@ -436,6 +436,12 @@ html.is-bm-route .b06-section__notice-bar {
|
||||
stroke-width: 2.5;
|
||||
}
|
||||
|
||||
/* 지반선 · 계획선은 누름을 통과 — 겹친 측점선 누름이 측점 고름 · 임시 횡단으로 감(505-3) */
|
||||
.b06-chart__profile,
|
||||
.b06-chart__design-profile {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.b06-chart__cross-profile {
|
||||
fill: none;
|
||||
stroke: var(--color-chart-1);
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
"""PLAN 505-3 화면 — 자기 포트(5179) Playwright 머리 없는 브라우저 · 서버가 떠 있을 때만 · 검증_sub4.
|
||||
|
||||
- 종단 측점선 · 원지반선 겹친 자리 누름 = 측점선이 받음(지반선 누름 통과) → 임시 횡단 창 = B06 카드 + 「⚠ 배관 관경 빈칸」 칩
|
||||
- 관경 800(세션만 · [저장] 안 함) → 창 카드에 관 형상 · Ctrl+휠 확대가 창 칸 밖으로 안 넘침 · 밝음 · 어두움 한 장씩
|
||||
- 곡선 판 = 도구줄 아래 · 열린 횡단 판 왼쪽(판을 나중에 열어도 비킴)
|
||||
- 계정 화면 취향(종단 · 횡단 판 열림 · 높이)만 시험 앞 값으로 되돌림
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_505_3_cross_card_screen.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
BASE = "http://localhost:5179"
|
||||
PROJECT = "c725d747-50ae-46b0-87ce-4d4e56a53cf3" # 검증_sub4(OWNERS.md)
|
||||
SHOTS = ROOT / "tmp" / "shots_505_3"
|
||||
PIPE_M = 388 # 검증_sub4 배관 하나(관경 빈칸 · 19+8)
|
||||
PREFS = "/api/dashboard/me/ui-prefs"
|
||||
# 이 시험이 건드리는 취향만 되돌림 — 계정을 다른 창도 같이 써서 통째로 덮으면 남의 값이 사라짐
|
||||
TOUCHED = ("profile-collapsed", "profile-height", "b05-routeedit-cross-v2")
|
||||
|
||||
|
||||
def _up() -> bool:
|
||||
try:
|
||||
urllib.request.urlopen(BASE, timeout=3)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
pytestmark = pytest.mark.skipif(not _up(), reason="5179 서버 없음")
|
||||
|
||||
# 측점 m 을 가로 가운데로 · 그 측점선과 원지반선 교점(화면 좌표) · 그 자리 맨 위 요소
|
||||
JS_SCROLL = r"""(m) => {
|
||||
const marker = [...document.querySelectorAll('.b06-chart__station')]
|
||||
.find((e) => Math.abs(+e.getAttribute('data-chainage') - m) < 1e-6);
|
||||
const body = marker.closest('.b05-route-profile__body');
|
||||
body.scrollLeft = Math.max(0, +marker.querySelector('line').getAttribute('x1') - body.clientWidth / 2);
|
||||
}"""
|
||||
JS_POINT = r"""(m) => {
|
||||
const marker = [...document.querySelectorAll('.b06-chart__station')]
|
||||
.find((e) => Math.abs(+e.getAttribute('data-chainage') - m) < 1e-6);
|
||||
const line = marker.querySelector('.b06-chart__station-line');
|
||||
const svg = line.ownerSVGElement;
|
||||
const x = +line.getAttribute('x1');
|
||||
const poly = svg.querySelector('.b06-chart__profile');
|
||||
const pts = poly.getAttribute('points').trim().split(/\s+/).map((s) => s.split(',').map(Number));
|
||||
const k = Math.max(1, pts.findIndex((p) => p[0] >= x));
|
||||
const [a, b] = [pts[k - 1], pts[k]];
|
||||
const pt = svg.createSVGPoint();
|
||||
pt.x = x;
|
||||
pt.y = a[1] + ((b[1] - a[1]) * (x - a[0])) / (b[0] - a[0] || 1);
|
||||
const s = pt.matrixTransform(poly.getScreenCTM());
|
||||
return { x: s.x, y: s.y, hit: document.elementFromPoint(s.x, s.y)?.getAttribute('class') };
|
||||
}"""
|
||||
JS_MODAL = r"""() => {
|
||||
const r = document.querySelector('.b05-crossmodal');
|
||||
const card = r.querySelector('.b06-cross-card');
|
||||
const box = r.querySelector('.b05-crossmodal__box').getBoundingClientRect();
|
||||
const svg = card?.querySelector('svg').getBoundingClientRect();
|
||||
return {
|
||||
station: r.querySelector('.b05-crossmodal__station').textContent,
|
||||
card: !!card,
|
||||
chips: [...r.querySelectorAll('.b05-cross-chips > *')].map((e) => e.textContent),
|
||||
pipes: card ? card.querySelectorAll('.b06-chart__culvert-pipe').length : 0,
|
||||
inside: !!svg && svg.top >= box.top && svg.bottom <= box.bottom + 1 && svg.right <= box.right + 1,
|
||||
width: Math.round(r.getBoundingClientRect().width),
|
||||
buttons: [...r.querySelectorAll('.b05-crossmodal__actions button')].map((e) => e.className),
|
||||
scale: [...(card?.querySelectorAll('svg g[transform]') ?? [])]
|
||||
.map((g) => g.getAttribute('transform')).find((t) => /^translate\([^)]*\) scale\(/.test(t)) ?? '',
|
||||
};
|
||||
}"""
|
||||
JS_RECTS = r"""() => {
|
||||
const q = (s) => [...document.querySelectorAll(s)].find((x) => x.getClientRects().length);
|
||||
const r = (s) => { const o = q(s)?.getBoundingClientRect(); return o ? [o.left, o.top, o.right, o.bottom] : null; };
|
||||
return { label: r('.b05-routeedit__label'), tools: r('.b05-routeedit__tools'), side: r('.b05-routeedit__side-wrap') };
|
||||
}"""
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def page():
|
||||
sync = pytest.importorskip("playwright.sync_api")
|
||||
cred = json.loads((ROOT / "tmp" / "aislo_cred.json").read_text(encoding="utf-8"))
|
||||
with sync.sync_playwright() as play:
|
||||
chrome = play.chromium.launch(channel="msedge", headless=True)
|
||||
ctx = chrome.new_context(viewport={"width": 1600, "height": 1000})
|
||||
login = {k: cred[k] for k in ("email", "password")}
|
||||
assert ctx.request.post(f"{BASE}/api/auth/login/request", data=login, timeout=60000).ok
|
||||
prefs = ctx.request.get(f"{BASE}{PREFS}").json().get("prefs", {})
|
||||
tab = ctx.new_page()
|
||||
tab.add_init_script(f"localStorage.setItem('frd_current_project_id', '{PROJECT}')")
|
||||
tab.goto(f"{BASE}/#/b05-profile", wait_until="domcontentloaded")
|
||||
tab.wait_for_function("() => !!window.__aisloPipeline", timeout=90000)
|
||||
tab.wait_for_timeout(8000)
|
||||
SHOTS.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
yield tab
|
||||
finally:
|
||||
tab.wait_for_timeout(2000) # 취향 올리기(1.5 s 모음) 끝난 뒤 되돌림
|
||||
now = ctx.request.get(f"{BASE}{PREFS}").json().get("prefs", {})
|
||||
for key in (f"aislo:pref:{name}" for name in TOUCHED):
|
||||
if key in prefs:
|
||||
now[key] = prefs[key]
|
||||
else:
|
||||
now.pop(key, None)
|
||||
assert ctx.request.put(f"{BASE}{PREFS}", data={"prefs": now}).ok
|
||||
chrome.close()
|
||||
|
||||
|
||||
def _profile(page, open_: bool) -> None:
|
||||
height = page.evaluate(
|
||||
"() => document.querySelector('.b05-route-profile')?.getBoundingClientRect().height ?? 0"
|
||||
)
|
||||
if (height > 100) != open_: # 접힘 = 높이 1 px(측점 요소는 DOM 에 남음)
|
||||
page.get_by_text("종단", exact=True).last.click()
|
||||
page.wait_for_timeout(3000)
|
||||
|
||||
|
||||
def _open_modal(page) -> dict:
|
||||
_profile(page, True)
|
||||
page.evaluate(JS_SCROLL, PIPE_M)
|
||||
page.wait_for_timeout(800)
|
||||
at = page.evaluate(JS_POINT, PIPE_M)
|
||||
assert "b06-chart__station-line" in (at["hit"] or ""), at # 지반선이 아니라 측점선이 받음
|
||||
page.mouse.move(at["x"], at["y"])
|
||||
page.wait_for_timeout(300)
|
||||
page.mouse.click(at["x"], at["y"])
|
||||
page.wait_for_function(
|
||||
"() => document.querySelector('.b05-crossmodal .b06-cross-card')", timeout=30000
|
||||
)
|
||||
page.wait_for_timeout(1500)
|
||||
return page.evaluate(JS_MODAL)
|
||||
|
||||
|
||||
def test_0_곡선_판_도구줄_횡단판_비킴(page):
|
||||
_profile(page, False)
|
||||
rects = page.evaluate(JS_RECTS)
|
||||
if rects["side"][2] - rects["side"][0] > 100: # 횡단 판 접고 시작
|
||||
page.locator(".b05-routeedit__side-wrap").get_by_text("횡단").first.click()
|
||||
page.wait_for_timeout(2000)
|
||||
shown = "() => [...document.querySelectorAll('.b05-routeedit__label')].some((x) => !x.hidden)"
|
||||
for _ in range(3): # 첫 누름이 앞 고름(구조물) 풂으로 갈 수 있음
|
||||
page.mouse.click(1199, 296) # 검증_sub4 첫 화면의 14번째 꺾임점
|
||||
page.wait_for_timeout(1500)
|
||||
if page.evaluate(shown):
|
||||
break
|
||||
page.screenshot(path=str(SHOTS / "curve_pick.png"))
|
||||
assert page.evaluate(shown)
|
||||
# 지도를 위 · 오른쪽으로 밀어 꺾임점을 도구줄 · 오른쪽 끝 밑으로
|
||||
page.mouse.move(800, 650)
|
||||
page.mouse.down(button="middle")
|
||||
page.mouse.move(1050, 380, steps=16)
|
||||
page.mouse.up(button="middle")
|
||||
page.wait_for_timeout(1200)
|
||||
rects = page.evaluate(JS_RECTS)
|
||||
assert rects["label"][1] >= rects["tools"][3] + 8 - 0.5
|
||||
page.locator(".b05-routeedit__side-wrap").get_by_text("횡단").first.click()
|
||||
page.wait_for_timeout(2500)
|
||||
rects = page.evaluate(JS_RECTS)
|
||||
assert rects["label"][2] <= rects["side"][0] - 8 + 0.5 # 판을 나중에 열어도 비킴
|
||||
assert rects["label"][1] >= rects["tools"][3] + 8 - 0.5
|
||||
page.screenshot(path=str(SHOTS / "curve_light.png"))
|
||||
page.evaluate("document.documentElement.setAttribute('data-theme', 'dark')")
|
||||
page.screenshot(path=str(SHOTS / "curve_dark.png"))
|
||||
page.evaluate("document.documentElement.setAttribute('data-theme', 'light')")
|
||||
page.keyboard.press("Escape") # 꺾임점 고름 풂
|
||||
|
||||
|
||||
def test_1_빈칸_칩_카드(page):
|
||||
modal = _open_modal(page)
|
||||
assert modal["station"] == "19+8" and modal["card"]
|
||||
assert modal["chips"] == ["⚠ 배관 관경 빈칸"]
|
||||
assert modal["pipes"] == 0 and modal["width"] == 380
|
||||
assert all("ui-btn ui-btn--ghost ui-btn--sm" in name for name in modal["buttons"])
|
||||
page.locator(".b05-crossmodal").get_by_text("닫기").click()
|
||||
|
||||
|
||||
def test_2_횡단_판_칩_관경_넣으면_형상_확대_밝음_어두움(page):
|
||||
_profile(page, False)
|
||||
page.locator(".b05-route__irregular-item", has_text="19+8").first.click()
|
||||
page.wait_for_timeout(3000)
|
||||
chips = page.eval_on_selector_all(
|
||||
".b05-routeedit__cross .b05-cross-chips > *", "(els) => els.map((e) => e.textContent)"
|
||||
)
|
||||
assert "⚠ 배관 관경 빈칸" in chips # 「횡단」 판 = 카드 머리를 걷어도 칩은 그래프 오른쪽 위
|
||||
page.locator(".b05-routeedit__cross").first.screenshot(path=str(SHOTS / "panel_chip.png"))
|
||||
if not page.locator("select:visible").filter(has=page.locator("option[value='800']")).count():
|
||||
page.get_by_text("구조물 배치", exact=True).click()
|
||||
page.wait_for_timeout(1500)
|
||||
page.locator("select:visible").filter(
|
||||
has=page.locator("option[value='800']")
|
||||
).first.select_option("800") # 세션 초안만 — [저장] 안 누름
|
||||
page.wait_for_timeout(5000)
|
||||
modal = _open_modal(page)
|
||||
assert modal["chips"] == [] and modal["pipes"] > 0
|
||||
shot = page.locator(".b05-crossmodal")
|
||||
shot.screenshot(path=str(SHOTS / "modal_light.png"))
|
||||
page.evaluate("document.documentElement.setAttribute('data-theme', 'dark')")
|
||||
shot.screenshot(path=str(SHOTS / "modal_dark.png"))
|
||||
page.evaluate("document.documentElement.setAttribute('data-theme', 'light')")
|
||||
box = page.locator(".b05-crossmodal__box").bounding_box()
|
||||
cx, cy = box["x"] + box["width"] / 2, box["y"] + box["height"] / 2
|
||||
page.evaluate(
|
||||
"([x, y]) => document.elementFromPoint(x, y).dispatchEvent(new WheelEvent('wheel',"
|
||||
" {deltaY: 300, ctrlKey: true, clientX: x, clientY: y, bubbles: true, cancelable: true}))",
|
||||
[cx, cy],
|
||||
)
|
||||
page.wait_for_timeout(800)
|
||||
zoomed = page.evaluate(JS_MODAL)
|
||||
assert "scale(1.15)" in zoomed["scale"] and zoomed["inside"]
|
||||
page.locator(".b05-crossmodal").get_by_text("닫기").click()
|
||||
@@ -0,0 +1,84 @@
|
||||
"""505-3 B05 그림 · 글 — 임시 횡단 창 = B06 카드(형상 얹은 단면) · 빈칸 칩 그래프 오른쪽 위 · 곡선 판 가둠 ·
|
||||
지반선 · 계획선 누름 통과 · STALE 글 · 창 단추 공용 꼴.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_505_3_cross_card_text.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def _read(*parts: str) -> str:
|
||||
return ROOT.joinpath(*parts).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_측점_뜨기는_형상_얹은_단면도_돌려줌() -> None:
|
||||
probe = _read("A00_Common", "design", "design_pipeline_probe.ts")
|
||||
assert "Promise<{ preview: CrossPreviewResponse; section: CrossSection } | null>" in probe
|
||||
assert "const section = stationSection(station, cross, {}, intervalM);" in probe
|
||||
assert "attachStatus([section], status);" in probe
|
||||
detail = _read("B05_Profile", "logic", "B05_Profile_Pipeline_Detail.ts")
|
||||
assert "export function attachStatus(sections: CrossSection[], status: ShapeStatus)" in detail
|
||||
|
||||
|
||||
def test_임시_횡단_창은_B06_카드() -> None:
|
||||
modal = _read("B05_Profile", "B05_Profile_UI_Profile_CrossModal.ts")
|
||||
assert "crossSourceOf(" in modal and "routeCrossCard(source," in modal
|
||||
assert "now ? [now.preview] : await previewStructureSections(projectId(), [m])" in modal
|
||||
# 서버 미리 뜬 것(분석 지형 다름)만 선 그림
|
||||
assert "buildCrossSvg(preview, width, height)" in modal
|
||||
assert 'class="ui-button"' not in modal
|
||||
assert modal.count('class="ui-btn ui-btn--ghost ui-btn--sm" data-act=') == 3
|
||||
css = _read("B05_Profile", "B05_Profile_UI_Profile_CrossModal.css")
|
||||
assert ".b05-crossmodal__box > .b06-cross-card {" in css
|
||||
|
||||
|
||||
def test_판_재료_한_벌() -> None:
|
||||
card = _read("B05_Profile", "B05_Profile_UI_RouteEdit_Cross_Card.ts")
|
||||
assert (
|
||||
"return crossSourceOf(projectId, found, sections, detail.longitudinal.design_profiles, pane);"
|
||||
in card
|
||||
)
|
||||
assert card.count("export function crossSourceOf(") == 1
|
||||
|
||||
|
||||
def test_빈칸_칩은_머리_걷기_전에_꺼냄() -> None:
|
||||
card = _read("B05_Profile", "B05_Profile_UI_RouteEdit_Cross_Card.ts")
|
||||
chips = card.index('":scope > header .b06-cross-card__warning"')
|
||||
remove = card.index(':scope > header, :scope > footer, .b06-cross-card__zoom"')
|
||||
assert chips < remove
|
||||
assert 'box.className = "b05-cross-chips";' in card
|
||||
css = _read("B05_Profile", "B05_Profile_UI_Style_RouteEdit.css")
|
||||
rule = css[css.index(".b05-cross-chips {") :]
|
||||
assert "top: 4px;" in rule[:200] and "right: 4px;" in rule[:200]
|
||||
assert ".b06-cross-card:has(> .b05-cross-chips) {\n position: relative;" in css
|
||||
|
||||
|
||||
def test_곡선_판_가둠_도구줄_횡단판_종단판() -> None:
|
||||
bar = _read("B05_Profile", "B05_Profile_UI_RouteEdit_CurveBar.ts")
|
||||
for selector in (
|
||||
".b05-routeedit__tools",
|
||||
".b05-routeedit__side-wrap",
|
||||
".b05-route-profile",
|
||||
):
|
||||
assert f'box("{selector}")' in bar
|
||||
assert "top: Math.max(rect.top, tools?.bottom ?? rect.top) + 8," in bar
|
||||
assert "right: Math.min(rect.right, side?.left ?? rect.right) - 8," in bar
|
||||
assert "bottom: Math.min(rect.bottom, profile?.top ?? rect.bottom) - 8," in bar
|
||||
|
||||
|
||||
def test_지반선_계획선_누름_통과() -> None:
|
||||
css = _read("B06_Section", "B06_Section_UI_Style_Cross.css")
|
||||
assert ".b06-chart__profile,\n.b06-chart__design-profile {\n pointer-events: none;" in css
|
||||
|
||||
|
||||
def test_STALE_글_앞_단계() -> None:
|
||||
locale = _read("ui_template", "ui_template_locale_common.ts")
|
||||
assert (
|
||||
'WF_State_Stale: ["다시 확인 필요 (앞 단계 변경)", "Stale (an earlier step changed)"],'
|
||||
in locale
|
||||
)
|
||||
assert "하위 단계" not in locale
|
||||
@@ -85,7 +85,7 @@ export const ui_locales_common = {
|
||||
WF_Step_DesignDetail: ["상세설계", "Detail Design"],
|
||||
WF_Step_Quantity: ["수량산출", "Quantity"],
|
||||
WF_Step_Estimation: ["원가계산", "Cost Estimate"],
|
||||
WF_State_Stale: ["무효화됨 (하위 단계 변경)", "Stale (invalidated by downstream changes)"],
|
||||
WF_State_Stale: ["다시 확인 필요 (앞 단계 변경)", "Stale (an earlier step changed)"],
|
||||
WF_State_Failed: ["실패", "Failed"],
|
||||
WF_State_Complete: ["완료", "Complete"],
|
||||
WF_State_InProgress: ["진행 중", "In Progress"],
|
||||
|
||||
Reference in New Issue
Block a user