Files
Aislo/B07_DesignDetail/B07_DesignDetail_UI_Panels.ts
T
eomsangdonandClaude Opus 5 6dd0dec93a feat(B07): 계획평면도 3종 — 수치등고선 배경 위 노선·측점·구조물
빈 도각이던 계획평면도(지형·노선배치도·배치도)를 실제 도면으로 만듦.

- 배경 공용화: map_background() 한 창구로 도엽 등고선·세류선 읽기·좌표 환산·
  도곽 절취를 모음. 유역도와 계획평면도가 같은 것을 부르고, 환산 결과는
  파일 mtime 을 키로 캐시(_metric_lines_cached).
- 축척 1/1,200 고정(DRAWING_SCALE_PLAN) — 지식DB 「설계제원_총괄」 측량·도면
  기준. 횡단면도와 같은 원칙으로 줄이지 않고 안 들어가면 장을 나눔(plan_chunks,
  종단 측점 기준·경계 측점 1개 중복).
- 세 장이 같은 배경·같은 도곽 배치를 쓰고 주제만 다름. 측점 눈금은 종단 측점
  좌표로 찍고, 구조물은 pipe_points.json 정본을 읽어 마름모+이름으로 표기.
- 도면 목록·단건 조회에 kind="plan" 추가. 화면 목록은 id 접두어로 묶어 장이
  나뉘어도 한 그룹으로 보임.

검증(용화_LAS): 콘텐츠 734.3x489.2 mm ≤ A1 작도영역 739.2x499.2, 노선 실거리
630.1x214.2 m → 종이 525.12x178.46 mm(실측 0.83333 mm/m = 1/1,200 일치),
배경 등고선 180줄이 세 장 동일, 유역도 8.5초 → 계획평면도 1.3초(캐시 적중,
파일 재읽기 없음).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 18:38:43 +09:00

