Files
Aislo/B08_Quantity/B08_Quantity_UI_Milestone.ts
T
eomsangdonandClaude Opus 5.5 271c460641 fix(부품·테마): 위험 채움 대비 4.72 · 남은 옛 꼴 → 부품(계산 줄 · 칸 램프 · 넓은 중간 창 · 규칙 꼴 · 구역 머리 · 입력 칸 2열) (PLAN 557-2)
- 위험 채움 토큰 밝음 #e5484d → #d6363c(흰 글 3.91 → 4.72) · 어두움 5.6 · 위험 글 #c0392b 그대로
- 새 부품 계산 줄(ui_template_calc_row) — B08 수리집수면적표 b08-hydro__row · 식 칸 배치를 대신함
- 표 부품 칸 램프(ui-data-table__lamp · --only · ✓) — B08 집계표 · 거리이정표 · B09 설계내역 램프 CSS 세 벌을 걷음(표지 class 는 풍선 · 가이드용으로 남김)
- 모달 넓은 중간 창(wide + width 1100px · 1100 × 680) — B09 운반 창 손 인라인 크기를 걷음
- 값 목록 규칙 꼴(rule) — ☰ 설정 규칙 줄 ul/li 를 걷음
- 접이 구역 머리(createCollapsibleHead = 패널 머리 + 접는 제목) — B08 구조물도 「조합 연결」 제목(옛 b08-sheets__title)을 대신함
- 일반 화면 틀 좁은 폭 · 입력 칸 2열 · 끝 단추 줄 — D01 · E01 이 B02 CSS 를 빌리던 것을 끊음(가이드 장면 선택자 맞춤)
- 부품 모음 견본 · 지킴 시험 test_557_2 · 낡은 시험(198_1 · 247_1 · 251_1 · 257_1 · 276_1 · 551_1)은 부품 CSS · 새 class 로 맞춤

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 10:53:34 +09:00

236 lines
9.4 KiB
TypeScript

