Merge remote-tracking branch 'origin/sub_laptop_2' into main_laptop_1

This commit is contained in:
2026-10-10 08:48:50 +09:00
11 changed files with 291 additions and 48 deletions
+41 -11
View File
@@ -2,7 +2,8 @@
* B06_Section_UI_Cross_Detail.ts
* 횡단 카드 측구 줄 맨 오른쪽 [상세] → 모달(138-4).
*
* 위 = 카드 그림 복제(어두운 바탕 · CAD 꼴 · 남은 높이를 채움) ·
* 위 = 카드 그리기 함수로 칸 크기에 맞춰 새로 그린 그림의 복제(173-4 · 테마 색 · 칸 가득 ·
* 칸 크기가 바뀌면 다시 그림) ·
* 가운데 = 도면 표 위 글(`design.structure_note` · 세션 초안 → [저장]이 patch 로 내보냄 ·
* B07 횡단도 수량표 바로 위에 줄마다 찍힘 — `_Engine_Cad_Table.cross_note_lines`) ·
* 아래 = B07 횡단 수량표(값 · 칸 배치 모두 서버 `cross-quantity-table` 한 길 · 캐시 설계로 · 열 때 부름).
@@ -35,6 +36,8 @@ export interface CrossDetailHost {
projectId: string;
routeId: () => number | null;
detail: () => SectionDetailResponse | null;
/** 칸 크기(W × H px)로 새로 그린 카드 svg(173-4 · 뷰 `detailChart`). */
chart: (stationId: string, widthPx: number, heightPx: number) => SVGSVGElement | null;
}
let host: CrossDetailHost | null = null;
@@ -127,9 +130,8 @@ export function attachDetailZoom(
const [, , width, height] = (clone.getAttribute("viewBox") ?? "").split(/[\s,]+/).map(Number);
const layer = document.createElementNS("http://www.w3.org/2000/svg", "g");
layer.append(...clone.childNodes);
// 칸 = viewBox(칸 크기로 그림 · 173-4) → 커서 고정 · 팬 속도 정확
clone.append(layer);
// 칸 비율 = viewBox 비율 → 커서 고정 · 팬 속도 정확(공용 조각이 칸 크기로 환산)
clone.style.setProperty("--ratio", String(width / height));
return attachSvgZoomPan({
svg: clone,
layer,
@@ -153,13 +155,33 @@ interface OpenDetail {
paint: () => void;
loadTable: () => void;
close: () => void;
stop: () => void;
}
let current: OpenDetail | null = null;
let tableTimer = 0;
/** 모달 그림을 그리는 동안 — 카드 그리기가 부르는 `refreshCrossDetail` 이 다시 그리기를 걸지 않게. */
let drawing = false;
const cardOf = (section: CrossSection): HTMLElement | null =>
document.getElementById(`cross-${section.station_id}`);
/** 칸 크기로 새로 그림 → 복제(듣개 없음) · clip id 를 카드와 가름(같은 id 면 카드 clip 을 씀). */
function drawChart(section: CrossSection, widthPx: number, heightPx: number): SVGSVGElement | null {
drawing = true;
try {
const svg = host?.chart(section.station_id, widthPx, heightPx);
if (!svg) return null;
const clone = svg.cloneNode(true) as SVGSVGElement;
clone.querySelectorAll("clipPath[id]").forEach((clip) => {
const id = clip.id;
clip.id = `b06-detail-${id}`;
clone
.querySelectorAll(`[clip-path="url(#${id})"]`)
.forEach((el) => el.setAttribute("clip-path", `url(#${clip.id})`));
});
return clone;
} finally {
drawing = false;
}
}
function titleOf(section: CrossSection): string {
const stations = host?.detail()?.longitudinal.stations ?? [];
@@ -209,18 +231,24 @@ export function openCrossDetail(section: CrossSection): void {
showPicked();
});
// 모달 배율 — 다시 떠도(재복제) 이어 감
// 모달 배율 — 다시 그려도 이어 감 · 칸 크기가 바뀌면 원복(173-4)
let zoom: ZoomPanState | undefined;
let drawnSize = "";
const resize = new ResizeObserver(() => state.paint());
resize.observe(cad);
const state: OpenDetail = {
stationId: section.station_id,
section,
paint() {
const card = cardOf(state.section);
const svg = card?.querySelector("svg.b06-section__chart");
if (!svg) cad.replaceChildren(document.createTextNode("그림 없음"));
const { clientWidth: width, clientHeight: height } = cad;
if (width < 1 || height < 1) return;
const size = `${width}x${height}`;
if (size !== drawnSize) zoom = undefined;
drawnSize = size;
const clone = drawChart(state.section, width, height);
if (!clone) cad.replaceChildren(document.createTextNode("그림 없음"));
else {
const clone = svg.cloneNode(true) as SVGSVGElement;
const handle = attachDetailZoom(clone, zoom, (next) => (zoom = next));
cad.replaceChildren(clone, buildZoomControls(handle));
}
@@ -256,6 +284,7 @@ export function openCrossDetail(section: CrossSection): void {
forget();
close();
},
stop: () => resize.disconnect(),
};
current = state;
tableBox.textContent = "수량표 불러오는 중…";
@@ -273,6 +302,7 @@ export function openCrossDetail(section: CrossSection): void {
/** 열린 모달 기억을 지움(닫힐 때). */
function forget(): void {
window.clearTimeout(tableTimer);
current?.stop();
current = null;
}
@@ -282,7 +312,7 @@ function forget(): void {
*/
export function refreshCrossDetail(section: CrossSection): void {
const open = current;
if (!open || open.stationId !== section.station_id) return;
if (drawing || !open || open.stationId !== section.station_id) return;
open.section = section;
requestAnimationFrame(() => current === open && open.paint());
window.clearTimeout(tableTimer);
@@ -77,6 +77,24 @@ export function naturalHeightAtWidth(
return (base.naturalHeight - fixed) * (inner(toWidthPx) / inner(fromWidthPx)) + fixed;
}
/**
* 칸(카드 폭 W 로 잰 기하 × 높이 H px) 가득 그리기(173-4 [상세]) — 자연 높이가 H 보다 크면 가로 범위를 좌우 같이 넓혀
* m당 px 를 줄임(넓힌 자리 = 샘플 밖 빈 자리). 작으면 그대로(`crossPlotFromBase` 가 세로를 늘림).
*/
export function fitPlotBase(base: CrossPlotBase, heightPx: number): CrossPlotBase {
const fixed = CROSS_PAD.top + CROSS_PAD.bottom + AREA_OVERLAY_HEADROOM_PX;
if (base.naturalHeight <= heightPx || heightPx <= fixed) return base;
const k = (base.naturalHeight - fixed) / (heightPx - fixed);
const grow = ((base.maxOffset - base.minOffset) * (k - 1)) / 2;
return {
...base,
minOffset: base.minOffset - grow,
maxOffset: base.maxOffset + grow,
pixelsPerMeter: base.pixelsPerMeter / k,
naturalHeight: heightPx,
};
}
/** 재 둔 기하에 행 높이만 얹는다 — 다시 계산하지 않는다. */
export function crossPlotFromBase(base: CrossPlotBase, forcedHeightPx?: number): CrossPlotMetrics {
const heightPx = forcedHeightPx ?? Math.max(base.naturalHeight, CROSS_HEIGHT);
@@ -43,8 +43,14 @@ export interface DesignSyncController {
export function createDesignSync(ctx: DesignSyncContext): DesignSyncController {
const { projectId } = ctx;
// 횡단 [상세] 모달(138-4)이 쓰는 프로젝트 · 노선 · 상세
if (projectId) setCrossDetailHost({ projectId, routeId: ctx.routeId, detail: ctx.detail });
// 횡단 [상세] 모달(138-4)이 쓰는 프로젝트 · 노선 · 상세 · 칸 크기 그림(173-4)
if (projectId)
setCrossDetailHost({
projectId,
routeId: ctx.routeId,
detail: ctx.detail,
chart: (stationId, widthPx, heightPx) => ctx.view().detailChart(stationId, widthPx, heightPx),
});
/**
* 측점 설계 버튼 변경 처리: (1) 선택을 즉시 로컬 반영해 해당 카드만 리프레시(버튼 즉시 반응),
+23 -1
View File
@@ -38,8 +38,10 @@ import {
type CrossPlotBase,
crossPlotBase,
effectiveCardHalfWidth,
fitPlotBase,
naturalHeightAtWidth,
} from "./B06_Section_UI_Cross_View_Metrics";
import { cardZoomStates } from "./B06_Section_UI_Cross_View_Zoom";
import { CROSS_GRID_GAP, CROSS_HEIGHT } from "./B06_Section_UI_Section_Common";
import type { SectionStructureEdit } from "./B06_Section_UI_Section_View_Menu";
import { culvertLinkFor as culvertLink } from "./B06_Section_UI_Cross_Culvert_Wire";
@@ -347,6 +349,7 @@ export function createSectionView(
section: CrossSection,
forcedHeightPx?: number,
plotBase?: CrossPlotBase | null,
widthPx = cachedCardWidth,
): HTMLElement => {
const card = createCrossSectionCard(
section,
@@ -355,7 +358,7 @@ export function createSectionView(
(stationId) => selectStation(stationId, false),
cachedStationInterval,
currentCrossHalfWidth,
cachedCardWidth,
widthPx,
forcedHeightPx,
currentDetail ? cardDesignElevation(currentDetail, section) : undefined,
onDesignChange,
@@ -430,6 +433,7 @@ export function createSectionView(
// CSS auto-fill의 자체 열 수와 JS columnCount가 어긋나면 같은 행 높이 그룹핑이 실제
// 렌더 행과 달라져 카드 내부가 뒤죽박죽된다(N-2-6). 열 수 · 폭을 JS가 명시해 일치시킨다.
grid.style.gridTemplateColumns = `repeat(${columnCount}, ${cachedCardWidth}px)`;
grid.dataset.columns = String(columnCount); // 4열부터 면적표 「계」만(173-3 · CSS)
cachedRowHeight.clear();
if (detail.cross_sections.length) {
// 1차: 각 카드의 자연 높이(250px 바닥 적용) 측정 → 같은 행(columnCount 단위) 최댓값을 행 높이로.
@@ -543,6 +547,24 @@ export function createSectionView(
},
refreshCard,
refreshCards,
detailChart(stationId, widthPx, heightPx) {
const detail = currentDetail;
const section = detail?.cross_sections.find((entry) => entry.station_id === stationId);
if (!detail || !section) return null;
const base = crossPlotBase(
section,
currentExaggeration,
widthPx,
effectiveCardHalfWidth(section, stationWidth?.widthFor(section) ?? currentCrossHalfWidth),
designElevationAt(detail.longitudinal.design_profiles, section.chainage_m),
);
// 카드 줌 · 팬은 카드 몫 — 모달 그림은 1배에서 시작(카드 상태는 되돌려 둠)
const cardZoom = cardZoomStates.get(stationId);
cardZoomStates.delete(stationId);
const card = buildCrossCard(section, heightPx, base && fitPlotBase(base, heightPx), widthPx);
if (cardZoom) cardZoomStates.set(stationId, cardZoom);
return card.querySelector<SVGSVGElement>("svg.b06-section__chart");
},
focusStation(stationId) {
if (stationId === null) clearSelection();
else if (selectedStationId !== stationId) selectStation(stationId, true);
@@ -37,6 +37,8 @@ export interface SectionViewController {
refreshCard: (chainageM: number) => void;
/** 여러 측점을 한꺼번에 교체한다 — 상단 패널은 **마지막에 한 번만** 다시 그린다. */
refreshCards: (chainages: ReadonlyArray<number>) => void;
/** [상세] 모달 그림(173-4) — 카드 그리기 함수로 칸 크기(W × H px)에 맞춰 새로 그린 svg(없으면 null). */
detailChart: (stationId: string, widthPx: number, heightPx: number) => SVGSVGElement | null;
/** 좌측 구조물 목록에서 고른 측점 카드를 선택하고 화면에 드러낸다 — 재클릭 토글 없음
* (2026-08-29 B05/B06 일원화: 목록 클릭 → 해당 카드 스크롤·강조). null = 카드 선택 풂(54-8). */
focusStation: (stationId: string | null) => void;
@@ -10,7 +10,14 @@
/* 절·성토 면적 오버레이(E-4) — 그래프 **좌측 상단**(축선 바로 안쪽), 배경색으로 그래프
선과 겹쳐도 가독. 절토/성토 × EA·RR·BR·계 표라서 칩을 늘어놓을 때보다 가로 폭을 덜 먹는다.
가운데(left:50%)에 두던 것을 좌측으로만 옮겼다 — 세로 자리는 그대로(2026-08-23 사용자 지시).
58px = CROSS_PAD.left(Y축 눈금 띠 폭) — 표가 플롯 왼쪽 끝에 맞춰 선다. */
58px = CROSS_PAD.left(Y축 눈금 띠 폭) — 표가 플롯 왼쪽 끝에 맞춰 선다.
글자 = 그림 칸(컨테이너) 폭 비례(173-3 · 3열 ≈ 0.72rem) · 표 높이 변수도 같은 식 —
조정창 · 보라 치수가 표 아래에서 시작(`_Cross_Controls`). */
.b06-cross-card__chart-wrap {
--b06-areas-font: clamp(0.6rem, 0.55rem + 0.7cqw, 0.9rem);
--b06-areas-height: calc(var(--b06-areas-font) * 5);
}
.b06-cross-card__areas {
position: absolute;
top: var(--spacing-8);
@@ -19,7 +26,7 @@
padding: 2px 6px;
border-radius: var(--radius-inputs);
background: color-mix(in srgb, var(--color-surface) 88%, transparent);
font-size: 0.72rem;
font-size: var(--b06-areas-font);
font-family: var(--font-mono);
white-space: nowrap;
border-collapse: collapse;
@@ -28,11 +35,18 @@
.b06-cross-card__areas th,
.b06-cross-card__areas td {
padding: 0 5px;
padding: 0 0.45em;
text-align: center;
font-weight: var(--font-weight-regular);
}
/* 4열부터 「계」만(173-3) — EA · RR · BR 칸(머리 · 본문 2~4째)을 접음 · 열 수 = 그리드 data-columns */
.b06-section__grid:is([data-columns="4"], [data-columns="5"], [data-columns="6"])
.b06-cross-card__areas
:is(th, td):nth-child(n + 2):nth-child(-n + 4) {
display: none;
}
/* 열 제목은 가운데 맞춤 — 값은 우측 정렬이라 자릿수가 흔들려도 머리글은 열 중앙에 선다. */
.b06-cross-card__areas thead th {
text-align: center;
@@ -432,7 +446,7 @@
.b06-design__rockb-readout {
padding: 0 var(--spacing-8);
font-size: 0.72rem;
font-size: var(--b06-areas-font);
font-family: var(--font-mono);
color: var(--color-warning);
align-self: center;
@@ -30,41 +30,22 @@
flex: none;
}
/* 가벼운 CAD 꼴 — 어두운 바탕 · 카드 그림 복제에 다크 색 토큰을 덮어씀 */
/* 그림 칸 = 테마 색(173-4 · 카드와 같은 바탕) · 그림은 칸 크기로 새로 그려 가득 */
.b06-detail__cad {
--color-bg: #111111;
--color-surface: #111111;
--color-surface-raised: #1a1a1a;
--color-border: #3a3a3a;
--color-text: #f2f2f2;
--color-text-body: #e6e6e6;
--color-text-secondary: #b8b8b8;
--color-text-muted: #8b8b8b;
--color-primary: #c9a8ff;
--color-warning: #ffd34d;
--color-danger: #ff5f57;
--color-success: #4cd37b;
border-radius: var(--radius-cards);
background: #111111;
color: #f2f2f2;
background: var(--color-surface-raised);
color: var(--color-text);
overflow: hidden;
flex: 1;
min-height: 0;
/* [+ − ⤢] 우상단(164-1) · 그림은 가운데 */
/* [+ − ⤢] 우상단(164-1) */
position: relative;
container-type: size;
display: flex;
align-items: center;
justify-content: center;
}
/* 칸 비율 = viewBox 비율(--ratio · 164-1) — 칸 가득 · 여백 없이 커서 고정 줌이 정확 */
.b06-detail__cad > svg {
display: block;
flex: none;
width: min(100cqw, 100cqh * var(--ratio, 2));
height: auto;
aspect-ratio: var(--ratio, 2);
width: 100%;
height: 100%;
}
/* B07 횡단 수량 산출표 — 칸 배치 그대로 */
@@ -0,0 +1,47 @@
/* 173-4 [상세] 그림 칸 가득 — `B06_Section_UI_Cross_View_Metrics.ts` `fitPlotBase` 를 그 자리에서 트랜스파일해 Node 로 돌림.
* 결과 = 표준출력 JSON 한 벌. 짝 = `test_173_areas_detail.py`.
*/
const fs = require("fs");
const path = require("path");
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
const B06 = path.join(__dirname, "..", "..", "B06_Section");
const common = fs.readFileSync(path.join(B06, "B06_Section_UI_Section_Common.ts"), "utf8");
// 카드 여백 · 바닥 높이는 원본에서 읽음(화면과 같은 값)
const CROSS_PAD = Function(`return ${/export const CROSS_PAD = (\{[^}]*\})/.exec(common)[1]}`)();
const CROSS_HEIGHT = Number(/export const CROSS_HEIGHT = (\d+)/.exec(common)[1]);
const stub = {
CROSS_PAD,
CROSS_HEIGHT,
validElevation: (s) => Number.isFinite(s.elevation_m),
};
const js = ts.transpileModule(
fs.readFileSync(path.join(B06, "B06_Section_UI_Cross_View_Metrics.ts"), "utf8"),
{ compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 } },
).outputText;
const box = { exports: {} };
new Function("exports", "module", "require", js)(box.exports, box, () => stub);
const m = box.exports;
// 반폭 10 m · 높이 차 8 m — 넓은 칸(1200 × 400)에선 자연 높이가 칸보다 큼
const section = {
samples: [-10, -5, 0, 5, 10].map((offset_m, i) => ({ offset_m, elevation_m: 100 + i * 2 })),
};
const base = m.crossPlotBase(section, 1, 1200, 10, 104);
const fit = m.fitPlotBase(base, 400);
const plot = m.crossPlotFromBase(fit, 400);
const tall = m.fitPlotBase(base, base.naturalHeight + 100);
const out = {
natural: base.naturalHeight,
fitHeight: fit.naturalHeight,
plotHeight: plot.heightPx,
// 도면 폭(px) 그대로 = m당 px × 가로 범위
innerBefore: base.pixelsPerMeter * (base.maxOffset - base.minOffset),
innerAfter: fit.pixelsPerMeter * (fit.maxOffset - fit.minOffset),
// 좌우 같이 넓힘 — 가운데 그대로
midBefore: (base.maxOffset + base.minOffset) / 2,
midAfter: (fit.maxOffset + fit.minOffset) / 2,
wider: fit.maxOffset - fit.minOffset > base.maxOffset - base.minOffset,
tallSame: tall === base,
};
process.stdout.write(JSON.stringify(out));
+1 -1
View File
@@ -95,4 +95,4 @@ def test_상세_큰_창_공용_막_그림_글_표_순서():
assert "body.append(cad, note, tableBox)" in src
assert "pointer-events: none" not in css and "b06-detail__backdrop" not in css
assert ".ui-modal.b06-detail > .ui-modal__body {\n overflow: hidden;" in css
assert "flex: 1;\n min-height: 0;" in css and "aspect-ratio: var(--ratio, 2);" in css
assert "flex: 1;\n min-height: 0;" in css and "height: 100%;" in css # 173-4 칸 가득
+5 -4
View File
@@ -42,7 +42,7 @@ def out() -> dict:
def test_복제_자식을_g_하나로_감쌈_비율(out):
assert out["wrapped"] is True # 축 · 눈금 · 도형이 한 장으로 움직임
assert out["ratio"] == "2" # viewBox 600 × 300
assert out.get("ratio") is None # 173-4 — 칸 = viewBox(칸 크기로 새로 그림) · 비율 변수 없음
assert out["before"] is None
@@ -71,9 +71,10 @@ def test_모달_배선_단추_칸_비율():
assert "wheelZoom: true" in src and "attachSvgZoomPan({" in src
assert "cad.replaceChildren(clone, buildZoomControls(handle))" in src
assert "attachDetailZoom(clone, zoom, (next) => (zoom = next))" in src
assert "position: relative;" in css and "container-type: size;" in css
assert "aspect-ratio: var(--ratio, 2);" in css
assert "width: min(100cqw, 100cqh * var(--ratio, 2));" in css
assert "position: relative;" in css
# 173-4 — 그림 = 칸 크기로 새로 그림 · svg 칸 가득
assert ".b06-detail__cad > svg {\n display: block;\n width: 100%;\n height: 100%;" in css
assert "--ratio" not in css and "--ratio" not in src
def test_강조_붉은_한_벌_고름_hover_다름_빈칸():
+122
View File
@@ -0,0 +1,122 @@
"""PLAN 173-3 · 173-4 — B06 횡단 카드 면적표 열 연동 · [상세] 그림 테마 색 · 칸 가득.
짝: `B06_Section/B06_Section_UI_Style_Cross_Areas.css`(글자 = 그림 칸 폭 · 4열부터 「계」만) ·
`_UI_Section_View.ts`(그리드 data-columns · `detailChart`) · `_UI_Cross_Detail.ts`(칸 크기로 새로 그림) ·
`_UI_Style_Cross_Detail.css`(테마 토큰) · `_UI_Cross_View_Metrics.ts` `fitPlotBase`(Node 헬퍼
`helper_173_detail_fit.cjs` 가 실제 TS 를 돌림).
"""
from __future__ import annotations
import json
import os
import re
import subprocess
import pytest
HERE = os.path.dirname(__file__)
ROOT = os.path.join(HERE, "..", "..")
B06 = os.path.join(ROOT, "B06_Section")
def _read(name: str) -> str:
with open(os.path.join(B06, name), encoding="utf-8") as f:
return f.read().replace(chr(13), "")
@pytest.fixture(scope="module")
def fit() -> dict:
if not os.path.isdir(os.path.join(ROOT, "config", "node_modules")):
pytest.skip("프론트엔드 의존성 미설치")
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
["node", os.path.join(HERE, "helper_173_detail_fit.cjs")],
capture_output=True,
text=True,
encoding="utf-8",
timeout=60,
)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout)
# ── 173-3 면적표 ─────────────────────────────────────────────
def test_면적표_글자_그림_칸_폭_비례_높이_변수_같은_식():
css = _read("B06_Section_UI_Style_Cross_Areas.css")
wrap = css.split(".b06-cross-card__chart-wrap {", 1)[1].split("}", 1)[0]
assert "--b06-areas-font: clamp(0.6rem, 0.55rem + 0.7cqw, 0.9rem);" in wrap
assert "--b06-areas-height: calc(var(--b06-areas-font) * 5);" in wrap
table = css.split(".b06-cross-card__areas {", 1)[1].split("}", 1)[0]
assert "font-size: var(--b06-areas-font);" in table
assert "font-size: 0.72rem" not in css
# 3열 그림 칸 ≈ 400 px → 지금 글자(0.72rem)와 거의 같음 · 1열 · 6열은 끝값
def rem(width_px: float) -> float: # 1cqw = 칸 폭 1% · 1rem = 16 px
return min(max(0.6, 0.55 + 0.7 * width_px / 100 / 16), 0.9)
assert rem(400) == pytest.approx(0.725)
assert rem(1232) == 0.9 and rem(192) == pytest.approx(0.634)
def test_면적표_4열부터_계만_접기():
css = _read("B06_Section_UI_Style_Cross_Areas.css")
rule = re.search(
r'\.b06-section__grid:is\(\[data-columns="4"\], \[data-columns="5"\], \[data-columns="6"\]\)\s+'
r"\.b06-cross-card__areas\s+:is\(th, td\):nth-child\(n \+ 2\):nth-child\(-n \+ 4\) \{\n display: none;",
css,
)
assert rule, "4~6열 EA · RR · BR 접기 규칙 없음"
src = _read("B06_Section_UI_Section_View.ts")
assert "grid.dataset.columns = String(columnCount);" in src
# ── 173-4 [상세] 그림 ────────────────────────────────────────
def test_상세_테마_토큰_어두운_고정색_없음():
css = _read("B06_Section_UI_Style_Cross_Detail.css")
cad = css.split(".b06-detail__cad {", 1)[1].split("}", 1)[0]
assert "background: var(--color-surface-raised);" in cad
assert "color: var(--color-text);" in cad
assert "--color-" not in cad.replace("var(--color-", "") # 토큰 덮어쓰기 없음
assert "#111111" not in css and "#f2f2f2" not in css and "#1a1a1a" not in css
def test_상세_칸_크기로_새로_그림_배선():
src = _read("B06_Section_UI_Cross_Detail.ts")
assert "const { clientWidth: width, clientHeight: height } = cad;" in src
assert "const clone = drawChart(state.section, width, height);" in src
assert "host?.chart(section.station_id, widthPx, heightPx)" in src
assert "new ResizeObserver(() => state.paint())" in src and "resize.disconnect()" in src
# 카드 clip id 와 가름 · 그리는 동안 refresh 되먹임 막음
assert "clip.id = `b06-detail-${id}`;" in src
assert "if (drawing || !open || open.stationId !== section.station_id) return;" in src
assert "getElementById(`cross-" not in src # 카드 DOM 복제 지움
view = _read("B06_Section_UI_Section_View.ts")
assert "detailChart(stationId, widthPx, heightPx) {" in view
assert "base && fitPlotBase(base, heightPx)" in view
assert "cardZoomStates.delete(stationId);" in view
types = _read("B06_Section_UI_Section_View_Types.ts")
assert "detailChart: (stationId: string, widthPx: number, heightPx: number) =>" in types
sync = _read("B06_Section_UI_Page_Design_Sync.ts")
assert "ctx.view().detailChart(stationId, widthPx, heightPx)" in sync
def test_칸_가득_넓은_칸은_가로_범위_넓힘(fit):
assert fit["natural"] > 400
assert fit["fitHeight"] == pytest.approx(400)
assert fit["plotHeight"] == pytest.approx(400)
assert fit["innerAfter"] == pytest.approx(fit["innerBefore"]) # 도면 폭 그대로
assert fit["wider"] is True
assert fit["midAfter"] == pytest.approx(fit["midBefore"]) # 좌우 같이
def test_칸_가득_높은_칸은_그대로(fit):
assert fit["tallSame"] is True # crossPlotFromBase 가 세로 범위를 늘려 채움
def test_뷰_파일_700줄_안():
for name in ("B06_Section_UI_Section_View.ts", "B06_Section_UI_Cross_Detail.ts"):
assert len(_read(name).splitlines()) <= 700, name