Files
Aislo/B08_Quantity/B08_Quantity_UI_Milestone.ts
T
eomsangdonandClaude Opus 5.5 5f2e6e18b6 feat(b08): 거리이정표 [+] 모달 · 주소 검색 재활용 · 방위 자동 · 시점 모달 · 팬 줌 · 경로 탭 = 길찾기 그림 (244-4)
- 공사지 · 기준점 [+] = 모달(항목 · 업체 이름 · 목적지 + B01 주소 검색 · 지도) · 방위 고르기 없앰 · 좌표로 자동 자리
- 상자 누름 = 모달(업체 = 항목 정보 · 구간 km · 도로 구분 · 고침 · 삭제 / 공사지 = 등록 주소 · VWorld 주소 · 지도 / 기준점 = 공통 구간)
- 다이어그램 팬 · 줌 = 공용 b_svg_zoom_pan
- 경로 하위 탭 = 그림만 · [다음 지도에서 길찾기] 새 창 · [화면 캡처] · 하위 탭 [추가] · [삭제] 없앰
- 서버 site-address(좌표 → 주소 · VWorld getAddress)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz
2026-10-10 21:33:53 +09:00

174 lines
6.7 KiB
TypeScript

/* =============================================================================
* B08_Quantity_UI_Milestone.ts
* B08 메인 탭 「거리이정표」(PLAN 244-1 · 244-4) — 하위 탭 줄 [거리이정표] + 경로 탭(업체마다 · 이름 = 업체 이름).
* 경로 탭은 다이어그램 [+] 등록 · 항목 정보 모달 [삭제]로만 생기고 없어짐(하위 탭 줄 [추가] · [삭제] 없음).
*
* [거리이정표] = 표(항목 · 업체 이름 · 목적지 ▾ · 2차포장 · 1차포장 · 비포장 · 포장 · 비포장 · 합계 · 비고)
* 표 합 = `…_Milestone_Calc` TS 짝(조작 즉시) · [저장] 때 서버가 파이썬 짝으로 다시 셈
* 입력 = 더블클릭(243) · 업체 이름을 고치면 그 경로 이름(하위 탭)도
* 위쪽 다이어그램 = `…_UI_Milestone_Diagram`(상자 누름 = 모달 · [+] = 항목 추가 모달 · 팬 · 줌)
* 경로 탭 = `…_UI_Milestone_Route`(길찾기 지도 그림만) · 데이터 · [저장] = `…_UI_Milestone_State`
* ========================================================================== */
import "./B08_Quantity_UI_Milestone.css";
import { createDataTable, type DataColumn } from "@ui/ui_template_data_table";
import { createButton, el, showToast } from "@ui/ui_template_elements";
import { computeMilestone, type MilestoneRoute } from "./B08_Quantity_Milestone_Calc";
import { createDiagram } from "./B08_Quantity_UI_Milestone_Diagram";
import {
liveMilestone,
loadMilestone,
onMilestoneChange,
putMilestone,
siteInfo,
} from "./B08_Quantity_UI_Milestone_State";
import {
DEST_TEXT,
editRoute,
pickAtCell,
routeView,
trackCells,
type MilestoneView,
} from "./B08_Quantity_UI_Milestone_Route";
const KM = { kind: "number", decimals: 3, trim: true, width: 72 } as const;
const COLUMNS: DataColumn[] = [
{ key: "item", label: "항목", width: 120 },
{ key: "company", label: "업체 이름", width: 120 },
{ key: "dest", label: "목적지", width: 72 },
{ key: "s2", label: "2차포장", group: ["도로 구분 (km)"], ...KM },
{ key: "s1", label: "1차포장", group: ["도로 구분 (km)"], ...KM },
{ key: "un", label: "비포장", group: ["도로 구분 (km)"], ...KM },
{ key: "paved", label: "포장", group: ["계 (km)"], ...KM },
{ key: "unpaved", label: "비포장", group: ["계 (km)"], ...KM },
{ key: "total", label: "합계", group: ["계 (km)"], ...KM },
{ key: "note", label: "비고", width: 160 },
];
/** 새 경로(업체) — 캐시에 넣고 id 를 돌려줌 */
export function addRoute(projectId: string, init: Partial<MilestoneRoute> = {}): string {
const data = liveMilestone(projectId);
const id = `r${Date.now().toString(36)}${Math.floor(Math.random() * 1296).toString(36)}`;
data.routes.push({
id,
name: `업체 ${data.routes.length + 1}`,
address: "",
via_base: true,
dir: null,
legs: [{ road: "2차포장", km: null }],
rows: [{ item: "", dest: "start", note: "" }],
...init,
});
putMilestone(projectId, data);
return id;
}
/** [거리이정표] 하위 탭 — 위 다이어그램 + 표 */
function tableView(projectId: string): MilestoneView {
const diagram = createDiagram(projectId, (init) => addRoute(projectId, init));
let keys: Array<{ route: string; index: number }> = [];
const table = createDataTable({
columns: COLUMNS,
rows: [],
caption: "거리이정표",
empty: "",
editOnDouble: true,
editable: (_i, key) => key === "item" || key === "company" || key === "note",
onEdit: (i, key, text) => {
const at = keys[i];
if (!at) return;
editRoute(projectId, at.route, (_, r) => {
if (key === "company") r.name = text;
else if (r.rows[at.index]) r.rows[at.index][key as "item" | "note"] = text;
});
},
onCellClick: (i, key) => {
const at = keys[i];
if (key !== "dest" || !at) return;
pickAtCell(
(["start", "mid"] as const).map((dest) => [
DEST_TEXT[dest],
() =>
editRoute(projectId, at.route, (_, r) => {
if (r.rows[at.index]) r.rows[at.index].dest = dest;
}),
]),
);
},
});
trackCells(table.root);
table.root.dataset.b08Milestone = "table";
const none = el("p", {
className: "ui-general-block__empty",
text: "경로가 없습니다 — 다이어그램 공사지 · 기준점 [+]로 항목을 넣으세요.",
});
const refresh = (): void => {
const rows = computeMilestone(liveMilestone(projectId), siteInfo()?.mid_default_m ?? null);
keys = rows.map(({ route, index }) => ({ route, index }));
none.hidden = rows.length > 0;
table.setRows(rows.map((r) => ({ ...r, dest: `${DEST_TEXT[r.dest]} ▾` })));
diagram.refresh();
};
refresh();
return {
root: el("div", { className: "b08-milestone", children: [diagram.root, none, table.root] }),
refresh,
};
}
export function renderMilestone(host: HTMLElement, projectId: string): void {
const tabs = el("div", {
className: "b08-quantity__tabs b08-structure__tabs",
attrs: { role: "tablist" },
});
const view = el("div");
let current = "table";
let body: MilestoneView | null = null;
const drawTabs = (): void => {
const routes = liveMilestone(projectId).routes;
const tab = (id: string, label: string): HTMLButtonElement => {
const button = createButton({
label,
variant: id === current ? "filled" : "ghost",
size: "sm",
onClick: () => id !== current && open(id),
});
button.setAttribute("role", "tab");
button.setAttribute("aria-selected", String(id === current));
button.dataset.b08MilestoneTab = id;
return button;
};
tabs.replaceChildren(
tab("table", "거리이정표"),
...routes.map((r, i) => tab(r.id, r.name.trim() || `업체 ${i + 1}`)),
);
};
function open(id: string): void {
current = id;
drawTabs();
body = id === "table" ? tableView(projectId) : routeView(projectId, id);
view.replaceChildren(body.root);
}
host.replaceChildren(
tabs,
el("p", { className: "ui-general-block__empty", text: "거리이정표를 불러오는 중…" }),
);
loadMilestone(projectId).then(
() => {
host.replaceChildren(tabs, view);
open("table");
onMilestoneChange(host, () => {
// 지운 경로 탭이면 [거리이정표] 로 · 아니면 탭 이름 · 몸만 다시 셈(입력 칸은 그대로)
if (current !== "table" && !liveMilestone(projectId).routes.some((r) => r.id === current))
return open("table");
drawTabs();
body?.refresh();
});
},
(error: unknown) => showToast(`거리이정표를 불러오지 못했습니다 — ${String(error)}`, "error"),
);
}