fix(507-3 덧 MC): B07 계획평면도 MC 큰 글 (MC) · B05 종단 아래 표 측점 행 (MC) · B07 횡단 장 단추 (MC) — 요구 311-4

- 평면도 측점 글 = 5 배수만 → 종단 kind curve 측점도 큰 글 + 원 + (MC)
- 종단 표 측점 행 · 고른 측점 값 열 = (MC) · 굵게 · 경고색 · 칸 폭 판정은 옛 글 그대로
- 횡단 장 단추 이름 뒤 (MC) · 긴 이름이라 두 열 가로지름
- test_507_3_mc_label 칸 넷 더함

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 16:23:33 +09:00
co-authored by Claude Sonnet 5.5
parent 44a73c4852
commit 7312b095bd
8 changed files with 89 additions and 5 deletions
@@ -294,6 +294,11 @@ export function renderProfile(ctx: ProfileRenderContext): void {
),
selectedStationId,
stationDisplay,
mcKeys: new Set(
(longitudinal.stations ?? [])
.filter((station) => station.kind === "curve")
.map((station) => station.chainage_m.toFixed(3)),
),
onCurveRadiusChange: (curve, radius) =>
ctx.applyEdits(setCurveRadius(store.edits(), curve, radius ?? 0)),
// 값 열 계획고 직접 입력 → 규칙 측점과 동일한 station_offset 파이프라인.
+20 -3
View File
@@ -43,6 +43,8 @@ export interface ProfileTableOptions {
irregularStations?: IrregularStation[];
/** 현재 선택된 측점 id. 규칙/비정규 측점 모두 그 측점의 값 열을 테이블에 겹쳐 보여준다. */
selectedStationId?: string | null;
/** MC(곡선 가운데) 측점 누가거리(`toFixed(3)` 열쇠) — 측점 행에 「 (MC)」 · 크게 · 굵게 · 경고색(507-3 덧). */
mcKeys?: ReadonlySet<string>;
/** 이어 공사 시작 기준 — 측점번호·누가거리 표시를 이만큼 더한다(내부 chainage는 0기준 유지). */
stationDisplay?: { station: number; cumulative: number };
onCurveRadiusChange: (curve: AlignmentCurve, radiusM: number | null) => void;
@@ -55,6 +57,8 @@ interface StationRowSpec {
unit: string;
cell: (index: number) => string;
modifier?: string;
/** 측점 이름 행 — MC 열에 (MC) 강조. */
station?: boolean;
}
interface SegmentRowSpec {
@@ -208,6 +212,7 @@ function buildStationRows(
{
label: "측점",
unit: "측점번호+잔여거리",
station: true,
cell: (index) => displayStationLabel(stations[index].chainage_m, interval, display.station),
},
];
@@ -458,6 +463,7 @@ function buildSelectedColumn(
display: { station: number; cumulative: number },
onAdjustStation?: (chainageM: number, deltaM: number) => void,
isIrregular = false,
mc = false,
): HTMLElement {
const plan = interpolateSample(alignment.samples, chainage, "elevation_m");
const ground = interpolateSample(alignment.samples, chainage, "ground_elevation_m");
@@ -487,7 +493,12 @@ function buildSelectedColumn(
{ value: gradeCell((segment) => segment.grade_percent, 2), modifier: "grade" }, // 구배 S
{ value: { text: plan !== null ? plan.toFixed(2) : "" }, modifier: "plan", plan: true }, // 계획고
{ value: { text: ground !== null ? ground.toFixed(2) : "" }, modifier: "" }, // 지반고
{ value: { text: displayStationLabel(chainage, interval, display.station) }, modifier: "" }, // 측점
{
value: {
text: displayStationLabel(chainage, interval, display.station) + (mc ? " (MC)" : ""),
},
modifier: mc ? "mc" : "",
}, // 측점
{ value: { text: curve ? curve.l_m.toFixed(2) : "" }, modifier: "" }, // 곡선 L
{ value: { text: curve ? curve.r_m.toFixed(1) : "" }, modifier: "" }, // 곡선 R
];
@@ -617,10 +628,13 @@ export function createProfileTable(options: ProfileTableOptions): HTMLElement {
alignment.stations.forEach((_, stationIndex) => {
// 구조물 측점 열은 기본값을 비운다 — 격자(빈 셀)만 남기고 값은 선택 오버레이가 맡는다.
const structural = isStructural[stationIndex];
// MC 열 = 측점 행에 「 (MC)」 · 굵게 · 경고색(507-3 덧) — 칸 폭 판정(`longest`)은 옛 글 그대로
const mc =
spec.station && options.mcKeys?.has(alignment.stations[stationIndex].chainage_m.toFixed(3));
const cell = element(
"span",
`b05-profile-table__cell${structural ? " is-structure" : ""}`,
structural ? "" : spec.cell(stationIndex),
`b05-profile-table__cell${structural ? " is-structure" : ""}${mc ? " b06-station--mc" : ""}`,
structural ? "" : spec.cell(stationIndex) + (mc ? " (MC)" : ""),
);
const rotated = stationThickness[stationIndex];
if (rotated !== null) {
@@ -649,6 +663,7 @@ export function createProfileTable(options: ProfileTableOptions): HTMLElement {
// 선택된 측점(규칙·비정규 공용)을 **값 열 오버레이**로 강조한다.
// 규칙 측점은 종점 이동을 반영한 `centers[index]`, 비정규 측점은 `x(chainage)`를 중심으로 쓴다.
const selectedIrregular = selectedIrregularEntry;
const mcAt = (chainageM: number): boolean => options.mcKeys?.has(chainageM.toFixed(3)) ?? false;
const selectedRegularIndex = selectedStationRow
? alignment.stations.indexOf(selectedStationRow)
: -1;
@@ -665,6 +680,7 @@ export function createProfileTable(options: ProfileTableOptions): HTMLElement {
display,
options.onAdjustStation,
true,
mcAt(selectedIrregular.chainage_m),
),
);
} else if (selectedRegularIndex >= 0) {
@@ -678,6 +694,7 @@ export function createProfileTable(options: ProfileTableOptions): HTMLElement {
display,
options.onAdjustStation,
false,
mcAt(alignment.stations[selectedRegularIndex].chainage_m),
),
);
}
@@ -288,6 +288,12 @@
color: rgb(37 99 235);
}
/* MC 측점 이름(507-3 덧) — 굵게 · 경고색 */
.b05-profile-table__irregular-col-cell.is-mc {
color: var(--color-warning);
font-weight: var(--font-weight-bold);
}
/* 계획고(is-plan)는 오버레이에서 별도 강조 없이 다른 값과 동일한 스타일을 쓴다(작업 C-2). */
/* 값 열의 계획고 직접 입력 셀 — 열은 pointer-events:none이라 입력만 되살린다. */
@@ -221,6 +221,7 @@ def _drawing_list(project_root: Path, longitudinal_path: Path) -> list[DesignDra
chainages = sheet["chainages"]
first = station_by_chainage.get(chainages[0], {})
station = station_no_label(float(first.get("chainage_m", chainages[0])), interval)
station += mc_suffix(first)
drawings.append(
DesignDrawingItem(
id=sheet["id"],
@@ -25,7 +25,11 @@ from B05_Profile.B05_Profile_Engine_Sections import (
)
from B05_Profile.B05_Profile_Structures_Repository import load_structures
from B06_Section.B06_Section_Engine_Design import compute_cross_design, curve_widening_args
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import infer_station_interval, station_no_label
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
infer_station_interval,
mc_suffix,
station_no_label,
)
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_IpTable import clip_lines, ip_table_piece
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Plan import (
_structure_label,
@@ -502,6 +506,11 @@ def build_pieces(
pieces["ip"], pieces["radius"] = curve_pieces(_load_design_curves(project_root), route, box)
if "station" in wanted:
items = []
curve_at = {
float(item["chainage_m"])
for item in longitudinal.get("stations") or []
if mc_suffix(item) and isinstance(item.get("chainage_m"), (int, float))
}
for chainage, x, y in stations:
if not (start_m <= chainage <= end_m):
continue
@@ -509,6 +518,8 @@ def build_pieces(
label = station_no_label(chainage, interval).replace("No.", "NO.")
number_only = "+" not in label
major = number_only and int(label[3:]) % 5 == 0
if chainage in curve_at: # MC = 고칠 자리(507-3) — 5 배수가 아니어도 글 · 큰 글(major)
label, major = label + " (MC)", True
items.append(
{
"chainage_m": chainage,
@@ -178,7 +178,10 @@ export function buildDrawingSidePanel(
}
for (const drawing of items) {
// 횡단면도는 장 단위(여러 측점)라 서버가 준 "N장 (구간)" 라벨을 그대로 쓴다.
section.append(drawingButton(drawing, drawing.label));
const button = drawingButton(drawing, drawing.label);
// 이름 뒤 「 (MC)」(507-3 덧) — 한 칸에 안 들어가 두 열을 가로지름
if (drawing.label.includes("(MC)")) button.dataset.mc = "true";
section.append(button);
}
if (structure && items.length === 0) {
const empty = document.createElement("p");
@@ -70,6 +70,11 @@
grid-column: 1 / -1;
}
/* MC 장 단추(507-3 덧) — 이름이 길어 한 줄 전체 */
.b07-drawing-group[data-kind="cross"] .b07-drawing-button[data-mc="true"] {
grid-column: 1 / -1;
}
.b07-drawing-button {
display: flex;
align-items: center;
+36
View File
@@ -87,3 +87,39 @@ def test_b05_b06_screen_mc_emphasis() -> None:
assert 'themeColor("--color-warning"' in ticks
swap = _src("B06_Section/logic/B06_Section_Station_Swap.ts")
assert 'formatStation(station.chainageM) + (kind === "curve" ? " (MC)" : "")' in swap
def test_b07_plan_station_mc_label() -> None:
"""계획평면도 측점 글 = 5 배수만이었음 → MC 도 큰 글(major) + (MC)."""
plan = _src("B07_DesignDetail/B07_DesignDetail_Router_Support_Plan.py")
assert 'label, major = label + " (MC)", True' in plan
assert "if mc_suffix(item)" in plan # 종단 kind curve 측점만
def test_b07_sheet_button_mc() -> None:
"""B07 횡단 장 단추 「3장 (No.29+19.0 (MC))」 — 그 장 첫 측점이 MC 일 때."""
support = _src("B07_DesignDetail/B07_DesignDetail_Router_Support.py")
assert "station += mc_suffix(first)" in support
assert "label=f\"{sheet['number']}장 ({station})\"" in support
def test_b05_profile_table_station_mc() -> None:
table = _src("B05_Profile/B05_Profile_UI_Profile_Table.ts")
# 칸 글 · 굵게 · 경고색 = B06 공용 규칙 · 칸 폭 판정(`longest`)은 (MC) 없는 글 그대로
assert 'spec.cell(stationIndex) + (mc ? " (MC)" : "")' in table
assert '${mc ? " b06-station--mc" : ""}' in table
assert 'display.station) + (mc ? " (MC)" : "")' in table # 고른 측점 값 열
render = _src("B05_Profile/B05_Profile_UI_Profile_Render.ts")
assert 'station.kind === "curve"' in render
css = _src("B05_Profile/B05_Profile_UI_Style_Table.css")
body = css[css.index("__irregular-col-cell.is-mc {") : css.index("}", css.index(".is-mc {"))]
assert "var(--color-warning)" in body
assert "var(--font-weight-bold)" in body
def test_b07_sheet_button_spans_row() -> None:
panels = _src("B07_DesignDetail/B07_DesignDetail_UI_Panels.ts")
assert 'button.dataset.mc = "true"' in panels
css = _src("B07_DesignDetail/B07_DesignDetail_UI_Style.css")
rule = css[css.index('.b07-drawing-button[data-mc="true"] {') :]
assert "grid-column: 1 / -1" in rule[: rule.index("}")]