/* =============================================================================
* B08_Quantity_UI_Milestone.ts
* B08 메인 탭 「거리이정표」(PLAN 244-1 · 244-4) — 하위 탭 줄 [거리이정표] + 경로 탭(거래처마다 · 이름 = 거래처 이름).
* 경로 탭은 다이어그램 [+] 등록 · 항목 정보 모달 [삭제]로만 생기고 없어짐(하위 탭 줄 [추가] · [삭제] 없음).
*
* [거리이정표] = 표(항목 · 거래처 이름 · 목적지 ▾ · 도로 구분 · 계 · 비고) — 열(244-13) = 기준점 있음 =
* 2차포장 · 1차포장 (· 포장) · 비포장 + 계 포장 · 비포장 · 합계 / 없음 = 포장 · 비포장 · 합계
* 표 합 = `…_Milestone_Calc` TS 짝(조작 즉시) · [저장] 때 서버가 파이썬 짝으로 다시 셈
* 입력 = 더블클릭(243) · 거래처 이름을 고치면 그 경로 이름(하위 탭)도
* 위쪽 다이어그램 = `…_UI_Milestone_Diagram`(상자 누름 = 모달 · [+] = 항목 추가 모달 · 팬 · 줌)
* 경로 탭 = `…_UI_Milestone_Route`(길찾기 지도 그림만) · 데이터 · [저장] = `…_UI_Milestone_State`
* 단가연결(251-4) = 항목 칸 앞 램프(초록 = 연결 · 노랑 = 없음 · 호버 = 무엇) · 줄 우클릭 [단가연결] ·
* [단가연결 풀기] — 창 = 연결 없음 → 단가연결 목록 → 옵션 값 창(276 · 옛 길) · 연결 있음 →
* 「운반 단가」 모달(253-3 · B09 운반공 줄과 같음 · 그 행 거리) · 경로 `ref` · B09 운반공 줄이 받음
* 표 열 · 줄 짜기 = `B08_Quantity_Milestone_Table`(화면 없음 · 인쇄 미리보기도 씀 · 302-3)
* ========================================================================== */
import "./B08_Quantity_UI_Milestone.css";
import { createDataTable, type DataColumn } from "@ui/ui_template_data_table";
import { el, showToast } from "@ui/ui_template_elements";
import { attachHoverInfo } from "@ui/ui_template_hover_info";
import { createText } from "@ui/ui_template_text";
import { fillTabs } from "../A00_Common/b_page_tabs";
import type { Row } from "../B09_Estimation/B09_Estimation_Calc_Base";
import { openHaulModal } from "../B09_Estimation/B09_Estimation_UI_Haul_Modal";
import { openPriceLink } from "../B09_Estimation/B09_Estimation_UI_Tabs_Bill_Link";
import type { MilestoneRoute } from "./B08_Quantity_Milestone_Calc";
import { DEST_TEXT, milestoneColumns, milestoneTable } from "./B08_Quantity_Milestone_Table";
import { createDiagram } from "./B08_Quantity_UI_Milestone_Diagram";
import {
liveMilestone,
loadMilestone,
onMilestoneChange,
putMilestone,
siteInfo,
} from "./B08_Quantity_UI_Milestone_State";
import {
editRoute,
pickAtCell,
routeView,
trackCells,
type MilestoneView,
} from "./B08_Quantity_UI_Milestone_Route";
/** 단가연결 램프 칸 표지(풍선이 잡음) · 꼴 = 표 부품 칸 램프 */
const LAMP = "b08-milestone__lamp";
/** 램프 호버 글 — 연결된 것 */
export function refText(ref: Record<string, unknown> | null | undefined): string {
if (!ref) return "단가연결 없음 — 줄 우클릭 [단가연결]";
const what =
ref.tab === "logic"
? `단가산출 ${String(ref.code ?? "")} ${String(ref.name ?? "")}`
: `일위대가 ${String(ref.name ?? ref.key ?? "")}`;
return `단가연결 — ${what.trim()}`;
}
/** 새 경로(거래처) — 캐시에 넣고 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: "",
to: "site",
dir: null,
legs: [{ road: null, km: null }], // 도로 구분 = 사용자 고름(244-7)
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; L1: number; L2: number }> = [];
const routeAt = (i: number) =>
liveMilestone(projectId).routes.find((r) => r.id === keys[i]?.route);
const makeTable = (columns: DataColumn[]) => {
const t = createDataTable({
columns,
rows: [],
caption: "거리이정표",
empty: "",
editOnDouble: true,
cellClass: (i, key) =>
key !== "item"
? null
: routeAt(i)?.ref
? `ui-data-table__lamp ${LAMP} is-linked`
: `ui-data-table__lamp ${LAMP}`,
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(t.root);
t.root.dataset.b08Milestone = "table";
attachHoverInfo(
t.root,
(cell) => refText(routeAt(Number(cell.closest("tr")?.dataset.i))?.ref),
`td.${LAMP}`,
);
t.root.addEventListener("contextmenu", (event) => {
const tr = (event.target as Element).closest<HTMLElement>("tr[data-i]");
const route = tr ? routeAt(Number(tr.dataset.i)) : undefined;
if (!route) return;
event.preventDefault();
const link = (ref: Row | null, from?: Row): void =>
editRoute(projectId, route.id, (_, r) => {
// 호버 글 이름(사전조합 · 구조물 조합 = 들여온 대가 이름) — B09 는 tab · key 만 읽음
r.ref = ref && { ...ref, name: ref.name ?? from?.name ?? "" };
});
const at = keys[Number(tr?.dataset.i)];
pickAtCell([
[
"단가연결",
() =>
route.ref
? openHaulModal({
ref: route.ref as Row,
dist: at && { L1: at.L1, L2: at.L2 },
name: route.rows[at?.index ?? 0]?.item || route.name || "운반",
apply: link,
})
: void openPriceLink({ apply: link }),
],
...(route.ref ? [["단가연결 풀기", () => link(null)] as [string, () => void]] : []),
]);
});
return t;
};
let mode = "false:false";
let table = makeTable(milestoneColumns(false, false));
const none = createText(
"경로가 없습니다 — 다이어그램 공사지 · 기준점 [+]로 항목을 넣으세요.",
"muted",
14,
);
const refresh = (): void => {
const data = liveMilestone(projectId);
const { options, rows, mode: next } = milestoneTable(data, siteInfo()?.mid_default_m ?? null);
// 기준점 생김 · 없앰 = 열 바뀜 → 표 새로
if (next !== mode) {
mode = next;
const fresh = makeTable(options.columns);
table.root.replaceWith(fresh.root);
table = fresh;
}
keys = rows.map(({ route, index, paved, unpaved }) => ({
route,
index,
L1: paved,
L2: unpaved,
}));
none.hidden = rows.length > 0;
table.setRows(options.rows.map((r) => ({ ...r, dest: `${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",
});
const view = el("div");
let current = "table";
let body: MilestoneView | null = null;
const drawTabs = (): void => {
const routes = liveMilestone(projectId).routes;
fillTabs(tabs, {
tabs: [
{ key: "table", label: "거리이정표" },
...routes.map((r, i) => ({
key: r.id,
label: r.name.trim() || r.rows[0]?.item || `거래처 ${i + 1}`,
})),
],
value: current,
kind: "pill",
data: "b08MilestoneTab",
onChange: open,
});
};
function open(id: string): void {
current = id;
drawTabs();
body = id === "table" ? tableView(projectId) : routeView(projectId, id);
view.replaceChildren(body.root);
}
host.replaceChildren(tabs, createText("거리이정표를 불러오는 중…", "muted", 14));
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"),
);
}