From 8eb389eb399e51c6817e46f50fea50643887d462 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 11 Oct 2026 01:29:32 +0900 Subject: [PATCH] =?UTF-8?q?feat(b08):=20=EA=B1=B0=EB=A6=AC=EC=9D=B4?= =?UTF-8?q?=EC=A0=95=ED=91=9C=20=EB=8B=A8=EA=B0=80=EC=97=B0=EA=B2=B0=20?= =?UTF-8?q?=EB=9E=A8=ED=94=84=20=C2=B7=20=EC=9A=B0=ED=81=B4=EB=A6=AD=20?= =?UTF-8?q?=EB=8B=A8=EA=B0=80=EC=97=B0=EA=B2=B0=20=C2=B7=20=EB=8B=A4?= =?UTF-8?q?=EC=9D=B4=EC=96=B4=EA=B7=B8=EB=9E=A8=20=ED=8B=80=20=EB=86=92?= =?UTF-8?q?=EC=9D=B4=20=EB=A7=9E=EC=B6=A4=20(251-4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LLCk2y5xE8YGVgjbTntWsY --- B08_Quantity/B08_Quantity_UI_Milestone.css | 16 +++++++ B08_Quantity/B08_Quantity_UI_Milestone.ts | 44 ++++++++++++++++++- .../B08_Quantity_UI_Milestone_Diagram.ts | 7 ++- .../B08_Quantity_UI_Milestone_Route.ts | 3 +- resources/tester/test_251_1_haul_groups.py | 35 +++++++++++++++ 5 files changed, 100 insertions(+), 5 deletions(-) diff --git a/B08_Quantity/B08_Quantity_UI_Milestone.css b/B08_Quantity/B08_Quantity_UI_Milestone.css index 2a210aa61..fe859ca05 100644 --- a/B08_Quantity/B08_Quantity_UI_Milestone.css +++ b/B08_Quantity/B08_Quantity_UI_Milestone.css @@ -268,3 +268,19 @@ .b08-milestone__route-addr { color: var(--color-text-secondary); } + +/* 단가연결 램프(251-4) — 항목 칸 앞 · 노랑 = 없음 · 초록 = 연결 */ +[data-b08-milestone="table"] td.b08-milestone__lamp::before { + content: ""; + display: inline-block; + width: 8px; + height: 8px; + margin-right: 6px; + border-radius: 50%; + background: var(--color-warning); + vertical-align: middle; +} + +[data-b08-milestone="table"] td.b08-milestone__lamp.is-linked::before { + background: var(--color-success); +} diff --git a/B08_Quantity/B08_Quantity_UI_Milestone.ts b/B08_Quantity/B08_Quantity_UI_Milestone.ts index 05058419a..148f27c34 100644 --- a/B08_Quantity/B08_Quantity_UI_Milestone.ts +++ b/B08_Quantity/B08_Quantity_UI_Milestone.ts @@ -9,11 +9,16 @@ * 입력 = 더블클릭(243) · 거래처 이름을 고치면 그 경로 이름(하위 탭)도 * 위쪽 다이어그램 = `…_UI_Milestone_Diagram`(상자 누름 = 모달 · [+] = 항목 추가 모달 · 팬 · 줌) * 경로 탭 = `…_UI_Milestone_Route`(길찾기 지도 그림만) · 데이터 · [저장] = `…_UI_Milestone_State` + * 단가연결(251-4) = 항목 칸 앞 램프(초록 = 연결 · 노랑 = 없음 · 호버 = 무엇) · 줄 우클릭 [단가연결] · + * [단가연결 풀기] — 창 = B09 설계내역서와 같은 `openPriceLink` · 경로 `ref` · B09 운반공 줄이 받음 * ========================================================================== */ 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 { attachHoverInfo } from "@ui/ui_template_hover_info"; +import type { Row } from "../B09_Estimation/B09_Estimation_Calc_Base"; +import { openPriceLink } from "../B09_Estimation/B09_Estimation_UI_Tabs_Bill_Link"; import { computeMilestone, type MilestoneRoute } from "./B08_Quantity_Milestone_Calc"; import { createDiagram } from "./B08_Quantity_UI_Milestone_Diagram"; import { @@ -35,6 +40,18 @@ import { const KM = { kind: "number", decimals: 3, trim: true, width: 72 } as const; const ROAD = ["도로 구분 (km)"]; const SUM = ["계 (km)"]; +/** 단가연결 램프 칸 class(`B08_Quantity_UI_Milestone.css`) */ +const LAMP = "b08-milestone__lamp"; + +/** 램프 호버 글 — 연결된 것 */ +export function refText(ref: Record | 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()}`; +} /** 표 열(244-13) — 2차포장 열 = 기준점 있을 때만 · 기준점 없음 = 포장(2차 + 1차 + 포장) · 비포장 · 합계 · * 기준점 있음 = 거침 안 한 거래처 「포장」 값이 있으면 포장 열도 */ @@ -53,7 +70,7 @@ export function milestoneColumns(withBase: boolean, anyPaved: boolean): DataColu { key: "un", label: "비포장", group: ROAD, ...KM }, ]; return [ - { key: "item", label: "항목", width: 120 }, + { key: "item", label: "항목", width: 134 }, { key: "company", label: "거래처 이름", width: 120 }, { key: "dest", label: "목적지", width: 72 }, ...roads, @@ -84,6 +101,8 @@ export function addRoute(projectId: string, init: Partial = {}): function tableView(projectId: string): MilestoneView { const diagram = createDiagram(projectId, (init) => addRoute(projectId, init)); let keys: Array<{ route: string; index: number }> = []; + const routeAt = (i: number) => + liveMilestone(projectId).routes.find((r) => r.id === keys[i]?.route); const makeTable = (columns: DataColumn[]) => { const t = createDataTable({ columns, @@ -91,6 +110,7 @@ function tableView(projectId: string): MilestoneView { caption: "거리이정표", empty: "", editOnDouble: true, + cellClass: (i, key) => (key !== "item" ? null : routeAt(i)?.ref ? `${LAMP} is-linked` : LAMP), editable: (_i, key) => key === "item" || key === "company" || key === "note", onEdit: (i, key, text) => { const at = keys[i]; @@ -116,6 +136,26 @@ function tableView(projectId: string): MilestoneView { }); 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("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 ?? "" }; + }); + pickAtCell([ + ["단가연결", () => void openPriceLink({ apply: link })], + ...(route.ref ? [["단가연결 풀기", () => link(null)] as [string, () => void]] : []), + ]); + }); return t; }; let mode = "false:false"; @@ -177,7 +217,7 @@ export function renderMilestone(host: HTMLElement, projectId: string): void { }; tabs.replaceChildren( tab("table", "거리이정표"), - ...routes.map((r, i) => tab(r.id, r.name.trim() || `거래처 ${i + 1}`)), + ...routes.map((r, i) => tab(r.id, r.name.trim() || r.rows[0]?.item || `거래처 ${i + 1}`)), ); }; diff --git a/B08_Quantity/B08_Quantity_UI_Milestone_Diagram.ts b/B08_Quantity/B08_Quantity_UI_Milestone_Diagram.ts index dc44b57f7..e7595d8a1 100644 --- a/B08_Quantity/B08_Quantity_UI_Milestone_Diagram.ts +++ b/B08_Quantity/B08_Quantity_UI_Milestone_Diagram.ts @@ -421,12 +421,15 @@ export function createDiagram( }); }; refresh(); - // 틀 크기가 바뀌면(창 · 패널 접기) 그 비율로 다시 그림 + // 틀 크기가 바뀌면(창 · 패널 접기 · 높이 = 58vh) 그 비율로 다시 그림 — 그림이 틀보다 크면 아래가 잘림(251) let lastW = 0; + let lastH = 0; new ResizeObserver(() => { const w = host.clientWidth; - if (w && Math.abs(w - lastW) > 1) { + const h = host.clientHeight; + if (w && (Math.abs(w - lastW) > 1 || Math.abs(h - lastH) > 1)) { lastW = w; + lastH = h; refresh(); } }).observe(host); diff --git a/B08_Quantity/B08_Quantity_UI_Milestone_Route.ts b/B08_Quantity/B08_Quantity_UI_Milestone_Route.ts index 606fa3c38..d4b9c85b6 100644 --- a/B08_Quantity/B08_Quantity_UI_Milestone_Route.ts +++ b/B08_Quantity/B08_Quantity_UI_Milestone_Route.ts @@ -34,7 +34,8 @@ let lastCell: Element | null = null; /** 표 칸 누름 자리 기억 — 「▾」 메뉴를 그 칸 아래에 엶 */ export function trackCells(root: HTMLElement): void { - root.addEventListener("click", (e) => (lastCell = (e.target as Element).closest("td")), true); + for (const type of ["click", "contextmenu"]) + root.addEventListener(type, (e) => (lastCell = (e.target as Element).closest("td")), true); } export function pickAtCell(items: MapContextMenuItem[]): void { diff --git a/resources/tester/test_251_1_haul_groups.py b/resources/tester/test_251_1_haul_groups.py index 4d97f0073..3251d7a59 100644 --- a/resources/tester/test_251_1_haul_groups.py +++ b/resources/tester/test_251_1_haul_groups.py @@ -107,3 +107,38 @@ def test_251_5_자동_줄_단가_줄_키_짝(): ts = (ROOT / "B09_Estimation" / "B09_Estimation_Calc_Bill.ts").read_text(encoding="utf-8") assert 'ref_price(item.get("ref"), books, hopyo, item["key"])' in py assert "refPrice(item.ref ?? null, book, hopyo, item.key)" in ts + + +def test_251_4_거리이정표_램프_우클릭_단가연결(): + import json + import subprocess + + src = (ROOT / "B08_Quantity" / "B08_Quantity_UI_Milestone.ts").read_text(encoding="utf-8") + fn = ( + "export function refText" + + src.split("export function refText")[1].split("\n}\n")[0] + + "\n}\n" + ) + js = ( + fn + + """ +console.log(JSON.stringify([refText(null), + refText({ tab: "logic", code: "GF000134", name: "골재운반 모래" }), + refText({ tab: "dg", key: "DG:x", name: "회사 조합" })])); +""" + ) + run = subprocess.run( + ["node", "--experimental-strip-types", "--no-warnings", "--input-type=module-typescript"], + input=js, capture_output=True, text=True, encoding="utf-8", timeout=60, + ) # fmt: skip + assert run.returncode == 0, run.stderr + assert json.loads(run.stdout) == [ + "단가연결 없음 — 줄 우클릭 [단가연결]", + "단가연결 — 단가산출 GF000134 골재운반 모래", + "단가연결 — 일위대가 회사 조합", + ] + # 램프 = 항목 칸 · 우클릭 = [단가연결] · 연결 있으면 [단가연결 풀기] · 창 = B09 와 같은 것 + assert 'key !== "item" ? null : routeAt(i)?.ref ? `${LAMP} is-linked` : LAMP' in src + assert "openPriceLink({ apply: link })" in src and '"단가연결 풀기"' in src + diagram = (ROOT / "B08_Quantity" / "B08_Quantity_UI_Milestone_Diagram.ts").read_text("utf-8") + assert "Math.abs(h - lastH) > 1" in diagram # 틀 높이 바뀜 = 다시 그림(아래 잘림 없음)