263 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* B07_DesignDetail_UI_Panels.ts
* 상세 설계 화면의 좌측 패널 — 도면 목록 12분류, 측점 설계 정보 카드.
*
* 화면 조립(`B07_DesignDetail_UI_Page.ts`)이 700줄을 넘어 떼어냈다(2026-09-04).
* DOM 만 만들고 상태는 갖지 않는다 — 고르기 동작은 인자로 받은 콜백에 넘긴다.
* ========================================================================== */
import { attachCollapsible } from "@ui/ui_template_collapsible";
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
import type {
CrossDesignInfo,
DesignDrawingItem,
} from "./B07_DesignDetail_Api_Fetch";
function L(key: keyof typeof ui_locales): string {
return ui_locales[key][currentLanguageIndex];
}
/**
* 도면 구성 12분류 (2026-08-29 사용자 확정 순서).
*
* 아직 내용을 만들지 않은 도면은 `blankId`로 서버의 빈 도각 도면에 물린다
* (2026-09-01 사용자 지시) — 눌리지 않는 회색 버튼으로 두면 고장난 것처럼 보인다.
*/
export const DRAWING_GROUPS: readonly {
label: string;
kind?: DesignDrawingItem["kind"];
blankId?: string;
/** 축척 고정으로 장이 나뉘는 도면 — `plan_route`, `plan_route_2` … 를 한 묶음으로 본다. */
idPrefix?: string;
}[] = [
{ label: "표지", kind: "cover" },
{ label: "계획평면도(지형)", idPrefix: "plan_terrain" },
{ label: "계획평면도(노선배치도)", idPrefix: "plan_route" },
{ label: "계획평면도(배치도)", idPrefix: "plan_layout" },
{ label: "계획평면도(라이다)", blankId: "blank_plan_lidar" },
{ label: "종단면도", kind: "longitudinal" },
{ label: "표준 횡단면도", blankId: "blank_cross_standard" },
{ label: "횡단면도", kind: "cross" },
{ label: "토적도(유토곡선)", kind: "mass_haul" },
{ label: "유역도(배수 유역도)", kind: "watershed" },
{ label: "표준도", blankId: "blank_standard" },
{ label: "용지도", blankId: "blank_landuse" },
];
/** B06 확정 산출물 기반 도면 목록 패널. */
export function buildDrawingSidePanel(
drawings: DesignDrawingItem[],
onSelect: (drawing: DesignDrawingItem) => void,
errorMessage?: string,
): HTMLDivElement {
const panel = document.createElement("div");
panel.className = "b07-drawing-list";
const heading = document.createElement("div");
heading.className = "b07-drawing-list__heading";
const title = document.createElement("strong");
title.textContent = "설계 도면";
const count = document.createElement("span");
count.textContent = `${drawings.length}건`;
heading.append(title, count);
panel.append(heading);
if (errorMessage || drawings.length === 0) {
const empty = document.createElement("p");
empty.className = "b07-drawing-list__empty";
empty.textContent = errorMessage ?? "확정된 종·횡단 도면이 없습니다.";
panel.append(empty);
return panel;
}
const drawingButton = (
drawing: DesignDrawingItem,
label: string,
): HTMLButtonElement => {
const button = document.createElement("button");
button.type = "button";
button.className = "b07-drawing-button";
button.dataset.drawingId = drawing.id;
button.dataset.confirmed = String(drawing.confirmed);
const name = document.createElement("span");
name.className = "b07-drawing-button__name";
name.textContent = label;
button.append(name);
button.addEventListener("click", () => onSelect(drawing));
return button;
};
for (const group of DRAWING_GROUPS) {
const items = group.kind
? drawings.filter((item) => item.kind === group.kind)
: group.idPrefix
? drawings.filter(
(item) =>
item.id === group.idPrefix ||
item.id.startsWith(`${group.idPrefix}_`),
)
: drawings.filter((item) => item.id === group.blankId);
// 한 장짜리(와 아직 내용이 없는 도면)는 컨테이너 없이 버튼 하나로 둔다.
if (items.length <= 1) {
const [drawing] = items;
// 서버가 빈 도각을 내주지 못한 경우에만 회색 버튼으로 남는다.
if (!drawing) {
const placeholder = document.createElement("button");
placeholder.type = "button";
placeholder.className = "b07-drawing-button";
placeholder.disabled = true;
placeholder.title = "준비 중";
placeholder.dataset.pending = "true";
const name = document.createElement("span");
name.className = "b07-drawing-button__name";
name.textContent = group.label;
placeholder.append(name);
panel.append(placeholder);
continue;
}
const button = drawingButton(drawing, group.label);
// 도각만 있는 도면은 그렇다고 알린다 — 빈 화면을 보고 오류로 오해하지 않게.
button.dataset.pending = String(drawing.kind === "blank");
if (drawing.kind === "blank")
button.title = "준비 중 — 도각만 표시합니다";
panel.append(button);
continue;
}
const section = document.createElement("section");
// ui-sidebar-section: 사이드 컨테이너 공통 외곽선(B04~B06과 통일, 2026-08-06 사용자 지시).
section.className = "b07-drawing-group ui-collapsible ui-sidebar-section";
if (group.kind) section.dataset.kind = group.kind;
const sectionTitle = document.createElement("h3");
sectionTitle.className = "ui-collapsible__title";
sectionTitle.textContent = `${group.label} ${items.length}`;
section.append(sectionTitle);
for (const drawing of items) {
// 횡단면도는 장 단위(여러 측점)라 서버가 준 "N장 (구간)" 라벨을 그대로 쓴다.
section.append(drawingButton(drawing, drawing.label));
}
panel.append(section);
}
attachCollapsible(panel);
return panel;
}
const GROUND_TYPE_LABEL: Record<
CrossDesignInfo["ground_type"],
keyof typeof ui_locales
> = {
soil: "B06_Design_Ground_Soil",
ripping_rock: "B06_Design_Ground_Ripping",
blasting_rock: "B06_Design_Ground_Blasting",
};
/** 단면유형에서 절토측 표기를 유도한다. */
function cutSideLabel(mode: CrossDesignInfo["section_mode"]): string {
if (mode === "left_cut") return L("B06_Design_Ditch_Left");
if (mode === "right_cut") return L("B06_Design_Ditch_Right");
if (mode === "both_cut") return L("B06_Design_Mode_BothCut");
return L("B06_Design_Mode_BothFill");
}
/** 장(여러 측점을 담은 횡단 도면)인가 — id 가 `cross_s…` 면 장이다. */
export function isCrossSheet(drawing: DesignDrawingItem): boolean {
return drawing.kind === "cross" && drawing.id.startsWith("cross_s");
}
/** 측구 규격 표시 문자열 (design 신구조: 형식별 ditch spec, F-2 호환). */
function ditchLabel(design: CrossDesignInfo): string {
const ditch = design.ditch;
if (!ditch || ditch.type === "none" || design.ditch_enabled === false)
return "없음";
if (ditch.type === "l_type")
return `L형 ${ditch.width_m.toFixed(2)}×${ditch.depth_m.toFixed(2)}m`;
return `${ditch.top_width_m.toFixed(2)}×${ditch.depth_m.toFixed(2)}m`;
}
function infoRow(label: string, value: string): HTMLElement {
const row = document.createElement("div");
row.className = "b07-info__row";
const key = document.createElement("span");
key.className = "b07-info__key";
key.textContent = label;
const val = document.createElement("span");
val.className = "b07-info__val";
val.textContent = value;
row.append(key, val);
return row;
}
/**
* 선택 횡단도의 지반정보/계획정보를 2분할로 렌더한다 (잠정치, B07 확정 시 재계산).
*
* **장(여러 측점을 담은 도면)에는 측점 단위 값이 없다** — 서버가 `design` 을 넘기지
* 않는데도 제목만 「측점 …」으로 달려 어느 측점 값인지 오해됐다(2026-09-03 정리).
* 장이면 제목을 「장」으로 바꾸고 어디서 봐야 하는지 한 줄로 알린다.
*/
export function buildDesignInfoPanel(
title: string,
design: CrossDesignInfo | null,
scope: "station" | "sheet" = "station",
): HTMLElement {
const panel = document.createElement("div");
panel.className = "b07-info";
const heading = document.createElement("div");
heading.className = "b07-info__heading";
const stationName = document.createElement("strong");
const scopeLabel =
scope === "sheet" ? L("B07_Info_Sheet") : L("B07_Info_Station");
stationName.textContent = `${scopeLabel} ${title}`;
const confirmed = design?.status === "confirmed";
const badge = document.createElement("span");
badge.className = `b07-info__badge${confirmed ? " b07-info__badge--confirmed" : ""}`;
badge.textContent = confirmed
? L("B07_Info_Confirmed")
: L("B07_Info_Provisional");
heading.append(stationName, badge);
panel.append(heading);
if (!design) {
const empty = document.createElement("p");
empty.className = "b07-info__empty";
empty.textContent =
scope === "sheet" ? L("B07_Info_SheetHint") : L("B07_Info_NoDesign");
panel.append(empty);
return panel;
}
const ground = document.createElement("section");
ground.className = "b07-info__block";
const groundTitle = document.createElement("h4");
groundTitle.textContent = L("B07_Info_Ground_Title");
ground.append(
groundTitle,
infoRow(L("B07_Info_GroundType"), L(GROUND_TYPE_LABEL[design.ground_type])),
infoRow(L("B07_Info_CutSide"), cutSideLabel(design.section_mode)),
infoRow(
L("B07_Info_DitchSide"),
design.ditch_side === "left"
? L("B06_Design_Ditch_Left")
: L("B06_Design_Ditch_Right"),
),
);
const plan = document.createElement("section");
plan.className = "b07-info__block";
const planTitle = document.createElement("h4");
planTitle.textContent = L("B07_Info_Plan_Title");
plan.append(
planTitle,
infoRow(
L("B07_Info_DesignElevation"),
`${design.design_elevation_m.toFixed(2)}m`,
),
infoRow(L("B07_Info_CutSlope"), `1:${design.cut_slope_ratio}`),
infoRow(L("B07_Info_FillSlope"), `1:${design.fill_slope_ratio}`),
infoRow(L("B07_Info_RoadWidth"), `${design.roadbed_width_m.toFixed(2)}m`),
infoRow(L("B07_Info_Ditch"), ditchLabel(design)),
infoRow(L("B07_Info_CutArea"), `${design.cut_area_m2.toFixed(2)}㎡`),
infoRow(L("B07_Info_FillArea"), `${design.fill_area_m2.toFixed(2)}㎡`),
);
panel.append(ground, plan);
return panel;
}