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

This commit is contained in:
2026-10-17 15:15:59 +09:00
16 changed files with 225 additions and 351 deletions
+3 -171
View File
@@ -1,6 +1,6 @@
/* =============================================================================
* B06_Section_UI_Cross_Areas.ts
* 횡단 절·성토 면적 readout과 면적 영역 하이라이트 밴드.
* 횡단 절·성토 면적 영역 하이라이트 밴드(카드 좌상 면적표는 걷음 · 값은 상세 모달 표 · 379-2).
*
* 절토는 암반 경계선(지면선 평행 복사)을 기준으로 위=토사, 아래=암반으로 갈린다. 경계선
* 위치가 곧 유토곡선 EA/RR/BR 비율을 만들므로, 사용자가 경계선을 올리내리면 여기 표시되는
@@ -16,7 +16,7 @@
* ========================================================================== */
import type { CrossDesign, SectionSample } from "./B06_Section_Api_Fetch";
import { L, svgElement } from "./B06_Section_UI_Section_Common";
import { svgElement } from "./B06_Section_UI_Section_Common";
/**
* 강조 대상 = 면적표의 칸. `cut_total`은 토사·암반 두 밴드를 함께 켠다.
@@ -135,7 +135,7 @@ function bandPolygons(
/**
* 그 측점 절토의 암종. 분리 필드가 없는 **구 데이터**는 지반유형을 그대로 쓴다 —
* 표(`cutBreakdown`)는 그 규칙으로 RR/BR에 값을 넣는데 여기서만 null로 보면
* 면적표(상세 모달)는 그 규칙으로 RR/BR에 값을 넣는데 여기서만 null로 보면
* 암반 밴드가 아예 안 만들어져 **표에는 값이 있는데 그림이 안 켜진다**(2026-08-02 사용자 지적).
*/
function rockKindOf(design: CrossDesign): CrossDesign["cut_rock_kind"] {
@@ -246,171 +246,3 @@ export function appendCrossAreaBands(
}
};
}
/** 절토 단면적을 도면 표기(EA 토사 / RR 리핑암 / BR 발파암)로 갈라 낸다. */
function cutBreakdown(design: CrossDesign): {
ea: number;
rr: number;
br: number;
total: number;
} {
const total = design.cut_area_m2;
// 구 데이터(분리 필드 없음)는 절토 전량을 측점 지반유형으로 돌린다 — 유토곡선 폴백과 같은 규칙.
if (design.cut_soil_area_m2 === undefined || design.cut_rock_area_m2 === undefined) {
const ground = design.ground_type;
return {
ea: ground === "soil" ? total : 0,
rr: ground === "ripping_rock" ? total : 0,
br: ground === "blasting_rock" ? total : 0,
total,
};
}
const rock = design.cut_rock_area_m2;
const kind = rockKindOf(design);
return {
ea: design.cut_soil_area_m2,
rr: kind === "ripping_rock" ? rock : 0,
br: kind === "blasting_rock" ? rock : 0,
total,
};
}
/** 표 한 칸. `key`가 없으면 강조 대상이 아닌 칸(성토의 EA/RR/BR 자리)이다. */
interface AreaCell {
key: CrossAreaKey | null;
value: number | null;
variant: string;
}
function appendCell(
row: HTMLTableRowElement,
cell: AreaCell,
onSelect: ((key: CrossAreaKey) => void) | undefined,
cells: Map<CrossAreaKey, HTMLElement[]>,
): void {
const td = document.createElement("td");
td.className = `b06-design__area b06-design__area--${cell.variant}`;
const text = cell.value === null ? "—" : cell.value.toFixed(2);
// 값이 0이어도 칸은 남긴다 — 측점마다 항목 수가 달라지면 카드끼리 비교가 안 된다.
if (!cell.key || !onSelect || cell.value === null) {
td.textContent = text;
row.append(td);
return;
}
const button = document.createElement("button");
button.type = "button";
button.textContent = text;
button.title = L("B06_Design_Area_Highlight");
button.setAttribute("aria-pressed", "false");
button.addEventListener("click", (event) => {
// 카드까지 올라가면 측점 선택이 다시 걸려 카드가 통째로 다시 그려진다 — 강조가 즉시 사라진다.
event.stopPropagation();
onSelect(cell.key as CrossAreaKey);
});
cells.set(cell.key, [...(cells.get(cell.key) ?? []), button]);
td.append(button);
row.append(td);
}
/**
* 절·성토 면적값 오버레이(E-4) — 그래프 중상단에 표(절토/성토 × EA·RR·BR·계)로 표시한다.
* 항목 이름은 도면 표기를 그대로 쓴다(EA 토사 / RR 리핑암 / BR 발파암).
* `onSelect`가 오면 값 칸이 강조 토글 버튼이 된다(선택된 카드에서만 넘어온다).
*/
export function buildAreaReadout(
design: CrossDesign | undefined,
onSelect?: (key: CrossAreaKey) => void,
): { root: HTMLElement; setActive: AreaHighlightSetter } {
if (!design) {
const unset = document.createElement("div");
unset.className = "b06-cross-card__areas b06-design__area--unset";
unset.textContent = L("B06_Design_Unset");
return { root: unset, setActive: () => undefined };
}
const table = document.createElement("table");
table.className = "b06-cross-card__areas";
const head = document.createElement("tr");
// 좌상단(행제목 × 열제목 교차) 칸은 단위 자리다 — 값 칸마다 단위를 붙이지 않는다.
for (const [text, tip] of [
[L("B06_Design_Area_Unit"), ""],
[L("B06_Design_Area_EA"), L("B06_Design_Ground_Soil")],
[L("B06_Design_Area_RR"), L("B06_Design_Ground_Ripping")],
[L("B06_Design_Area_BR"), L("B06_Design_Ground_Blasting")],
[L("B06_Design_Area_Total"), ""],
]) {
const th = document.createElement("th");
th.textContent = text;
if (tip) th.title = tip;
head.append(th);
}
const thead = document.createElement("thead");
thead.append(head);
const cells = new Map<CrossAreaKey, HTMLElement[]>();
const cut = cutBreakdown(design);
const tbody = document.createElement("tbody");
const rows: Array<{ label: string; variant: string; cells: AreaCell[] }> = [
{
label: L("B06_Design_Cut_Area"),
variant: "cut_total",
// 값이 0이어도 누를 수 있다(2026-08-02 사용자 지시) — 그림에 없는 면적이면 강조만 켜지고
// 밴드는 그대로다. 칸마다 눌리는 자리가 달라지면 어느 칸이 눌리는지 매번 확인해야 한다.
cells: [
{ key: "cut_soil", value: cut.ea, variant: "cut_soil" },
{ key: "cut_rr", value: cut.rr, variant: "cut_rr" },
{ key: "cut_br", value: cut.br, variant: "cut_br" },
{ key: "cut_total", value: cut.total, variant: "cut_total" },
],
},
{
// 성토는 완성 단면(다짐 상태)이라 지반유형으로 갈리지 않는다 — 계 한 칸만 쓴다.
label: L("B06_Design_Fill_Area"),
variant: "fill",
cells: [
{ key: null, value: null, variant: "fill" },
{ key: null, value: null, variant: "fill" },
{ key: null, value: null, variant: "fill" },
{ key: "fill", value: design.fill_area_m2, variant: "fill" },
],
},
];
// 사토장이 선 측점만 한 줄 더 — **성토와 합치지 않는다**(확정 ㉠). 없는 측점에 빈 줄을
// 세우면 어느 측점에 사토장이 있는지 표에서 안 보인다.
const spoilArea = Number(design.spoil_fill_area_m2 ?? 0);
if (spoilArea > 0) {
rows.push({
label: L("B06_Design_SpoilFill_Area"),
variant: "spoil-fill",
cells: [
{ key: null, value: null, variant: "spoil-fill" },
{ key: null, value: null, variant: "spoil-fill" },
{ key: null, value: null, variant: "spoil-fill" },
{ key: null, value: spoilArea, variant: "spoil-fill" },
],
});
}
for (const row of rows) {
const tr = document.createElement("tr");
const th = document.createElement("th");
th.className = `b06-design__area b06-design__area--${row.variant}`;
th.textContent = row.label;
tr.append(th);
for (const cell of row.cells) appendCell(tr, cell, onSelect, cells);
tbody.append(tr);
}
table.append(thead, tbody);
return {
root: table,
setActive: (key) => {
for (const [cellKey, elements] of cells) {
const active = cellKey === key;
for (const element of elements) {
element.classList.toggle("is-active", active);
element.setAttribute("aria-pressed", String(active));
}
}
},
};
}
+37 -10
View File
@@ -28,6 +28,8 @@ export interface CrossAxesOptions {
exaggeration: number;
/** 중심고(실제 표고) — Y축 라벨 자리에 둥근 상자로(318-2 · 소수 첫째 자리). 없으면 안 그림. */
centerZ?: number | null;
/** 계획 중심고(십자선 계획고) — 보라 상자를 원지반 상자 뒤(위)에(379-3). 없으면 안 그림. */
designZ?: number | null;
}
/** X 눈금 라벨("-12" 등)이 겹치지 않는 최소 간격(px). */
@@ -43,6 +45,8 @@ const Y_LABEL_GAP_PX = 7;
/** 중심고 상자 — 글자 좌우 여백 · 높이(px). 글자 크기는 눈금과 같음(10px). */
const CENTER_PAD_X = 3;
const CENTER_BOX_H = 14;
/** 두 상자 y 차가 이보다 작으면 원지반 상자만 비킴(px · 379-3). */
const TAG_GAP_PX = 16;
/**
* 축 레이어를 그리는 함수를 만든다. 줌·팬이 바뀔 때마다 그 상태로 다시 부르면 된다.
@@ -118,12 +122,12 @@ export function createCrossAxes(
const screenYOf = (raw: number) =>
ty + scale * y(options.elevationMid + (raw - options.elevationMid) * options.exaggeration);
// 중심고 상자 자리 — 보이는 범위 안일 때만 · 겹친 눈금 글도 그림(상자를 맨 뒤에 붙여 위에 ·
// 바탕이 안 비쳐 가림 · 319-1)
const centerZ = options.centerZ;
const centerY =
typeof centerZ === "number" && centerZ >= rawBottom && centerZ <= rawTop
? screenYOf(centerZ)
: null;
// 바탕이 안 비쳐 가림 · 319-1) · 원지반(녹) 먼저 · 계획(보라) 나중(379-3)
const { centerZ, designZ } = options;
const inView = (z: number | null | undefined): number | null =>
typeof z === "number" && z >= rawBottom && z <= rawTop ? screenYOf(z) : null;
const groundY = inView(centerZ);
const designY = inView(designZ);
for (let raw = Math.ceil(rawBottom / yStep) * yStep; raw <= rawTop + 1e-9; raw += yStep) {
const screenY = screenYOf(raw);
layer.append(
@@ -144,26 +148,49 @@ export function createCrossAxes(
}),
);
}
if (centerY !== null) appendCenterTag(layer, (centerZ as number).toFixed(1), plotLeft, centerY);
if (groundY !== null) {
const at = designY === null ? groundY : dodgeY(groundY, designY, plotTop, plotBottom);
appendCenterTag(layer, (centerZ as number).toFixed(1), plotLeft, at, "ground");
}
if (designY !== null)
appendCenterTag(layer, (designZ as number).toFixed(1), plotLeft, designY, "design");
};
}
/** 원지반 상자 y — 계획 상자와 `TAG_GAP_PX` 안이면 아래(원지반이 낮으면) · 위로 비킴 · 칸 밖이면 반대(379-3). */
function dodgeY(groundY: number, designY: number, top: number, bottom: number): number {
if (Math.abs(groundY - designY) >= TAG_GAP_PX) return groundY;
const below = groundY >= designY; // 화면 아래 = 낮은 표고
const half = CENTER_BOX_H / 2;
const away = designY + (below ? TAG_GAP_PX : -TAG_GAP_PX);
return away - half < top || away + half > bottom
? designY + (below ? -TAG_GAP_PX : TAG_GAP_PX)
: away;
}
/** 중심고 둥근 상자 — 글 오른끝 = 다른 눈금 자리 · 상자는 Y 축선(`axisX`) 4px 앞에서 멈춤 ·
* 글 폭은 잴 수 있으면 재고(붙은 뒤) 아니면 어림. */
function appendCenterTag(layer: SVGGElement, text: string, axisX: number, centerY: number): void {
function appendCenterTag(
layer: SVGGElement,
text: string,
axisX: number,
centerY: number,
kind: "ground" | "design",
): void {
const tone = `b06-chart__center-tag--${kind}`; // 색 = 원지반선 · 설계선(379-3)
const textRight = axisX - Y_LABEL_GAP_PX;
const box = svgElement("rect", {
y: centerY - CENTER_BOX_H / 2,
height: CENTER_BOX_H,
rx: 3,
ry: 3,
class: "b06-chart__center-box",
class: `b06-chart__center-box ${tone}`,
});
const label = svgText(text, {
x: textRight,
y: centerY + 3.5,
"text-anchor": "end",
class: "b06-chart__tick b06-chart__center-text",
class: `b06-chart__tick b06-chart__center-text ${tone}`,
});
layer.append(box, label);
const measured = (label as SVGTextElement).getComputedTextLength?.() ?? 0;
@@ -443,7 +443,6 @@ export function buildDesignControls(
scheduleReflow(card);
// 암 경계선 제어는 그래프 X축 제목 행으로 이동(E-7, Cross_View에서 배치).
// 절·성토 면적 readout은 그래프 중상단 오버레이로 이동(E-4, Cross_View에서 배치).
return { bar, groundSegment };
}
@@ -37,7 +37,7 @@ import {
type LineCellState,
type RockCellSpec,
} from "./B06_Section_UI_Cross_Line_Cell";
import { extendStop, toePoints } from "./logic/B06_Section_UI_Cross_Fit";
import { edgeOnGround, extendStop, toePoints } from "./logic/B06_Section_UI_Cross_Fit";
const SVG_NS = "http://www.w3.org/2000/svg";
const DESIGN_CLASS = "b06-chart__design-cross";
@@ -379,6 +379,7 @@ export function designLineMoves(
berm?: BermDepthControl;
center?: number;
toes?: ReturnType<typeof toePoints>;
bare?: ReturnType<typeof edgeOnGround>;
rock?: RockCellSpec;
} {
const design = section.design;
@@ -400,6 +401,7 @@ export function designLineMoves(
stopAt: (side, now, next) => extendStop(section.samples, design.road_edges, side, now, next),
},
toes: toePoints(section), // 교차점 손잡이(370-2)
bare: edgeOnGround(section.samples, design.road_edges), // 절·성토 없는 쪽(379-1)
berm: {
set: (side, valueM) => send({ berm_depth: { side, valueM } }),
// 끔은 이 측점이 켠 때만 — 다른 카드 소단을 고르며 옛 카드가 풀릴 때 새 켬을 안 지움
@@ -438,6 +440,8 @@ export function attachDesignLinePick(input: {
center?: number;
/** 있으면 쪽마다 교차점 손잡이(설계선 ∩ 원지반 · 키 `toe:left|right` · 늘 보임 · 370-2). */
toes?: ReturnType<typeof toePoints>;
/** 있으면 쪽마다 노견 끝 = 원지반(절·성토 없음 · 379-1) — 손잡이 값 앞 알림. */
bare?: ReturnType<typeof edgeOnGround>;
/** 있으면 암 경계선 고름(키 `rock` · ▲▼↺ · 세로 끌기) · 안 고른 때 공용 칸 = 암(370-3). */
rock?: RockCellSpec;
}): DesignLinePick {
@@ -500,7 +504,6 @@ export function attachDesignLinePick(input: {
const toe = input.toes?.[at] as { offset_m: number; elevation_m: number };
const hit = centerHit(x(toe.offset_m), toDisplayY(toe.elevation_m));
hit.classList.add(TOE_CLASS);
hit.setAttribute("r", "5");
hits.push(hit);
}
// 암 경계선(370-3) — 점선(`appendRockBoundaryOverlay`) 위 판정 선 하나(끊긴 토막도 한 path) · 키 rock
@@ -631,6 +634,7 @@ export function attachDesignLinePick(input: {
const moved = extend > 0 ? ` · 이동 +${extendText(extend)}m` : "";
const toe = toeSide ? input.toes?.[toeSide] : null;
const dims = piece ? pieceLabel(design, piece).replace("\n", " · ") : "";
const bare = toeSide && input.bare?.[toeSide] ? "절·성토 없음 · " : ""; // 379-1
// 아래 띠 공용 칸(370-3) — 안 고르면 암(암 지반만) · 고르면 그 선 제목 · 버튼 · 값 한 줄
const state: LineCellState | null =
onRock || (!chosen && rock)
@@ -639,8 +643,8 @@ export function attachDesignLinePick(input: {
? { title: "중심점", value: centerPreviewText(0), buttons: sideways(false) }
: toe
? {
title: `교차점 이동 ${sideName}`,
value: `노선에서 ${Math.abs(toe.offset_m).toFixed(2)}m${moved}`,
title: `사면 이동 ${sideName}`,
value: `${bare}노선에서 ${Math.abs(toe.offset_m).toFixed(2)}m${moved}`,
buttons: shift ? sideways(true) : {},
}
: !piece
+3 -15
View File
@@ -13,7 +13,6 @@ import type { CrossSection } from "./B06_Section_Api_Fetch";
import {
appendCrossAreaBands,
type AreaHighlightSetter,
buildAreaReadout,
type CrossAreaKey,
} from "./B06_Section_UI_Cross_Areas";
import {
@@ -192,7 +191,6 @@ export function createCrossSectionCard(
let isSelected = selected;
let activeArea: CrossAreaKey | null = selected ? (initialAreaKey ?? null) : null;
let setBandActive: AreaHighlightSetter = () => undefined;
let setChipActive: AreaHighlightSetter = () => undefined;
let activeRevet: RevetPick | null = revetOffset?.selectedFor(section) ?? null;
/** 이 카드에서 **강조**할 벽. 연동으로 이어진 옆 측점이 고른 벽도 켠다 —
* 조정창(`activeRevet`)은 고른 카드 하나에만 뜬다(2026-08-30 사용자). */
@@ -226,7 +224,6 @@ export function createCrossSectionCard(
if (activeArea === null) return;
activeArea = null;
setBandActive(null);
setChipActive(null);
onAreaSelect?.(section.station_id, null);
},
selectCard: () => onSelect(section.station_id),
@@ -245,7 +242,6 @@ export function createCrossSectionCard(
if (activeRevet !== null && activeArea !== null) {
activeArea = null;
setBandActive(null);
setChipActive(null);
}
if (activeRevet !== null) {
dropLine(); // 고름 하나만(137-3)
@@ -262,7 +258,6 @@ export function createCrossSectionCard(
card.classList.toggle("b06-cross-card--selected", nextSelected);
activeArea = nextSelected ? (areaKey ?? null) : null;
setBandActive(activeArea);
setChipActive(activeArea);
// 면적이 켜졌거나 카드가 선택에서 빠지면 구조물 선택은 풀린다(같은 레벨).
if ((activeArea !== null || !nextSelected) && activeRevet !== null) {
// 전역 선택이 **아직 이 카드 것일 때만** 비운다 — 옆 카드에서 방금 고른 벽까지
@@ -306,7 +301,6 @@ export function createCrossSectionCard(
dropLine();
}
setBandActive(activeArea);
setChipActive(activeArea);
onAreaSelect?.(section.station_id, activeArea);
};
/** 보라 선 고름 — 다른 고름(벽 · 본체 · 면적)을 풀고 카드도 고름(137-3). 다시 누르면 풂. */
@@ -319,7 +313,6 @@ export function createCrossSectionCard(
if (activeArea !== null) {
activeArea = null;
setBandActive(null);
setChipActive(null);
onAreaSelect?.(section.station_id, null);
}
if (!isSelected) {
@@ -387,6 +380,7 @@ export function createCrossSectionCard(
elevationMid,
exaggeration,
centerZ: section.center_z ?? null,
designZ: hasDesign ? (designElevation as number) : null,
});
renderAxes(cardZoomStates.get(section.station_id) ?? IDENTITY_VIEW);
@@ -649,9 +643,6 @@ export function createCrossSectionCard(
contentBounds,
);
scaleCenter(cardZoomStates.get(section.station_id)?.scale ?? 1);
// 절·성토 면적값은 그래프 중상단 오버레이로 표시(E-4). 값 칸은 항상 강조 토글이다.
const readout = buildAreaReadout(section.design, toggleArea);
setChipActive = readout.setActive;
// 배수관 · 흙막이 조정창 두 벌(카드 안 · 좌측 사본) — `_Cross_View_Panels`(137-1).
const panel = buildCardPanels({
section,
@@ -684,12 +675,9 @@ export function createCrossSectionCard(
fit: () => stationWidth.request(section.chainage_m, crossHalfWidth ?? maxOffset),
};
const zoomBar = buildZoomControls(zoomPan, widthActions);
chartWrap.append(svg, readout.root, zoomBar, panel.root);
chartWrap.append(svg, zoomBar, panel.root);
for (const panel of bodies.panels) chartWrap.append(panel.root);
if (activeArea) {
setBandActive(activeArea);
setChipActive(activeArea);
}
if (activeArea) setBandActive(activeArea);
card.append(chartWrap);
}
+2 -1
View File
@@ -455,7 +455,7 @@ export function createSectionView(
// CSS auto-fill의 자체 열 수와 JS columnCount가 어긋나면 같은 행 높이 그룹핑이 실제
// 렌더 행과 달라져 카드 내부가 뒤죽박죽된다(N-2-6). 열 수 · 폭을 JS가 명시해 일치시킨다.
grid.style.gridTemplateColumns = `repeat(${cols}, ${cachedCardWidth}px)`;
grid.dataset.columns = String(cols); // 4열부터 면적표 「계」만(173-3 · CSS)
grid.dataset.columns = String(cols); // 실제 그린 열 수(CSS)
// 맹암거 형상 누름 = 그 구조물 id 고름(288-2 · 노면공 포장 범위 표식과 따로 · 같은 id 고름 창구)
grid.addEventListener(SHAPE_PICK_EVENT, (event) => {
const id = (event as CustomEvent<string>).detail;
@@ -627,6 +627,7 @@ export function createSectionView(
elevationMid: plot.elevationMid,
exaggeration: Math.max(currentExaggeration, 0.1),
centerZ: section.center_z ?? null,
designZ: cardDesignElevation(detail, section) ?? null,
};
// 칸 맞춤이 넓힌 가로 범위를 처음 배율로 되좁힘 = 카드 반폭(266-1)
const homeScale = base && fitted ? base.pixelsPerMeter / fitted.pixelsPerMeter : 1;
+5 -5
View File
@@ -395,16 +395,16 @@ html.is-bm-route .b06-section__notice-bar {
font-size: 10px;
}
/* 중심고 Y축 라벨(318-2) — 눈금과 같은 10px · 둥근 상자 · 테마 강조색 · 축선 앞에서 멈춤 ·
바탕 = 불투명 섞음(겹친 눈금 글을 위에서 가림 · 319-1) */
/* 중심고 Y축 라벨(318-2) — 눈금과 같은 10px · 둥근 상자 · 축선 앞에서 멈춤 ·
바탕 = 불투명 섞음(겹친 눈금 글을 위에서 가림 · 319-1) · 색 = 원지반선 · 설계선(379-3 · `--b06-tag` = _Cross_Areas.css) */
.b06-chart__center-box {
fill: color-mix(in srgb, var(--color-accent) 14%, var(--color-surface-raised));
stroke: var(--color-accent);
fill: color-mix(in srgb, var(--b06-tag) 14%, var(--color-surface-raised));
stroke: var(--b06-tag);
stroke-width: 1;
}
.b06-chart__center-text {
fill: var(--color-accent);
fill: var(--b06-tag);
font-weight: var(--font-weight-medium);
}
+15 -101
View File
@@ -1,117 +1,24 @@
/* =============================================================================
* B06_Section_UI_Style_Cross_Areas.css
* 횡단 카드의 **면적 오버레이 계열** 스타일 — 절·성토 면적 표, 면적 강조 밴드,
* 횡단 카드의 **면적 오버레이 계열** 스타일 — 면적 강조 밴드,
* 설계선·암 경계선·포장층, 암 경계선 제어 버튼.
*
* `_UI_Style_Cross.css`가 700줄을 넘겨, 도면 뼈대(섹션·카드·차트·줌/팬)와
* 그 위에 얹는 면적 표기 계열을 갈랐다. 색은 모두 theme.css 토큰만 쓴다.
* ========================================================================== */
/* 절·성토 면적 오버레이(E-4) — 그래프 **좌측 상단**(축선 바로 안쪽), 배경색으로 그래프
선과 겹쳐도 가독. 절토/성토 × EA·RR·BR·계 표라서 칩을 늘어놓을 때보다 가로 폭을 덜 먹는다.
가운데(left:50%)에 두던 것을 좌측으로만 옮겼다 — 세로 자리는 그대로(2026-08-23 사용자 지시).
58px = CROSS_PAD.left(Y축 눈금 띠 폭) — 표가 플롯 왼쪽 끝에 맞춰 선다.
글자 = 그림 칸(컨테이너) 폭 비례(173-3 · 3열 ≈ 0.72rem) · 표 높이 변수도 같은 식 —
조정창 · 보라 치수가 표 아래에서 시작(`_Cross_Controls`). */
/* 그림 칸 글자 = 칸(컨테이너) 폭 비례(173-3). 카드 좌상 면적표는 걷음 — 값은 상세 모달 표(379-2). */
.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);
left: 58px;
transform: none;
padding: 2px 6px;
border-radius: var(--radius-inputs);
background: color-mix(in srgb, var(--color-surface) 88%, transparent);
font-size: var(--b06-areas-font);
font-family: var(--font-mono);
white-space: nowrap;
border-collapse: collapse;
pointer-events: none;
/* Y축 중심고 상자 색(379-3) — 원지반 = 원지반선 · 계획 = 설계선. */
.b06-chart__center-tag--ground {
--b06-tag: var(--color-chart-1);
}
.b06-cross-card__areas th,
.b06-cross-card__areas td {
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;
color: var(--color-text-muted);
}
/* 행 이름(절토/성토)은 왼쪽 정렬 — 숫자 열과 구분된다. */
.b06-cross-card__areas tbody th {
text-align: left;
}
/* 면적 표기 색은 **테마 토큰만** 쓴다. 고정 RGB를 박으면 다크 테마에서 글자가 배경에
묻힌다(발파암에 쓰던 검정이 그랬다). 차트 팔레트는 밝기가 두 테마 모두에 맞춰져 있다. */
.b06-design__area--cut,
.b06-design__area--cut_total {
color: var(--color-danger);
}
/* 절토 내역 — 토사(EA), 리핑암(RR), 발파암(BR)을 색으로 갈라 둔다. */
.b06-design__area--cut_soil {
color: var(--color-chart-2);
}
/* RR은 `--color-slate`를 쓰면 안 된다 — 그 토큰이 곧 `--color-text-secondary`라
라이트 테마에서 회색 글자가 "비활성"으로 읽히고, 28% 밴드는 흰 배경에 묻힌다.
`--color-warning`도 못 쓴다(암 경계선 점선이 이미 그 색이라 밴드가 제 경계선과 섞인다). */
.b06-design__area--cut_rr {
color: var(--color-success);
}
.b06-design__area--cut_br {
color: var(--color-chart-3);
}
.b06-design__area--fill {
color: var(--color-chart-0);
}
/* 사토장 — 성토와 **다른 색**으로 둔다. 같은 색이면 표에서 한 덩어리로 읽힌다. */
.b06-design__area--spoil-fill {
color: #6d7a20;
}
.b06-design__area--unset {
color: var(--color-text-muted);
}
/* 선택된 카드에서만 값 칸이 버튼이 된다 — 부모가 pointer-events: none이라 여기서 되살린다. */
.b06-cross-card__areas button {
padding: 0 2px;
border: none;
border-radius: var(--radius-inputs);
background: none;
color: inherit;
font-family: inherit;
font-size: inherit;
cursor: pointer;
pointer-events: auto;
}
/* 켠 칸은 글자색을 유지한 채 같은 색으로 옅게 칠하고 테두리를 둘러 대비를 살린다. */
.b06-cross-card__areas button.is-active {
background: color-mix(in srgb, currentcolor 18%, transparent);
box-shadow: inset 0 0 0 1px currentcolor;
font-weight: var(--font-weight-medium);
.b06-chart__center-tag--design {
--b06-tag: var(--color-royal-amethyst);
}
/* 면적 강조 밴드 — 평소엔 클릭 대상으로만 남고, 켜질 때만 채워진다. */
@@ -170,6 +77,8 @@
cursor: pointer;
}
/* 마우스 올림 = 고른 모양(379-1) · 고름은 누를 때만 */
.b06-chart__design-hit:hover,
.b06-chart__design-hit.is-active {
stroke: color-mix(in srgb, var(--color-royal-amethyst) 45%, transparent);
stroke-width: 7;
@@ -193,21 +102,26 @@
cursor: ew-resize;
}
/* 교차점 손잡이(370-2) — 사면 ∩ 원지반 작은 원 · 늘 보임 · 고르면 채움. */
/* 교차점 손잡이(370-2) — 사면 ∩ 원지반 작은 원 · 늘 보임 · 고르면 채움 · 올리면 같은 모양(379-1). */
.b06-chart__toe-hit {
fill: var(--color-surface);
stroke: var(--color-royal-amethyst);
stroke-width: 1.5;
}
.b06-chart__toe-hit:hover,
.b06-chart__toe-hit.is-active {
fill: var(--color-royal-amethyst);
cursor: pointer;
}
/* 암 경계선 고름(370-3) — 점선 위 투명 판정 선 · 고르면 경고색 띠 · 세로 끌기. */
.b06-chart__design-hit.b06-chart__rock-hit:hover,
.b06-chart__design-hit.b06-chart__rock-hit.is-active {
stroke: color-mix(in srgb, var(--color-warning) 40%, transparent);
}
.b06-chart__design-hit.b06-chart__rock-hit.is-active {
cursor: ns-resize;
}
@@ -4,15 +4,13 @@
* 넘칠 만큼 길어지면 창 안에서 스크롤한다. z-index는 카드 안 오버레이 중 최상단. */
.b06-structure-panel {
position: absolute;
/* 좌측 상단에는 절·성토 면적표가 이미 서 있다 — 조정창을 같은 자리에 두면 표를 덮어
수치가 안 보인다(2026-09-12 사용자). 표 높이(머리글+절토+성토 세 줄)만큼 내려 시작한다.
아래를 기준으로 붙이지 않는 까닭은 종전 그대로다 — 낮은 횡단도에서 창이 잘린다. */
top: calc(var(--spacing-8) + var(--b06-areas-height, 3.6rem));
/* 좌측 상단 면적표를 걷어(379-2) 위에서 바로 시작. 아래 기준이 아닌 까닭 = 낮은 횡단도에서 잘림. */
top: var(--spacing-8);
/* Y축·눈금 라벨 띠(CROSS_PAD.left)를 덮지 않게 그림 영역 안쪽에서 시작한다. */
left: calc(var(--b06-plot-left, 58px) + var(--spacing-8));
z-index: 20;
display: flex;
max-height: calc(100% - 2 * var(--spacing-8) - var(--b06-areas-height, 3.6rem));
max-height: calc(100% - 2 * var(--spacing-8));
flex-direction: column;
gap: 2px;
padding: var(--spacing-8);
@@ -112,6 +112,19 @@ export function extendStop(
return next;
}
/** 절·성토 없는 쪽(379-1) — 노견 끝 = 원지반(±5 mm · 멈춤 반올림 0.001 m 를 덮음). 지반 모르면 false. */
export function edgeOnGround(
samples: CrossSection["samples"],
edges: CrossDesign["road_edges"] | undefined,
): Record<"left" | "right", boolean> {
const groundAt = groundInterpolator(samples ?? []);
const on = (side: "left" | "right") =>
!!groundAt &&
!!edges &&
Math.abs(groundAt(edges[side].offset_m) - edges[side].elevation_m) <= 0.005;
return { left: on("left"), right: on("right") };
}
/**
* 성토측 사면의 **본래** 경사길이(m) — 성토가 아닌 측은 null (2026-09-03 사용자 지시).
*
@@ -283,6 +283,39 @@ const out = {};
hidden_when_out_of_view: draw(800).querySelector(".b06-chart__center-box") === null,
absent_without_center: draw(null).querySelector(".b06-chart__center-box") === null,
};
// 379-3 상자 둘 — 원지반(녹) 먼저 · 계획(보라) 나중 · 16 px 안이면 원지반 상자만 비킴
const yOf = (z) => CROSS_PAD.top + (base.displayMax - z) * base.pixelsPerMeter;
const tagsOf = (centerZ, designZ) => {
const layer = new FakeNode("g");
createCrossAxes(layer, { ...base, centerZ, designZ })(IDENTITY_VIEW);
const pick = (kind) => {
const all = layer.querySelectorAll(`.b06-chart__center-tag--${kind}`);
const box = all.find((n) => n.classList.contains("b06-chart__center-box"));
const text = all.find((n) => n.classList.contains("b06-chart__center-text"));
return (
box && {
mid: Number(box.attrs.y) + Number(box.attrs.height) / 2,
text: text?.textContent ?? null,
}
);
};
return {
ground: pick("ground"),
design: pick("design"),
order: layer.children.slice(-4).map((node) => node.className),
groundY: yOf(centerZ),
designY: yOf(designZ),
};
};
const bottomZ =
base.displayMax - (base.heightPx - CROSS_PAD.bottom - CROSS_PAD.top) / base.pixelsPerMeter;
out.tags = {
apart: tagsOf(876.57, 878.5),
groundLow: tagsOf(876.57, 876.9),
groundHigh: tagsOf(876.9, 876.57),
edge: tagsOf(bottomZ + 0.4, bottomZ + 0.5), // 아래로 비키면 칸 밖 → 위로
designOnly: tagsOf(null, 876.9),
};
}
// ── 2. 아래 띠(318-2 · 318-3) — 중심고 글 없음 · 암 경계 맨 왼쪽 · 토사도 띠 남음 ─────
+35 -2
View File
@@ -107,11 +107,12 @@ class FakeNode {
(this.listeners.click ?? []).forEach((fn) => fn(event));
}
}
const docKeys = new Set(); // 방향키 듣개(379-1 · 정확히 0 자리)
globalThis.document = {
createElement: (tag) => new FakeNode(tag),
createElementNS: (_ns, tag) => new FakeNode(tag),
addEventListener: () => {},
removeEventListener: () => {},
addEventListener: (type, fn) => type === "keydown" && docKeys.add(fn),
removeEventListener: (type, fn) => type === "keydown" && docKeys.delete(fn),
};
globalThis.window = { setTimeout, clearTimeout, addEventListener: () => {} };
@@ -302,4 +303,36 @@ out.inherit = {
after.handle.setActive(afterIndex);
out.inherit.label = cellOf(after.handle).title;
// ④ 정확히 0 자리(379-1) — 노견 끝 = 원지반(±5 mm = 절·성토 없음) · 손잡이 · 누르면 사면처럼 ◀↺▶ · 방향키
// 조각 0 은 안 봄 — 그림 규칙(노견 끝 → 첫 접점 보호 구간)이 지반 위 짧은 선을 남김(브레인 A)
const zero = section(out.stop.value);
const zeroPicks = [];
const zeroLayer = new FakeNode("g");
appendCrossDesignOverlay(zeroLayer, zero.design, x, y, zero.samples, undefined);
zeroLayer.children.forEach((el) => el.setAttribute(pick.DESIGN_LINE_ATTR, ""));
const zeroHandle = pick.attachDesignLinePick({
layer: zeroLayer,
design: zero.design,
x,
toDisplayY: y,
onPick: (i) => zeroPicks.push(i),
...pick.designLineMoves(zero, () => undefined),
});
const zeroToes = zeroLayer.children.filter((k) => k.classList.contains("b06-chart__toe-hit"));
const rightToe = zeroToes.find((t) => Number(t.getAttribute("cx")) > x(0));
docKeys.clear();
rightToe?.click();
zeroHandle.setActive(zeroPicks[0]);
const zeroCell = cellOf(zeroHandle);
out.zero = {
bare: pick.designLineMoves(zero, () => undefined).bare,
nearBare: pick.designLineMoves(near, () => undefined).bare,
toe: !!rightToe,
radius: rightToe ? rightToe.getAttribute("r") : null,
title: zeroCell.title,
value: zeroCell.value,
buttons: ["left", "reset", "right"].filter((slot) => zeroCell.button(slot)),
keys: docKeys.size,
};
process.stdout.write(JSON.stringify(out));
+15 -16
View File
@@ -1,6 +1,6 @@
"""PLAN 173-3 · 173-4 — B06 횡단 카드 면적표 열 연동 · [상세] 그림 테마 색 · 칸 가득.
짝: `B06_Section/B06_Section_UI_Style_Cross_Areas.css`(글자 = 그림 칸 폭 · 4열부터 「계」만) ·
짝: `B06_Section/B06_Section_UI_Style_Cross_Areas.css`(글자 = 그림 칸 폭 · 379-2 카드 면적표 걷음) ·
`_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 를 돌림).
@@ -10,7 +10,6 @@ from __future__ import annotations
import json
import os
import re
import subprocess
import pytest
@@ -40,16 +39,14 @@ def fit() -> dict:
return json.loads(proc.stdout)
# ── 173-3 면적표 ─────────────────────────────────────────────
# ── 173-3 그림 칸 글자 · 379-2 카드 좌상 면적표 걷음 ─────────────────
def test_면적표_글자_그림_칸_폭_비례_높이_변수_같은_식():
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: var(--b06-areas-font);" in css # 남은 쓰는 곳
assert "font-size: 0.72rem" not in css
# 3열 그림 칸 ≈ 400 px → 지금 글자(0.72rem)와 거의 같음 · 1열 · 6열은 끝값
@@ -60,16 +57,18 @@ def test_면적표_글자_그림_칸_폭_비례_높이_변수_같은_식():
assert rem(1232) == 0.9 and rem(192) == pytest.approx(0.634)
def test_면적표_4열부터_계만_접기():
def test_카드_좌상_면적표_걷음_조정창은_위에서():
"""379-2 — 카드 좌상 면적표 지움 · 조정창 top = spacing-8 · 모달 표는 그대로."""
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(cols);" in src # 실제 그린 열 수(휴대폰 폭 = 1 · 기기대응)
assert "b06-cross-card__areas" not in css and "--b06-areas-height" not in css
assert "buildAreaReadout" not in _read("B06_Section_UI_Cross_Areas.ts")
view = _read("B06_Section_UI_Cross_View.ts")
assert "buildAreaReadout" not in view and "setChipActive" not in view
controls = _read("B06_Section_UI_Style_Cross_Controls.css")
panel = controls.split(".b06-structure-panel {", 1)[1].split("}", 1)[0]
assert "top: var(--spacing-8);" in panel and "--b06-areas-height" not in controls
table = os.path.join(ROOT, "B07_DesignDetail", "B07_DesignDetail_Engine_Cad_Table.py")
assert os.path.isfile(table) # 모달 표 그대로
# ── 173-4 [상세] 그림 ────────────────────────────────────────
+39 -3
View File
@@ -80,14 +80,40 @@ def test_center_tag_on_y_axis(result: dict) -> None:
assert center["text_x"] == center["tick_x"] # 글 오른끝 = 다른 눈금 자리
assert center["tick_near_center"] >= 1 # 겹친 눈금 글도 그대로(319-1)
assert center["on_top"] == [ # 상자 · 글을 맨 뒤에 붙여 위에서 가림(319-1)
"b06-chart__center-box",
"b06-chart__tick b06-chart__center-text",
"b06-chart__center-box b06-chart__center-tag--ground",
"b06-chart__tick b06-chart__center-text b06-chart__center-tag--ground",
]
assert center["tick_count"] > 3
assert center["hidden_when_out_of_view"]
assert center["absent_without_center"]
def test_two_tags_ground_dodges(result: dict) -> None:
"""379-3 — 원지반 상자 먼저 · 계획 상자 나중(위) · 16 px 안이면 원지반만 비킴 · 값 그대로."""
tags = result["tags"]
apart = tags["apart"]
assert abs(apart["ground"]["mid"] - apart["groundY"]) < 0.01 # 멀면 제자리
assert abs(apart["design"]["mid"] - apart["designY"]) < 0.01
assert [c.split()[-1] for c in apart["order"]] == [
"b06-chart__center-tag--ground",
"b06-chart__center-tag--ground",
"b06-chart__center-tag--design",
"b06-chart__center-tag--design",
]
low = tags["groundLow"] # 원지반이 낮음 → 아래로
assert abs(low["design"]["mid"] - low["designY"]) < 0.01
assert abs(low["ground"]["mid"] - (low["designY"] + 16)) < 0.01
assert low["ground"]["text"] == "876.6" and low["design"]["text"] == "876.9"
high = tags["groundHigh"] # 원지반이 높음 → 위로
assert abs(high["ground"]["mid"] - (high["designY"] - 16)) < 0.01
assert high["ground"]["text"] == "876.9" and high["design"]["text"] == "876.6"
edge = tags["edge"] # 아래가 칸 밖 → 반대(위)
assert edge["groundY"] > edge["designY"]
assert abs(edge["ground"]["mid"] - (edge["designY"] - 16)) < 0.01
only = tags["designOnly"]
assert only.get("ground") is None and abs(only["design"]["mid"] - only["designY"]) < 0.01
def test_footer_without_center_text(result: dict) -> None:
footer = result["footer"]
assert "중심고" not in footer["rock_text"]
@@ -109,6 +135,13 @@ def test_center_wired_into_card_and_modal() -> None:
assert "centerZ: section.center_z ?? null" in _src(
"B06_Section", "B06_Section_UI_Section_View.ts"
)
# 379-3 계획 상자 = 십자선 계획고(카드 designElevation · 모달 cardDesignElevation)
assert "designZ: hasDesign ? (designElevation as number) : null" in _src(
"B06_Section", "B06_Section_UI_Cross_View.ts"
)
assert "designZ: cardDesignElevation(detail, section) ?? null" in _src(
"B06_Section", "B06_Section_UI_Section_View.ts"
)
# ── 318-2 · 318-3 양식 ──────────────────────────────────────────────────────
@@ -127,7 +160,10 @@ def test_css_layout() -> None:
assert "position: absolute" not in rock and "translateX" not in rock # 왼쪽 맞춤
assert 'content: "\\200b"' in _rule(css, ".b06-cross-card > footer:empty::before")
box = _rule(css, ".b06-chart__center-box")
assert "var(--color-accent)" in box # 테마 색
assert "var(--b06-tag)" in box # 색 = 갈래 변수(379-3)
areas = _src("B06_Section", "B06_Section_UI_Style_Cross_Areas.css")
assert "var(--color-chart-1)" in _rule(areas, ".b06-chart__center-tag--ground") # 원지반선
assert "var(--color-royal-amethyst)" in _rule(areas, ".b06-chart__center-tag--design") # 설계선
assert "font-size" not in _rule(css, ".b06-chart__center-text") # 글자 크기 = 눈금 그대로
+14 -3
View File
@@ -1,6 +1,6 @@
"""PLAN 370-2 — B06 사면 이동 멈춤 · 교차점 손잡이 · 고름 이어받기(요구 240-14).
짝: `B06_Section/logic/B06_Section_UI_Cross_Fit.ts`(`extendStop` · `toePoints`) ·
짝: `B06_Section/logic/B06_Section_UI_Cross_Fit.ts`(`extendStop` · `toePoints` · `edgeOnGround`) ·
`B06_Section/B06_Section_UI_Cross_Design_Pick.ts`(손잡이 · 키 이어받기). 실제 TS 를
`helper_370_2_toe_stop.cjs` 가 Node 로 돌린다.
"""
@@ -63,7 +63,7 @@ def test_교차점_손잡이_쪽마다_늘_보임(out) -> None:
def test_손잡이를_고르면_그_쪽_사면_좌우_이동(out) -> None:
got = out["toePick"]
assert got["active"] is True and got["panelOpen"] is True
assert got["title"] == "교차점 이동 (우)" # 아래 띠 공용 칸(370-3)
assert got["title"] == "사면 이동 (우)" # 아래 띠 공용 칸(370-3)
assert got["value"].startswith("노선에서 ")
assert len(got["sent"]) == 1 and got["sent"][0]["side"] == "right"
low, high = out["stop"]["crossing"]
@@ -76,4 +76,15 @@ def test_절에서_성으로_넘어가도_고름_유지(out) -> None:
assert inherit["cutIndex"] >= 0 and inherit["before"] == inherit["cutIndex"]
assert inherit["farMode"] == "left_cut" # 우 = 성토로 넘어감
assert inherit["afterIndex"] is not None
assert inherit["label"] == "교차점 이동 (우)"
assert inherit["label"] == "사면 이동 (우)"
def test_정확히_0_자리는_절성토_없음_사면처럼_고름(out) -> None:
"""379-1 — 멈춘 자리(노견 끝 = 원지반) 손잡이를 누르면 사면과 같은 조작."""
zero = out["zero"]
assert zero["bare"]["right"] is True and zero["nearBare"]["right"] is False
assert zero["toe"] is True and zero["radius"] == "8" # 판정 원 8 px
assert zero["title"] == "사면 이동 (우)"
assert zero["value"].startswith("절·성토 없음 · 노선에서 ")
assert zero["buttons"] == ["left", "reset", "right"] # ◀↺▶
assert zero["keys"] >= 1 # 방향키 듣개
-14
View File
@@ -424,23 +424,9 @@ export const ui_locales_b2 = {
B06_Design_Ditch_Legend: ["측구위치", "Ditch side"],
B06_Design_Ditch_Left: ["좌", "Left"],
B06_Design_Ditch_Right: ["우", "Right"],
B06_Design_Cut_Area: ["절토", "Cut"],
/* 절토 내역 — 지표면~암반 경계선이 토사, 그 아래가 암반(합=절토). */
B06_Design_Cut_Soil_Area: ["절토(토사)", "Cut (soil)"],
B06_Design_Cut_Rock_Area: ["절토(암반)", "Cut (rock)"],
/* 횡단 면적표 열 이름은 유토곡선 도면 표기를 그대로 쓴다(EA 토사 / RR 리핑암 / BR 발파암). */
B06_Design_Area_EA: ["EA", "EA"],
B06_Design_Area_RR: ["RR", "RR"],
B06_Design_Area_BR: ["BR", "BR"],
B06_Design_Area_Total: ["계", "Total"],
/* 단위는 값 칸마다 붙이지 않고 표 좌상단(행제목 × 열제목 교차) 칸에 한 번만 적는다. */
B06_Design_Area_Unit: ["㎡", "㎡"],
B06_Design_Area_Highlight: ["누르면 해당 면적을 강조합니다", "Click to highlight this area"],
B06_Design_Fill_Area: ["성토", "Fill"],
// 사토장(유용토운반작업장) — **성토와 갈라 보인다**. 합치면 「쌓기」가 갑자기 커 보여
// 사람이 오해한다(2026-09-09 네 창 확정 ㉠).
B06_Design_SpoilFill_Area: ["사토장", "Spoil site"],
B06_Design_Unset: ["미지정", "Not set"],
B06_Design_DitchType_Legend: ["측구형식", "Ditch type"],
B06_Design_DitchType_Standard: ["일반", "Standard"],
B06_Design_DitchType_LType: ["L형", "L-type"],