feat(B06): 알림 줄 상단 고정 · 열 수 칸 · 카드 그래프 3열 비율 유지(138-3)
- 알림 줄 sticky · 알림 없어도 줄 유지 · 오른쪽 끝 열 수 칸(1~6 · 기본 3 · pref cross-columns) - 카드 폭 = (칸 폭 − 간격) ÷ 열 수 · 높이 = 폭 × 3열 비율 · pickCrossColumns 지움 - 카드 scroll-margin-top = 고정 줄 높이 · test_138_cross_columns Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JywHxQuWPk6UYXTebyrBoU
This commit is contained in:
@@ -102,6 +102,8 @@ export const STATE_REGISTRY = {
|
||||
scope: "global",
|
||||
legacy: () => "b06:profile-panel-collapsed",
|
||||
},
|
||||
/** B06 횡단 카드 한 줄 카드 수(138-3 · 1~6 · 기본 3). */
|
||||
"cross-columns": { bucket: "pref", scope: "global" },
|
||||
"section-panel-height": {
|
||||
bucket: "pref",
|
||||
scope: "global",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* 측점을 누르면 그 카드로 간다. 경고만 — 구조물을 세우거나 값을 바꾸지 않는다.
|
||||
* ⭐ 2026-09-15 — 그 밑에 사면길이 3m 초과 **소단 검토 정보 한 줄**(경고 모양 아님 · 브레인 ③).
|
||||
* ⭐ 138-1 — 「⚠ 포장 권장 · n측점」 줄(B05 법정 경사 제안 · 포장 범위 밖 측점 · 옛 카드 배지 자리).
|
||||
* ⭐ 138-3 — 줄 전체가 카드 칸 위에 고정(sticky) · 알림 없어도 줄 유지(오른쪽 끝 열 수 칸은 부르는 쪽이 붙임).
|
||||
* ========================================================================== */
|
||||
|
||||
import type { CrossSection } from "./B06_Section_Api_Fetch";
|
||||
@@ -60,11 +61,15 @@ function noticeBlock(className: string): {
|
||||
|
||||
export function createFillSlopeNotice(onPick: (stationId: string) => void): FillSlopeNotice {
|
||||
const root = document.createElement("div");
|
||||
root.className = "b06-section__notice-bar";
|
||||
const blocks = document.createElement("div");
|
||||
blocks.className = "b06-section__notice-blocks";
|
||||
root.append(blocks);
|
||||
const warn = noticeBlock("b06-section__notice");
|
||||
const info = noticeBlock("b06-section__notice b06-section__notice--info");
|
||||
const pave = noticeBlock("b06-section__notice");
|
||||
pave.root.title = L("B06_Design_Paved_Suggested");
|
||||
root.append(warn.root, pave.root, info.root);
|
||||
blocks.append(warn.root, pave.root, info.root);
|
||||
root.addEventListener("click", (event) => {
|
||||
const id = (event.target as HTMLElement).closest("button")?.dataset.stationId;
|
||||
if (id) onPick(id);
|
||||
|
||||
@@ -63,6 +63,20 @@ export function crossPlotBase(
|
||||
return { sourceSamples, minOffset, maxOffset, elevationMid, pixelsPerMeter, naturalHeight };
|
||||
}
|
||||
|
||||
/**
|
||||
* 같은 기하를 다른 카드 폭으로 그렸을 때 자연 높이(바닥 적용 전 · 다시 계산 안 함 · 138-3).
|
||||
* 그림 몫은 폭(좌우 여백 뺀)에 비례 · 위아래 여백은 그대로.
|
||||
*/
|
||||
export function naturalHeightAtWidth(
|
||||
base: CrossPlotBase,
|
||||
fromWidthPx: number,
|
||||
toWidthPx: number,
|
||||
): number {
|
||||
const fixed = CROSS_PAD.top + CROSS_PAD.bottom + AREA_OVERLAY_HEADROOM_PX;
|
||||
const inner = (width: number): number => Math.max(width - CROSS_PAD.left - CROSS_PAD.right, 1e-6);
|
||||
return (base.naturalHeight - fixed) * (inner(toWidthPx) / inner(fromWidthPx)) + fixed;
|
||||
}
|
||||
|
||||
/** 재 둔 기하에 행 높이만 얹는다 — 다시 계산하지 않는다. */
|
||||
export function crossPlotFromBase(base: CrossPlotBase, forcedHeightPx?: number): CrossPlotMetrics {
|
||||
const heightPx = forcedHeightPx ?? Math.max(base.naturalHeight, CROSS_HEIGHT);
|
||||
|
||||
@@ -1,47 +1,43 @@
|
||||
/* =============================================================================
|
||||
* B06_Section_UI_Section_Columns.ts
|
||||
* 횡단 카드 한 줄 카드 수 고르기 — 보기 칸 높이에 첫 줄 + 둘째 줄 절반이 들어가는
|
||||
* 가장 적은 카드 수. 카드 폭은 최소~최대 폭 안(그림은 축척 그대로 · 폭만 바뀜).
|
||||
* 횡단 카드 열 수 · 폭 · 높이(138-3) — 열 수 = 사용자 값(알림 줄 오른쪽 끝 · 1~6 · 기본 3).
|
||||
* 카드 폭 = (칸 폭 − 간격) ÷ 열 수 · 그래프 높이 = 폭 × (3열 기준 높이 ÷ 3열 폭) — 열 수가
|
||||
* 바뀌어도 그림 비율 그대로(글자 크기는 그대로).
|
||||
* import 없음 — 시험이 이 파일만 옮겨 돌림(`test_138_cross_columns.py`).
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
CROSS_CARD_CHROME,
|
||||
CROSS_GRID_GAP,
|
||||
CROSS_GRID_MAX_WIDTH,
|
||||
CROSS_GRID_MIN_WIDTH,
|
||||
} from "./B06_Section_UI_Section_Common";
|
||||
export const CROSS_COLUMNS_MIN = 1;
|
||||
export const CROSS_COLUMNS_MAX = 6;
|
||||
export const CROSS_COLUMNS_DEFAULT = 3;
|
||||
/** 높이 비율의 기준 열 수. */
|
||||
export const CROSS_COLUMNS_BASE = 3;
|
||||
|
||||
export interface CrossColumns {
|
||||
columns: number;
|
||||
cardWidth: number;
|
||||
/** 저장값 → 열 수(정수 · 1~6 · 못 읽으면 기본 3). */
|
||||
export function clampCrossColumns(value: unknown): number {
|
||||
const number = typeof value === "string" && value.trim() === "" ? NaN : Number(value);
|
||||
if (!Number.isFinite(number)) return CROSS_COLUMNS_DEFAULT;
|
||||
return Math.min(CROSS_COLUMNS_MAX, Math.max(CROSS_COLUMNS_MIN, Math.round(number)));
|
||||
}
|
||||
|
||||
const widthFor = (columns: number, viewWidth: number): number =>
|
||||
Math.min(CROSS_GRID_MAX_WIDTH, (viewWidth - (columns - 1) * CROSS_GRID_GAP) / columns);
|
||||
/** 카드 폭(px) = (칸 폭 − 간격 × (열 수 − 1)) ÷ 열 수. */
|
||||
export function crossCardWidth(viewWidth: number, columns: number, gap: number): number {
|
||||
return Math.max(0, (viewWidth - gap * (columns - 1)) / columns);
|
||||
}
|
||||
|
||||
/**
|
||||
* `heightsFor(cardWidth, count)` — 그 폭일 때 앞 `count` 장의 카드 자연 높이.
|
||||
* `viewHeight` 가 0 이하(숨김 · 아직 안 잰 칸)면 가장 큰 카드 쪽을 고른다.
|
||||
* 카드마다 강제 높이(px) — `baseHeights[i]` = 그 카드를 3열 폭(`baseWidth`)으로 그렸을 때 높이.
|
||||
* 지금 폭으로 비례해 늘리고 · 같은 줄(열 수 단위) 최댓값으로 맞춤.
|
||||
*/
|
||||
export function pickCrossColumns(
|
||||
viewWidth: number,
|
||||
viewHeight: number,
|
||||
heightsFor: (cardWidth: number, count: number) => number[],
|
||||
): CrossColumns {
|
||||
const unit = CROSS_GRID_MIN_WIDTH + CROSS_GRID_GAP;
|
||||
const most = Math.max(1, Math.floor((viewWidth + CROSS_GRID_GAP) / unit));
|
||||
const fewest = Math.min(
|
||||
most,
|
||||
Math.max(1, Math.ceil((viewWidth + CROSS_GRID_GAP) / (CROSS_GRID_MAX_WIDTH + CROSS_GRID_GAP))),
|
||||
);
|
||||
for (let columns = fewest; columns < most; columns += 1) {
|
||||
const cardWidth = widthFor(columns, viewWidth);
|
||||
if (viewHeight <= 0) return { columns, cardWidth };
|
||||
const heights = heightsFor(cardWidth, columns * 2);
|
||||
const first = Math.max(0, ...heights.slice(0, columns)) + CROSS_CARD_CHROME;
|
||||
const second = Math.max(0, ...heights.slice(columns)) + CROSS_CARD_CHROME;
|
||||
const needed = heights.length > columns ? first + CROSS_GRID_GAP + second / 2 : first;
|
||||
if (needed <= viewHeight) return { columns, cardWidth };
|
||||
}
|
||||
return { columns: most, cardWidth: widthFor(most, viewWidth) };
|
||||
export function crossRowHeights(
|
||||
baseHeights: readonly number[],
|
||||
baseWidth: number,
|
||||
cardWidth: number,
|
||||
columns: number,
|
||||
): number[] {
|
||||
const scale = baseWidth > 0 ? cardWidth / baseWidth : 1;
|
||||
const heights = baseHeights.map((height) => height * scale);
|
||||
return heights.map((_, index) => {
|
||||
const start = index - (index % columns);
|
||||
return Math.max(...heights.slice(start, start + columns));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -35,11 +35,6 @@ export const LONG_WIDTH = 1200;
|
||||
export const LONG_HEIGHT = 220;
|
||||
export const CROSS_WIDTH = 560;
|
||||
export const CROSS_HEIGHT = 250;
|
||||
// 카드 폭 범위 — 한 줄 카드 수는 보기 칸 높이로 정한다(`_Section_Columns`).
|
||||
export const CROSS_GRID_MIN_WIDTH = 400;
|
||||
export const CROSS_GRID_MAX_WIDTH = 800;
|
||||
// 카드에서 그림 높이 밖 몫 — 머리 · 설계 줄 · 꼬리(실측 135).
|
||||
export const CROSS_CARD_CHROME = 135;
|
||||
export const CROSS_GRID_GAP = 16;
|
||||
// bottom은 측점 라벨 한 줄 몫이다. X축 제목을 그래프 상단으로 올린 뒤 남던 여백을 걷어내,
|
||||
// 종단면도 아래 라벨이 그 밑 유토곡선과의 틈 **가운데**에 오도록 좁혔다(2026-08-02 사용자 지시).
|
||||
|
||||
@@ -38,12 +38,22 @@ import {
|
||||
type CrossPlotBase,
|
||||
crossPlotBase,
|
||||
effectiveCardHalfWidth,
|
||||
naturalHeightAtWidth,
|
||||
} from "./B06_Section_UI_Cross_View_Metrics";
|
||||
import { CROSS_HEIGHT } from "./B06_Section_UI_Section_Common";
|
||||
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";
|
||||
import type { CulvertLink } from "./B06_Section_UI_Cross_Culvert_Wire";
|
||||
import { pickCrossColumns } from "./B06_Section_UI_Section_Columns";
|
||||
import {
|
||||
CROSS_COLUMNS_BASE,
|
||||
CROSS_COLUMNS_MAX,
|
||||
CROSS_COLUMNS_MIN,
|
||||
clampCrossColumns,
|
||||
crossCardWidth,
|
||||
crossRowHeights,
|
||||
} from "./B06_Section_UI_Section_Columns";
|
||||
import { readStateRaw, writeStateRaw } from "../A00_Common/b_page_state";
|
||||
import { createInputField } from "@ui/ui_template_elements";
|
||||
import {
|
||||
CROSS_WIDTH,
|
||||
type CrossDesignChange,
|
||||
@@ -93,9 +103,9 @@ export function createSectionView(
|
||||
let currentStationInterval: number | undefined;
|
||||
let structureEdit: SectionStructureEdit | null = null;
|
||||
let renderWidth = 0;
|
||||
let renderHeight = 0;
|
||||
let drawnColumns = 0;
|
||||
let resizeTimer = 0;
|
||||
/** 한 줄 카드 수 — 사용자 값(보기 설정 pref · 138-3). */
|
||||
let columnCount = clampCrossColumns(readStateRaw("cross-columns"));
|
||||
// 카드 단위 재빌드에 재사용하는 렌더 컨텍스트 (draw에서 갱신)
|
||||
let cachedStationInterval = 1;
|
||||
let cachedCardWidth = CROSS_WIDTH;
|
||||
@@ -113,6 +123,29 @@ export function createSectionView(
|
||||
const fillSlopeNotice = createFillSlopeNotice((id) =>
|
||||
selectedStationId === id ? revealCard(id, "smooth") : selectStation(id, true),
|
||||
);
|
||||
// 열 수 칸(138-3) — 고정 알림 줄 오른쪽 끝 · 1~6 · 바꾸면 카드 전부 다시 그림
|
||||
const columnsField = createInputField({
|
||||
type: "number",
|
||||
compact: true,
|
||||
min: CROSS_COLUMNS_MIN,
|
||||
max: CROSS_COLUMNS_MAX,
|
||||
value: String(columnCount),
|
||||
});
|
||||
columnsField.root.classList.add("b06-section__columns");
|
||||
columnsField.input.title = L("B06_Cross_Columns");
|
||||
columnsField.input.addEventListener("change", () => {
|
||||
const next = clampCrossColumns(columnsField.input.value);
|
||||
columnsField.input.value = String(next);
|
||||
if (next === columnCount) return;
|
||||
columnCount = next;
|
||||
writeStateRaw("cross-columns", String(next));
|
||||
draw();
|
||||
});
|
||||
fillSlopeNotice.root.append(columnsField.root);
|
||||
// 카드 scroll-margin-top = 고정 줄 높이 — 고른 카드가 줄 밑에 숨지 않게
|
||||
new ResizeObserver(() =>
|
||||
cards.style.setProperty("--b06-notice-h", `${fillSlopeNotice.root.offsetHeight}px`),
|
||||
).observe(fillSlopeNotice.root);
|
||||
|
||||
/** 종단도 한 벌 — 첫 그리기에서 횡단 보기 알림을 건다(`_Section_View_Profile`). */
|
||||
const profile = createSectionProfile(
|
||||
@@ -138,14 +171,7 @@ export function createSectionView(
|
||||
return Number.isFinite(width) ? Math.max(0, width) : 0;
|
||||
};
|
||||
|
||||
const contentHeight = (): number => {
|
||||
const style = getComputedStyle(cards);
|
||||
const pad = (value: string): number => Number.parseFloat(value) || 0;
|
||||
const height = cards.clientHeight - pad(style.paddingTop) - pad(style.paddingBottom);
|
||||
return Number.isFinite(height) ? Math.max(0, height) : 0;
|
||||
};
|
||||
|
||||
/** 카드 폭에서 앞 `count` 장의 기하 — 한 줄 카드 수 고르기 · 그리기가 함께 쓴다. */
|
||||
/** 카드 폭에서 앞 `count` 장의 기하. */
|
||||
const plotBasesFor = (detail: SectionDetailResponse, cardWidth: number, count: number) =>
|
||||
detail.cross_sections
|
||||
.slice(0, count)
|
||||
@@ -158,11 +184,17 @@ export function createSectionView(
|
||||
designElevationAt(detail.longitudinal.design_profiles, section.chainage_m),
|
||||
),
|
||||
);
|
||||
const naturalHeightsOf = (bases: ReadonlyArray<CrossPlotBase | null | undefined>): number[] =>
|
||||
bases.map((base) => Math.max(base?.naturalHeight ?? CROSS_HEIGHT, CROSS_HEIGHT));
|
||||
const pickColumns = (detail: SectionDetailResponse) =>
|
||||
pickCrossColumns(renderWidth, renderHeight, (cardWidth, count) =>
|
||||
naturalHeightsOf(plotBasesFor(detail, cardWidth, count)),
|
||||
/** 3열 폭으로 그렸을 때 높이(바닥 250) — 다른 열 수도 이 비율을 지킴(138-3). */
|
||||
const baseHeightsOf = (
|
||||
bases: ReadonlyArray<CrossPlotBase | null | undefined>,
|
||||
cardWidth: number,
|
||||
baseWidth: number,
|
||||
): number[] =>
|
||||
bases.map((base) =>
|
||||
Math.max(
|
||||
base ? naturalHeightAtWidth(base, cardWidth, baseWidth) : CROSS_HEIGHT,
|
||||
CROSS_HEIGHT,
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -368,14 +400,11 @@ export function createSectionView(
|
||||
grid.addEventListener("click", (event) => {
|
||||
if (!(event.target as HTMLElement).closest(".b06-cross-card")) clearSelection();
|
||||
});
|
||||
// 열 수 = 보기 칸 높이에 1.5줄이 드는 가장 적은 수 · 카드 폭 400~800(`_Section_Columns`).
|
||||
const picked = pickColumns(detail);
|
||||
const columnCount = picked.columns;
|
||||
drawnColumns = columnCount;
|
||||
// 열 수 = 사용자 값 · 폭 = (칸 폭 − 간격) ÷ 열 수(`_Section_Columns` · 138-3).
|
||||
cachedCardWidth = crossCardWidth(renderWidth, columnCount, CROSS_GRID_GAP);
|
||||
// CSS auto-fill의 자체 열 수와 JS columnCount가 어긋나면 같은 행 높이 그룹핑이 실제
|
||||
// 렌더 행과 달라져 카드 내부가 뒤죽박죽된다(N-2-6). 열 수 · 폭을 JS가 명시해 일치시킨다.
|
||||
grid.style.gridTemplateColumns = `repeat(${columnCount}, ${picked.cardWidth}px)`;
|
||||
cachedCardWidth = picked.cardWidth;
|
||||
grid.style.gridTemplateColumns = `repeat(${columnCount}, ${cachedCardWidth}px)`;
|
||||
cachedRowHeight.clear();
|
||||
if (detail.cross_sections.length) {
|
||||
// 1차: 각 카드의 자연 높이(250px 바닥 적용) 측정 → 같은 행(columnCount 단위) 최댓값을 행 높이로.
|
||||
@@ -385,19 +414,18 @@ export function createSectionView(
|
||||
// 반폭도 카드가 실제로 쓰는 값(`effectiveCardHalfWidth`, 자동 줌아웃 포함)으로 맞춰
|
||||
// 재 둔다 — 카드·행 높이가 어긋나지 않는다.
|
||||
const plotBases = plotBasesFor(detail, cachedCardWidth, sections.length);
|
||||
const naturalHeights = naturalHeightsOf(plotBases);
|
||||
// 2차: 행별 최댓값으로 강제 높이를 정해 같은 행 카드를 동일 높이로 렌더한다.
|
||||
for (let start = 0; start < sections.length; start += columnCount) {
|
||||
const rowHeight = Math.max(...naturalHeights.slice(start, start + columnCount));
|
||||
for (
|
||||
let index = start;
|
||||
index < Math.min(start + columnCount, sections.length);
|
||||
index += 1
|
||||
) {
|
||||
cachedRowHeight.set(sections[index].station_id, rowHeight);
|
||||
grid.append(buildCrossCard(sections[index], rowHeight, plotBases[index]));
|
||||
}
|
||||
}
|
||||
// 2차: 높이 = 폭 × (3열 높이 ÷ 3열 폭) · 같은 행 최댓값으로 맞춰 동일 높이로 렌더한다.
|
||||
const baseWidth = crossCardWidth(renderWidth, CROSS_COLUMNS_BASE, CROSS_GRID_GAP);
|
||||
const rowHeights = crossRowHeights(
|
||||
baseHeightsOf(plotBases, cachedCardWidth, baseWidth),
|
||||
baseWidth,
|
||||
cachedCardWidth,
|
||||
columnCount,
|
||||
);
|
||||
sections.forEach((section, index) => {
|
||||
cachedRowHeight.set(section.station_id, rowHeights[index]);
|
||||
grid.append(buildCrossCard(section, rowHeights[index], plotBases[index]));
|
||||
});
|
||||
} else {
|
||||
grid.append(emptyView(L("B06_Profile_View_NoCross")));
|
||||
}
|
||||
@@ -441,19 +469,14 @@ export function createSectionView(
|
||||
else fillSlopeNotice.update(currentDetail.cross_sections, cachedStationInterval);
|
||||
};
|
||||
|
||||
// 높이만 바뀌면(아래 패널 끌기 · 접기) 다시 그리지 않는다 — 열 수는 사용자 값(138-3).
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
const nextWidth = contentWidth();
|
||||
const nextHeight = contentHeight();
|
||||
if (!Number.isFinite(nextWidth) || nextWidth <= 0) return;
|
||||
const widthMoved = Math.abs(nextWidth - renderWidth) >= 1;
|
||||
if (!widthMoved && Math.abs(nextHeight - renderHeight) < 1) return;
|
||||
if (Math.abs(nextWidth - renderWidth) < 1) return;
|
||||
window.clearTimeout(resizeTimer);
|
||||
resizeTimer = window.setTimeout(() => {
|
||||
renderWidth = nextWidth;
|
||||
renderHeight = nextHeight;
|
||||
// 높이만 바뀌면(아래 패널 끌기 · 접기) 한 줄 카드 수가 바뀔 때만 다시 그린다.
|
||||
if (!widthMoved && currentDetail && pickColumns(currentDetail).columns === drawnColumns)
|
||||
return;
|
||||
draw();
|
||||
}, 150);
|
||||
});
|
||||
@@ -490,7 +513,6 @@ export function createSectionView(
|
||||
// 진입 시 측점을 자동으로 고르지 않는다(2026-08-02 사용자 지시) — 0측점이 선택된 채로
|
||||
// 시작하면 사용자가 고르지도 않은 카드가 강조돼 있고 유토곡선 말풍선도 떠 있다.
|
||||
renderWidth = contentWidth();
|
||||
renderHeight = contentHeight();
|
||||
draw();
|
||||
if (renderWidth <= 0) requestAnimationFrame(() => resizeObserver.observe(cards));
|
||||
},
|
||||
|
||||
@@ -44,7 +44,8 @@
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-cards);
|
||||
background: var(--color-surface-raised);
|
||||
scroll-margin-top: var(--spacing-8);
|
||||
/* 고정 알림 줄 높이만큼 띄움(138-3) — 고른 카드가 줄 밑에 숨지 않게 */
|
||||
scroll-margin-top: calc(var(--b06-notice-h, 0px) + var(--spacing-8));
|
||||
/* 카드를 **자체 쌓임 맥락**으로 만든다. 이게 없으면 카드 안 오버레이(줌 버튼·면적 표)의
|
||||
z-index가 최상위 축에서 겨루게 되어 이웃 판 위로 삐져나온다(2026-08-02 사용자 지적). */
|
||||
isolation: isolate;
|
||||
@@ -122,6 +123,41 @@
|
||||
gap: var(--spacing-16);
|
||||
}
|
||||
|
||||
/* 알림 줄(138-3) — 카드 칸 맨 위 고정 · 알림 없어도 줄 유지 · 오른쪽 끝 열 수 칸.
|
||||
위 여백(카드 칸 padding)만큼 끌어올려 그 틈으로 카드가 비치지 않게 */
|
||||
.b06-section__notice-bar {
|
||||
position: sticky;
|
||||
top: calc(-1 * var(--spacing-16));
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-8);
|
||||
margin-top: calc(-1 * var(--spacing-16));
|
||||
padding-block: var(--spacing-16) var(--spacing-4);
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
.b06-section__notice-blocks {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.b06-section__notice-blocks > .b06-section__notice:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.b06-section__columns {
|
||||
flex: none;
|
||||
width: 4.5em;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* 펼친 목록이 길면 줄 안에서 스크롤 — 고정 줄이 카드 칸을 다 덮지 않게 */
|
||||
.b06-section__notice-list {
|
||||
max-height: 30vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* 성토사면 5m 초과 경고 줄(2026-09-14) — 요약 한 줄, 펼치면 측점 단추 목록. */
|
||||
.b06-section__notice {
|
||||
margin-bottom: var(--spacing-8);
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/* 138-3 횡단 카드 열 수 · 폭 · 높이 — `B06_Section_UI_Section_Columns.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, "..", "..", "B06_Section", "B06_Section_UI_Section_Columns.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 c = box.exports;
|
||||
|
||||
// 칸 폭 1232 · 간격 16 → 3열 폭 400
|
||||
const baseWidth = c.crossCardWidth(1232, 3, 16);
|
||||
const base = [250, 300, 280, 260, 400];
|
||||
const out = {
|
||||
clamp: [undefined, "", "abc", 0, 1, 2.6, 6, 9, "4"].map(c.clampCrossColumns),
|
||||
widths: [1, 2, 3, 6].map((n) => c.crossCardWidth(1232, n, 16)),
|
||||
baseWidth,
|
||||
rows3: c.crossRowHeights(base, baseWidth, baseWidth, 3),
|
||||
rows2: c.crossRowHeights(base, baseWidth, c.crossCardWidth(1232, 2, 16), 2),
|
||||
rows1: c.crossRowHeights(base, baseWidth, c.crossCardWidth(1232, 1, 16), 1),
|
||||
};
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -0,0 +1,46 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""B06 횡단 카드 열 수 · 폭 · 높이(PLAN 138-3) — 열 수 1~6 · 기본 3 · 폭 = (칸 − 간격) ÷ n · 높이 = 폭 × 3열 비율.
|
||||
|
||||
규칙은 Node 헬퍼(`helper_138_cross_columns.cjs`)가 `B06_Section_UI_Section_Columns.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_138_cross_columns.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_열_수_1_6_기본_3(out):
|
||||
assert out["clamp"] == [3, 3, 3, 1, 1, 3, 6, 6, 4]
|
||||
|
||||
|
||||
def test_카드_폭은_칸_폭_나누기_열_수(out):
|
||||
assert out["widths"] == [1232, 608, 400, (1232 - 80) / 6]
|
||||
assert out["baseWidth"] == 400
|
||||
|
||||
|
||||
def test_3열은_자연_높이_줄_최댓값(out):
|
||||
assert out["rows3"] == [300, 300, 300, 400, 400]
|
||||
|
||||
|
||||
def test_다른_열_수는_3열_비율_그대로(out):
|
||||
# 2열 폭 608 = 3열 400 × 1.52 → 높이도 1.52 배 · 줄(2장) 최댓값
|
||||
assert [round(h, 6) for h in out["rows2"]] == [456, 456, 425.6, 425.6, 608]
|
||||
# 1열 폭 1232 = 3.08 배
|
||||
assert [round(h, 6) for h in out["rows1"]] == [770, 924, 862.4, 800.8, 1232]
|
||||
@@ -473,6 +473,7 @@ export const ui_locales_b2 = {
|
||||
"Available only when a ditch is created.",
|
||||
],
|
||||
B06_Notice_PaveSuggested: ["포장 권장", "Pavement recommended"],
|
||||
B06_Cross_Columns: ["한 줄 카드 수(1~6)", "Cards per row (1-6)"],
|
||||
B06_Design_Paved_Suggested: [
|
||||
"종단경사 법정 상한 초과 — 포장 권장 (임도설치 및 관리 등에 관한 규정 별표 1-2)",
|
||||
"Grade exceeds legal limit — pavement recommended (Forest Road Regulation, Annex 1-2)",
|
||||
|
||||
Reference in New Issue
Block a user