Merge remote-tracking branch 'origin/sub_laptop_8' into main_laptop_1
This commit is contained in:
@@ -67,6 +67,8 @@ export interface SvgZoomPanOptions {
|
||||
content?: ContentBounds;
|
||||
/** 휠을 줌에 묶을지(기본 안 묶음). 묶으면 **커서 아래 지점**을 붙잡고 확대한다. */
|
||||
wheelZoom?: boolean;
|
||||
/** 팬을 시작할 누름인지 — 안 주면 가운데 버튼만. 고르기가 없는 빈 곳 왼쪽 끌기를 더할 때 쓴다. */
|
||||
panStart?: (event: PointerEvent) => boolean;
|
||||
}
|
||||
|
||||
/** 휠 한 칸에 얼마나 — 노선 편집 지도와 같은 계수(`_RouteEdit_Input`). */
|
||||
@@ -146,8 +148,8 @@ export function attachSvgZoomPan(options: SvgZoomPanOptions): ZoomPanHandle {
|
||||
if (event.button === 1) event.preventDefault();
|
||||
});
|
||||
svg.addEventListener("pointerdown", (event) => {
|
||||
// 팬은 **가운데 버튼**만. 좌클릭은 그 화면의 고르기 몫이다.
|
||||
if (event.button !== 1) return;
|
||||
// 팬은 **가운데 버튼**만(기본). 좌클릭은 그 화면의 고르기 몫이다.
|
||||
if (!(options.panStart ? options.panStart(event) : event.button === 1)) return;
|
||||
event.preventDefault();
|
||||
panning = true;
|
||||
moved = false;
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
* B08_Quantity_Milestone_Layout.ts
|
||||
* 거리이정표 다이어그램 자리 계산(PLAN 244-2) — 화면 없음 · 형 import 만(시험이 node 로 돌림).
|
||||
*
|
||||
* 상자 = 공사지(시점 · 중점 한 컨테이너 · 가운데) · 기준점 · 업체들
|
||||
* 업체 자리 = 기준점(거침 · 없으면 공사지) 기준 지도 방위 — 업체 경위도(모달 주소 검색)가 있으면
|
||||
* 상자 = 공사지(시점 · 중점 한 컨테이너 · 가운데) · 기준점 · 거래처들
|
||||
* 거래처 자리 = 기준점(거침 · 없으면 공사지) 기준 지도 방위 — 거래처 경위도(모달 주소 검색)가 있으면
|
||||
* 방위각 자동(244-4 · 기준점 좌표 없으면 시점 좌표로) · 없으면 옛 8방 · 그도 없으면 빈 방위 차례 ·
|
||||
* 같은 방위 여럿 = 옆으로 나란히
|
||||
* 기준점 자리 = 기준점 주소 좌표 → 공사지(시점) 기준 방위각 · 좌표 없으면 서(왼 · 견본) — 옛 8방 무시(244-5)
|
||||
* 선 = 업체 → 기준점(또는 시점) · 기준점 → 시점 · 시점 → 중점 — 글 = 「도로 구분 km」 줄마다
|
||||
* 선 = 거래처 → 기준점(또는 시점) · 기준점 → 시점 · 시점 → 중점 — 글 = 「도로 구분 km」 줄마다
|
||||
* 화면 = 북 위(SVG y 아래로 = 남)
|
||||
* 선 글 바로 고침(244-4 ⑥) = `legAt` · `setLeg` — 선(from) · 글 차례 → 그 구간(모달과 같은 값 한 벌)
|
||||
* ========================================================================== */
|
||||
@@ -22,6 +22,8 @@ export interface DiagramBox {
|
||||
w: number;
|
||||
h: number;
|
||||
label: string;
|
||||
/** 거래처 상자 윗줄 — 표 줄 항목(244-6 · 예 「장비」) */
|
||||
items?: string;
|
||||
}
|
||||
|
||||
export interface DiagramLine {
|
||||
@@ -160,10 +162,10 @@ export function layoutMilestone(
|
||||
line("base", "start", legText(data.base.legs));
|
||||
}
|
||||
|
||||
// 업체 — 기준점(거침) · 공사지 기준 방위 · 같은 방위끼리 옆으로 나란히
|
||||
// 거래처 — 기준점(거침) · 공사지 기준 방위 · 같은 방위끼리 옆으로 나란히
|
||||
const groups = new Map<
|
||||
string,
|
||||
Array<{ id: string; label: string; anchor: string; deg: number; legs: Leg[] }>
|
||||
Array<{ id: string; label: string; items: string; anchor: string; deg: number; legs: Leg[] }>
|
||||
>();
|
||||
const used = new Set<string>();
|
||||
const keyOf = (anchor: string, deg: number): string => `${anchor}:${Math.round(deg) % 360}`;
|
||||
@@ -173,7 +175,7 @@ export function layoutMilestone(
|
||||
});
|
||||
for (const it of items) if (it.deg !== null) used.add(keyOf(it.anchor, it.deg));
|
||||
let auto = 0;
|
||||
/** 방위 안 고른 업체 — 빈 8방 차례(기준점 쪽 = 공사지 반대편부터 · 기준점 · 공사지로 향하는 쪽 뺌) */
|
||||
/** 방위 안 고른 거래처 — 빈 8방 차례(기준점 쪽 = 공사지 반대편부터 · 기준점 · 공사지로 향하는 쪽 뺌) */
|
||||
const free = (anchor: string): number => {
|
||||
const turn = anchor === "base" ? baseDeg : 0;
|
||||
const blocked = anchor === "base" ? baseDeg + 180 : useBase ? baseDeg : -1;
|
||||
@@ -187,7 +189,10 @@ export function layoutMilestone(
|
||||
used.add(keyOf(anchor, d));
|
||||
const key = keyOf(anchor, d);
|
||||
if (!groups.has(key)) groups.set(key, []);
|
||||
groups.get(key)!.push({ id: r.id, label: r.name || "업체", anchor, deg: d, legs: r.legs });
|
||||
const items = [...new Set(r.rows.map((row) => row.item.trim()).filter(Boolean))].join(" · ");
|
||||
groups
|
||||
.get(key)!
|
||||
.push({ id: r.id, label: r.name || "거래처", items, anchor, deg: d, legs: r.legs });
|
||||
}
|
||||
for (const group of groups.values()) {
|
||||
group.forEach((g, k) => {
|
||||
@@ -205,6 +210,7 @@ export function layoutMilestone(
|
||||
w: BOX.w,
|
||||
h: BOX.h,
|
||||
label: g.label,
|
||||
items: g.items,
|
||||
});
|
||||
line(g.id, g.anchor === "base" ? "base" : "start", legText(g.legs));
|
||||
});
|
||||
@@ -229,7 +235,7 @@ export function kakaoRouteUrl(points: Array<{ name: string; lon: number; lat: nu
|
||||
return `https://map.kakao.com/link/by/car/${points.map(part).join("/")}`;
|
||||
}
|
||||
|
||||
/** 선 글 한 줄 → 그 구간 — from = 업체 id(업체 구간) · "base"(공통 구간) · "start"(시점 → 중점) */
|
||||
/** 선 글 한 줄 → 그 구간 — from = 거래처 id(거래처 구간) · "base"(공통 구간) · "start"(시점 → 중점) */
|
||||
export function legAt(data: MilestoneData, from: string, i: number, midKm: number): Leg | null {
|
||||
if (from === "start") return { road: data.site.mid_road, km: Math.round(midKm * 1000) / 1000 };
|
||||
const legs = from === "base" ? data.base.legs : data.routes.find((r) => r.id === from)?.legs;
|
||||
@@ -254,3 +260,14 @@ export function setLeg(
|
||||
if (leg) Object.assign(leg, patch);
|
||||
return data;
|
||||
}
|
||||
|
||||
/** 다이어그램 팬 시작 — 가운데 버튼 · 빈 곳 왼쪽 끌기(상자 · 선 글 · [+] 위는 그 화면 고르기 몫 · 244-6) */
|
||||
export const PAN_SKIP = ".b08-milestone__box, .b08-milestone__km, .b08-milestone__plus";
|
||||
export function panStart(e: { button: number; target: EventTarget | null }): boolean {
|
||||
if (e.button === 1) return true;
|
||||
const target = e.target as { closest?: (selector: string) => unknown } | null;
|
||||
return e.button === 0 && !target?.closest?.(PAN_SKIP);
|
||||
}
|
||||
|
||||
/** 팬 한계 — 원배율에서도 그림 둘레 한 폭씩 끌어 옮길 수 있게(공용 팬은 이 범위까지 엶) */
|
||||
export const panContent = (w: number, h: number) => ({ x: -w, y: -h, width: 3 * w, height: 3 * h });
|
||||
|
||||
@@ -4,6 +4,13 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
/* 틀 너비 = 표 너비(244-6) — 다이어그램은 너비를 보태지 않고 표에 맞춰 늘음 */
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.b08-milestone > .b08-milestone__diagram {
|
||||
contain: inline-size;
|
||||
}
|
||||
|
||||
.b08-milestone__route {
|
||||
@@ -147,3 +154,9 @@
|
||||
.b08-milestone__leg-edit .ui-input {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
/* 거래처 상자 윗줄 = 항목(244-6) */
|
||||
.b08-milestone__items {
|
||||
fill: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/* =============================================================================
|
||||
* B08_Quantity_UI_Milestone.ts
|
||||
* B08 메인 탭 「거리이정표」(PLAN 244-1 · 244-4) — 하위 탭 줄 [거리이정표] + 경로 탭(업체마다 · 이름 = 업체 이름).
|
||||
* B08 메인 탭 「거리이정표」(PLAN 244-1 · 244-4) — 하위 탭 줄 [거리이정표] + 경로 탭(거래처마다 · 이름 = 거래처 이름).
|
||||
* 경로 탭은 다이어그램 [+] 등록 · 항목 정보 모달 [삭제]로만 생기고 없어짐(하위 탭 줄 [추가] · [삭제] 없음).
|
||||
*
|
||||
* [거리이정표] = 표(항목 · 업체 이름 · 목적지 ▾ · 2차포장 · 1차포장 · 비포장 · 포장 · 비포장 · 합계 · 비고)
|
||||
* [거리이정표] = 표(항목 · 거래처 이름 · 목적지 ▾ · 2차포장 · 1차포장 · 비포장 · 포장 · 비포장 · 합계 · 비고)
|
||||
* 표 합 = `…_Milestone_Calc` TS 짝(조작 즉시) · [저장] 때 서버가 파이썬 짝으로 다시 셈
|
||||
* 입력 = 더블클릭(243) · 업체 이름을 고치면 그 경로 이름(하위 탭)도
|
||||
* 입력 = 더블클릭(243) · 거래처 이름을 고치면 그 경로 이름(하위 탭)도
|
||||
* 위쪽 다이어그램 = `…_UI_Milestone_Diagram`(상자 누름 = 모달 · [+] = 항목 추가 모달 · 팬 · 줌)
|
||||
* 경로 탭 = `…_UI_Milestone_Route`(길찾기 지도 그림만) · 데이터 · [저장] = `…_UI_Milestone_State`
|
||||
* ========================================================================== */
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
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: "company", label: "거래처 이름", width: 120 },
|
||||
{ key: "dest", label: "목적지", width: 72 },
|
||||
{ key: "s2", label: "2차포장", group: ["도로 구분 (km)"], ...KM },
|
||||
{ key: "s1", label: "1차포장", group: ["도로 구분 (km)"], ...KM },
|
||||
@@ -45,13 +45,13 @@ const COLUMNS: DataColumn[] = [
|
||||
{ key: "note", label: "비고", width: 160 },
|
||||
];
|
||||
|
||||
/** 새 경로(업체) — 캐시에 넣고 id 를 돌려줌 */
|
||||
/** 새 경로(거래처) — 캐시에 넣고 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}`,
|
||||
name: `거래처 ${data.routes.length + 1}`,
|
||||
address: "",
|
||||
via_base: true,
|
||||
dir: null,
|
||||
@@ -141,7 +141,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() || `거래처 ${i + 1}`)),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,17 +2,25 @@
|
||||
* B08_Quantity_UI_Milestone_Diagram.ts
|
||||
* [거리이정표] 하위 탭 위쪽 다이어그램(PLAN 244-2) — SVG · 자리 = `…_Milestone_Layout`.
|
||||
*
|
||||
* 상자 = 공사지(시점 · 중점 한 컨테이너) · 기준점 · 업체들 · 선마다 「도로 구분 km」 글
|
||||
* 상자 누름 = 모달(244-4 · 탭 이동 아님 · `…_UI_Milestone_Modal`) — 업체 = 항목 정보 · 공사지 = 시점 주소 ·
|
||||
* 상자 = 공사지(시점 · 중점 한 컨테이너) · 기준점 · 거래처들 · 선마다 「도로 구분 km」 글
|
||||
* 상자 누름 = 모달(244-4 · 탭 이동 아님 · `…_UI_Milestone_Modal`) — 거래처 = 항목 정보 · 공사지 = 시점 주소 ·
|
||||
* 기준점 = 공통 구간 · 공사지 · 기준점 상자 [+] = 항목 추가 모달 · 조작 = 캐시(`…_UI_Milestone_State`)
|
||||
* 팬 · 줌 = 공용 `A00_Common/b_svg_zoom_pan`(휠 = 줌 · 가운데 버튼 끌기 = 팬 · 빈 곳 더블클릭 = 원배율)
|
||||
* 팬 · 줌 = 공용 `A00_Common/b_svg_zoom_pan`(휠 = 줌 · 가운데 버튼 · 빈 곳 왼쪽 끌기 = 팬 · 빈 곳 더블클릭 = 원배율)
|
||||
* 거래처 상자 = 항목(윗줄) + 거래처 이름 · 기준점 없으면 위에 [기준점 +](244-6)
|
||||
* 선 글 더블클릭 = 그 구간 도로 구분 ▾ · km 바로 고침(⑥ · 글 화면 자리에 띄움 · Enter = 반영 · Esc = 취소)
|
||||
* ========================================================================== */
|
||||
|
||||
import { createSelectField, el } from "@ui/ui_template_elements";
|
||||
import { createButton, createSelectField, el } from "@ui/ui_template_elements";
|
||||
import { attachSvgZoomPan, type ZoomPanState } from "../A00_Common/b_svg_zoom_pan";
|
||||
import { midKm, type MilestoneRoute } from "./B08_Quantity_Milestone_Calc";
|
||||
import { layoutMilestone, legAt, setLeg, type DiagramBox } from "./B08_Quantity_Milestone_Layout";
|
||||
import {
|
||||
layoutMilestone,
|
||||
legAt,
|
||||
panContent,
|
||||
panStart,
|
||||
setLeg,
|
||||
type DiagramBox,
|
||||
} from "./B08_Quantity_Milestone_Layout";
|
||||
import {
|
||||
openAddItem,
|
||||
openBaseInfo,
|
||||
@@ -138,7 +146,8 @@ export function createDiagram(
|
||||
role: "img",
|
||||
"aria-label": "거리이정표 다이어그램",
|
||||
});
|
||||
root.style.maxWidth = "100%";
|
||||
// 틀 너비 = 아래 표 너비(244-6) · 높이는 비율대로(팬 · 줌 화면 px ↔ 그림 좌표가 어긋나지 않게)
|
||||
root.style.width = "100%";
|
||||
root.style.height = "auto";
|
||||
const layer = svg("g", {});
|
||||
const shift = svg("g", { transform: `translate(${-vx} ${-vy})` });
|
||||
@@ -162,9 +171,19 @@ export function createDiagram(
|
||||
"text-anchor": "middle",
|
||||
"dominant-baseline": "central",
|
||||
},
|
||||
box.label,
|
||||
box.kind === "route" && box.items ? "" : box.label,
|
||||
),
|
||||
);
|
||||
if (box.kind === "route" && box.items) {
|
||||
// 거래처 상자 = 항목(윗줄) + 거래처 이름(아랫줄)
|
||||
const x = box.x + box.w / 2;
|
||||
const two = g.lastChild as SVGTextElement;
|
||||
two.setAttribute("y", String(box.y + box.h / 2 - 8));
|
||||
two.append(
|
||||
svg("tspan", { x, class: "b08-milestone__items" }, box.items),
|
||||
svg("tspan", { x, dy: 16 }, box.label),
|
||||
);
|
||||
}
|
||||
// 상자 누름 = 모달(탭 이동 아님)
|
||||
const info = (): void => INFO[box.kind](box.id);
|
||||
g.setAttribute("role", "button");
|
||||
@@ -194,7 +213,15 @@ export function createDiagram(
|
||||
});
|
||||
shift.append(text);
|
||||
}
|
||||
host.replaceChildren(root);
|
||||
const addBase = createButton({
|
||||
label: "기준점 +",
|
||||
size: "sm",
|
||||
variant: "ghost",
|
||||
onClick: () => openBaseInfo(projectId, true),
|
||||
});
|
||||
addBase.dataset.b08MilestoneAddBase = "";
|
||||
addBase.hidden = data.base.enabled;
|
||||
host.replaceChildren(el("div", { className: "b08-milestone__bar", children: [addBase] }), root);
|
||||
attachSvgZoomPan({
|
||||
svg: root,
|
||||
layer,
|
||||
@@ -204,6 +231,8 @@ export function createDiagram(
|
||||
initial: zoomState,
|
||||
onChange: (state) => (zoomState = state),
|
||||
wheelZoom: true,
|
||||
content: panContent(vw, vh), // 원배율에서도 팬(244-6 · 범위 없으면 1배는 안 움직임)
|
||||
panStart, // 가운데 버튼 · 빈 곳 왼쪽 끌기
|
||||
});
|
||||
};
|
||||
refresh();
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* B08_Quantity_UI_Milestone_Modal.ts
|
||||
* 거리이정표 다이어그램 모달(PLAN 244-4 · 사용자 130번) — 페이지 이동 없이 창에서 보고 고침.
|
||||
*
|
||||
* [+] 항목 추가 = 항목 · 업체 이름 · 목적지 + 아래 주소 검색(후보 고름 → 지도 표지) → [등록] =
|
||||
* 경로(하위 탭) 생김 · 같은 업체 이름이면 그 경로에 표 줄만 · 방위 = 고른 주소 좌표로 자동 자리
|
||||
* 업체 상자 = 그 항목 정보(업체 · 표 줄 · 주소 · 지도 · 구간 km · 도로 구분) · [고침] · [삭제]
|
||||
* [+] 항목 추가 = 항목 · 거래처 이름 · 목적지 + 아래 주소 검색(후보 고름 → 지도 표지) → [등록] =
|
||||
* 경로(하위 탭) 생김 · 같은 거래처 이름이면 그 경로에 표 줄만 · 방위 = 고른 주소 좌표로 자동 자리
|
||||
* 거래처 상자 = 그 항목 정보(거래처 · 표 줄 · 주소 · 지도 · 구간 km · 도로 구분) · [고침] · [삭제]
|
||||
* 시점(공사지) 상자 = 프로젝트 등록 주소 · VWorld 주소(좌표 → 주소 · 서버) · 아래 지도
|
||||
* 기준점 상자 = 기준점 이름 · 주소 · 지도 · 공통 구간(기준점 → 시점)
|
||||
* 주소 검색 · 지도 = B01 회사 주소 칸 재활용(VWorld 대리 · 새 키 없음) · 조작 = 캐시 · [저장] 때 서버
|
||||
@@ -36,7 +36,12 @@ import {
|
||||
type Road,
|
||||
} from "./B08_Quantity_Milestone_Calc";
|
||||
import { request } from "./B08_Quantity_UI_Hydro";
|
||||
import { liveMilestone, putMilestone, roadOptions } from "./B08_Quantity_UI_Milestone_State";
|
||||
import {
|
||||
liveMilestone,
|
||||
putMilestone,
|
||||
roadOptions,
|
||||
siteInfo,
|
||||
} from "./B08_Quantity_UI_Milestone_State";
|
||||
|
||||
const DEST_OPTIONS = [
|
||||
{ value: "start", text: "시점" },
|
||||
@@ -145,7 +150,7 @@ function placeOf(
|
||||
|
||||
const head = (text: string): HTMLElement => el("h4", { text });
|
||||
|
||||
/** [+] — 항목 · 업체 이름 · 목적지 + 주소 검색 · 지도 → [등록] */
|
||||
/** [+] — 항목 · 거래처 이름 · 목적지 + 주소 검색 · 지도 → [등록] */
|
||||
export function openAddItem(
|
||||
projectId: string,
|
||||
viaBase: boolean,
|
||||
@@ -156,7 +161,7 @@ export function openAddItem(
|
||||
closeLabel: "닫기",
|
||||
mount: (body, close, actions) => {
|
||||
const item = createInputField({ label: "항목", compact: true, placeholder: "예 장비(20톤)" });
|
||||
const company = createInputField({ label: "업체 이름", compact: true });
|
||||
const company = createInputField({ label: "거래처 이름", compact: true });
|
||||
const dest = createSelectField({ label: "목적지", compact: true, options: DEST_OPTIONS });
|
||||
const address = buildAddressField(null);
|
||||
item.input.dataset.b08MilestoneAdd = "item";
|
||||
@@ -166,7 +171,7 @@ export function openAddItem(
|
||||
variant: "filled",
|
||||
onClick: () => {
|
||||
const name = company.input.value.trim();
|
||||
if (!name) return company.setError("업체 이름을 넣으세요.");
|
||||
if (!name) return company.setError("거래처 이름을 넣으세요.");
|
||||
const row = { item: item.input.value.trim(), dest: dest.select.value as Dest, note: "" };
|
||||
const place = placeOf(address, { address: "" });
|
||||
const data = liveMilestone(projectId);
|
||||
@@ -184,23 +189,23 @@ export function openAddItem(
|
||||
item.root,
|
||||
company.root,
|
||||
dest.root,
|
||||
head("업체 위치 — 주소 검색(방위는 이 자리로 자동)"),
|
||||
head("거래처 위치 — 주소 검색(방위는 이 자리로 자동)"),
|
||||
address.root,
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 업체 상자 — 그 항목 정보 · 고침 · 삭제(탭 이동 아님) */
|
||||
/** 거래처 상자 — 그 항목 정보 · 고침 · 삭제(탭 이동 아님) */
|
||||
export function openRouteInfo(projectId: string, routeId: string): void {
|
||||
const data = liveMilestone(projectId);
|
||||
const route = data.routes.find((r) => r.id === routeId);
|
||||
if (!route) return;
|
||||
openModal({
|
||||
title: `항목 정보 — ${route.name || "업체"}`,
|
||||
title: `항목 정보 — ${route.name || "거래처"}`,
|
||||
closeLabel: "닫기",
|
||||
mount: (body, close, actions) => {
|
||||
const name = createInputField({ label: "업체 이름", compact: true });
|
||||
const name = createInputField({ label: "거래처 이름", compact: true });
|
||||
name.input.value = route.name;
|
||||
name.input.dataset.b08MilestoneInfo = "name";
|
||||
const via = createCheckbox("기준점 거침");
|
||||
@@ -232,7 +237,7 @@ export function openRouteInfo(projectId: string, routeId: string): void {
|
||||
onClick: async () => {
|
||||
if (
|
||||
!(await showConfirmDialog(
|
||||
`「${route.name || "업체"}」 항목을 지울까요? — [저장] 때 반영`,
|
||||
`「${route.name || "거래처"}」 항목을 지울까요? — [저장] 때 반영`,
|
||||
"삭제",
|
||||
))
|
||||
)
|
||||
@@ -252,7 +257,7 @@ export function openRouteInfo(projectId: string, routeId: string): void {
|
||||
head("표 줄 — 항목 · 목적지"),
|
||||
rows.root,
|
||||
head(
|
||||
`구간 — ${route.name || "업체"} → ${data.base.enabled && route.via_base ? data.base.name || "기준점" : "시점"} (km · 도로 구분)`,
|
||||
`구간 — ${route.name || "거래처"} → ${data.base.enabled && route.via_base ? data.base.name || "기준점" : "시점"} (km · 도로 구분)`,
|
||||
),
|
||||
legs.root,
|
||||
head("주소 · 지도"),
|
||||
@@ -263,22 +268,26 @@ export function openRouteInfo(projectId: string, routeId: string): void {
|
||||
}
|
||||
|
||||
/** 기준점 상자 — 이름 · 주소 · 지도 · 공통 구간(기준점 → 시점) */
|
||||
export function openBaseInfo(projectId: string): void {
|
||||
export function openBaseInfo(projectId: string, create = false): void {
|
||||
const base = liveMilestone(projectId).base;
|
||||
openModal({
|
||||
title: `기준점 — ${base.name || "기준점"}`,
|
||||
title: create ? "기준점 추가" : `기준점 — ${base.name || "기준점"}`,
|
||||
closeLabel: "닫기",
|
||||
mount: (body, close, actions) => {
|
||||
const name = createInputField({ label: "기준점 이름", compact: true });
|
||||
name.input.value = base.name;
|
||||
const address = buildAddressField(base.address || null);
|
||||
const legs = legsEditor(base.legs, "1차포장");
|
||||
const legs = legsEditor(
|
||||
base.legs.length ? base.legs : [{ road: "1차포장", km: null }],
|
||||
"1차포장",
|
||||
);
|
||||
const save = createButton({
|
||||
label: "고침",
|
||||
label: create ? "등록" : "고침",
|
||||
variant: "filled",
|
||||
onClick: () => {
|
||||
const fresh: MilestoneData = liveMilestone(projectId);
|
||||
Object.assign(fresh.base, placeOf(address, fresh.base), {
|
||||
enabled: true,
|
||||
name: name.input.value.trim(),
|
||||
legs: legs.value(),
|
||||
});
|
||||
@@ -286,7 +295,21 @@ export function openBaseInfo(projectId: string): void {
|
||||
close();
|
||||
},
|
||||
});
|
||||
actions.append(save);
|
||||
// 기준점 없앰 = 꺼 둠(값은 남김 · 거래처는 시점으로 바로)
|
||||
const off = createButton({
|
||||
label: "기준점 없앰",
|
||||
variant: "danger",
|
||||
onClick: () => {
|
||||
const fresh: MilestoneData = liveMilestone(projectId);
|
||||
fresh.base.enabled = false;
|
||||
putMilestone(projectId, fresh);
|
||||
close();
|
||||
},
|
||||
});
|
||||
off.hidden = create;
|
||||
save.dataset.b08MilestoneBase = "save";
|
||||
off.dataset.b08MilestoneBase = "off";
|
||||
actions.append(off, save);
|
||||
body.append(
|
||||
name.root,
|
||||
head("공통 구간 — 기준점 → 시점 (km · 도로 구분)"),
|
||||
@@ -313,14 +336,56 @@ interface SiteAddress {
|
||||
/** 시점(공사지) 상자 — 프로젝트 등록 주소 · VWorld 주소 · 아래 지도(시점 표지) */
|
||||
export function openSiteInfo(projectId: string): void {
|
||||
openModal({
|
||||
title: "공사지 — 시점",
|
||||
title: "공사지 — 시점 · 중점",
|
||||
closeLabel: "닫기",
|
||||
mount: (body) => {
|
||||
mount: (body, close, actions) => {
|
||||
const text = (label: string, value: string): HTMLElement =>
|
||||
el("p", { className: "b08-options__hint", text: `${label} — ${value}` });
|
||||
// 중점 측점 · 시점 → 중점 도로(좌측 패널 카드에서 옮김 · 244-6)
|
||||
const data = liveMilestone(projectId);
|
||||
const site = siteInfo();
|
||||
const km = (m: number | null | undefined): string =>
|
||||
m == null ? "-" : (m / 1000).toLocaleString(undefined, { maximumFractionDigits: 3 });
|
||||
const mid = createInputField({
|
||||
label: "중점 측점 (m)",
|
||||
type: "number",
|
||||
min: 0,
|
||||
compact: true,
|
||||
});
|
||||
mid.input.placeholder = site?.mid_default_m != null ? String(site.mid_default_m) : "";
|
||||
mid.input.value = data.site.mid_station_m == null ? "" : String(data.site.mid_station_m);
|
||||
mid.input.dataset.b08Milestone = "mid_station_m";
|
||||
const road = createSelectField({
|
||||
label: "시점 → 중점 도로",
|
||||
options: roadOptions,
|
||||
compact: true,
|
||||
});
|
||||
road.select.value = data.site.mid_road;
|
||||
const save = createButton({
|
||||
label: "고침",
|
||||
variant: "filled",
|
||||
onClick: () => {
|
||||
const fresh = liveMilestone(projectId);
|
||||
const value = Number(mid.input.value);
|
||||
fresh.site.mid_station_m = mid.input.value.trim() && value >= 0 ? value : null;
|
||||
fresh.site.mid_road = road.select.value as Road;
|
||||
putMilestone(projectId, fresh);
|
||||
close();
|
||||
},
|
||||
});
|
||||
save.dataset.b08MilestoneSite = "save";
|
||||
actions.append(save);
|
||||
const map = buildAddressMap();
|
||||
const info = el("div", { children: [text("주소", "불러오는 중…")] });
|
||||
body.append(info, map.root);
|
||||
body.append(
|
||||
line(mid.root, road.root),
|
||||
text(
|
||||
"시점 = 노선 첫 점(측점 0)",
|
||||
`노선 ${km(site?.length_m)} km · 중점 기본 = 노선 ÷ 2(${km(site?.mid_default_m)} km) · 빈칸 = 기본`,
|
||||
),
|
||||
info,
|
||||
map.root,
|
||||
);
|
||||
request<SiteAddress>(`/projects/${projectId}/quantity/milestone/site-address`).then(
|
||||
(s) => {
|
||||
const ll = (p: [number, number] | null): string =>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* B08_Quantity_UI_Milestone_Route.ts
|
||||
* 거리이정표 경로 하위 탭(PLAN 244-1 · 244-4) — 길찾기 지도 결과 그림 하나만(구간 · 표 줄은 항목 정보 모달).
|
||||
*
|
||||
* 길찾기 API 키 없음 → [다음 지도에서 길찾기] = 새 창 map.kakao.com 자동차 길찾기(출발 = 업체 좌표 ·
|
||||
* 길찾기 API 키 없음 → [다음 지도에서 길찾기] = 새 창 map.kakao.com 자동차 길찾기(출발 = 거래처 좌표 ·
|
||||
* 경유 = 기준점 좌표(거침) · 도착 = 목적지 좌표) → 사용자가 길 확인 → [화면 캡처] = getDisplayMedia 로
|
||||
* 고른 창 한 장 → 이 탭 그림(그림 올리기 길) · [그림 올리기] · [그림 지우기] 곁에
|
||||
* 그림 = 올리는 즉시 서버(교체 · 지우기)
|
||||
@@ -65,7 +65,7 @@ export interface MilestoneView {
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
/** 새 창 길찾기 지점 — 업체 → (기준점 거침 · 좌표 있으면) → 목적지(첫 표 줄 · 시점 · 중점) */
|
||||
/** 새 창 길찾기 지점 — 거래처 → (기준점 거침 · 좌표 있으면) → 목적지(첫 표 줄 · 시점 · 중점) */
|
||||
export function routePoints(
|
||||
d: MilestoneData,
|
||||
r: MilestoneRoute,
|
||||
@@ -74,7 +74,7 @@ export function routePoints(
|
||||
const dest = r.rows[0]?.dest ?? "start";
|
||||
const to = dest === "mid" ? site?.mid_lonlat : site?.start_lonlat;
|
||||
if (r.lon == null || r.lat == null || !to) return null;
|
||||
const points = [{ name: r.name || "업체", lon: r.lon, lat: r.lat }];
|
||||
const points = [{ name: r.name || "거래처", lon: r.lon, lat: r.lat }];
|
||||
if (d.base.enabled && r.via_base && d.base.lon != null && d.base.lat != null)
|
||||
points.push({ name: d.base.name || "기준점", lon: d.base.lon, lat: d.base.lat });
|
||||
points.push({ name: `임도 ${DEST_TEXT[dest]}`, lon: to[0], lat: to[1] });
|
||||
@@ -151,7 +151,7 @@ export function routeView(projectId: string, routeId: string): MilestoneView {
|
||||
const points = r ? routePoints(d, r) : null;
|
||||
if (!points)
|
||||
return showToast(
|
||||
"업체 주소(다이어그램 상자 → 항목 정보)와 노선 시점을 먼저 정하세요.",
|
||||
"거래처 주소(다이어그램 상자 → 항목 정보)와 노선 시점을 먼저 정하세요.",
|
||||
"info",
|
||||
);
|
||||
window.open(kakaoRouteUrl(points), "_blank", "noopener");
|
||||
@@ -205,7 +205,7 @@ export function routeView(projectId: string, routeId: string): MilestoneView {
|
||||
const r = d.routes.find((x) => x.id === routeId);
|
||||
if (!r) return;
|
||||
const dest = DEST_TEXT[r.rows[0]?.dest ?? "start"];
|
||||
title.textContent = `${r.name || "업체"} → ${dest}${r.address ? ` · ${r.address}` : ""}`;
|
||||
title.textContent = `${r.name || "거래처"} → ${dest}${r.address ? ` · ${r.address}` : ""}`;
|
||||
loadImage();
|
||||
};
|
||||
refresh();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* =============================================================================
|
||||
* B08_Quantity_UI_Milestone_State.ts
|
||||
* 거리이정표(PLAN 244-1) 데이터 층 — 저장본 · 캐시 · [저장] · 경로 그림 길 + 좌측 패널 카드(공사지 · 기준점).
|
||||
* 거리이정표(PLAN 244-1) 데이터 층 — 저장본 · 캐시 · [저장] · 경로 그림 길(좌측 패널 카드는 244-6 에서 없앰 ·
|
||||
* 공사지 · 기준점은 다이어그램 모달).
|
||||
*
|
||||
* 저장본 = GET …/quantity/milestone(서버 정본 · 표는 서버가 셈) · 조작 = 캐시 `b08-milestone` 즉시 +
|
||||
* 표는 TS 짝(`…_Milestone_Calc`)이 바로 셈 · [저장](좌측 패널 한 줄) = PUT → 캐시 비움 · 자동 저장 없음.
|
||||
@@ -8,11 +9,9 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import { createCheckbox } from "@ui/ui_template_checkbox";
|
||||
import { createInputField, createSelectField, el } from "@ui/ui_template_elements";
|
||||
import { clearState, readState, writeState } from "../A00_Common/b_page_state";
|
||||
import { request } from "./B08_Quantity_UI_Hydro";
|
||||
import { ROADS, type MilestoneData, type Road } from "./B08_Quantity_Milestone_Calc";
|
||||
import { ROADS, type MilestoneData } from "./B08_Quantity_Milestone_Calc";
|
||||
|
||||
export interface SiteInfo {
|
||||
length_m: number | null;
|
||||
@@ -127,73 +126,3 @@ export async function imageBlobUrl(projectId: string, routeId: string): Promise<
|
||||
}
|
||||
|
||||
export const roadOptions = ROADS.map((r) => ({ value: r, text: r }));
|
||||
|
||||
const km = (m: number | null | undefined): string =>
|
||||
m == null ? "-" : (m / 1000).toLocaleString(undefined, { maximumFractionDigits: 3 });
|
||||
|
||||
/** 좌측 패널 「거리이정표」 카드 몸 — 공사지(시점 · 중점) · 기준점(있음 · 이름 · 주소 · 방위 = 주소 좌표 자동) */
|
||||
export function milestoneCardBody(projectId: string | null): HTMLElement[] {
|
||||
const mid = createInputField({ label: "중점 측점 (m)", type: "number", compact: true, min: 0 });
|
||||
const midRoad = createSelectField({
|
||||
label: "시점 → 중점 도로",
|
||||
options: roadOptions,
|
||||
compact: true,
|
||||
});
|
||||
const siteHint = el("p", { className: "b08-options__hint" });
|
||||
const has = createCheckbox("기준점 있음");
|
||||
const name = createInputField({ label: "기준점 이름", compact: true });
|
||||
const address = createInputField({ label: "기준점 주소", compact: true });
|
||||
mid.input.dataset.b08Milestone = "mid_station_m";
|
||||
has.input.dataset.b08Milestone = "base_enabled";
|
||||
const baseFields = el("div", { children: [name.root, address.root] });
|
||||
|
||||
const show = (): void => {
|
||||
if (!projectId) return;
|
||||
const data = liveMilestone(projectId);
|
||||
const site = siteInfo();
|
||||
mid.input.placeholder = site?.mid_default_m != null ? String(site.mid_default_m) : "";
|
||||
if (document.activeElement !== mid.input)
|
||||
mid.input.value = data.site.mid_station_m == null ? "" : String(data.site.mid_station_m);
|
||||
midRoad.select.value = data.site.mid_road;
|
||||
siteHint.textContent =
|
||||
`시점 = 노선 첫 점(측점 0) · 노선 ${km(site?.length_m)} km · ` +
|
||||
`중점 기본 = 노선 ÷ 2(${km(site?.mid_default_m)} km) · 빈칸 = 기본`;
|
||||
has.input.checked = data.base.enabled;
|
||||
baseFields.hidden = !data.base.enabled;
|
||||
if (document.activeElement !== name.input) name.input.value = data.base.name;
|
||||
if (document.activeElement !== address.input) address.input.value = data.base.address;
|
||||
};
|
||||
const edit = (fn: (data: MilestoneData) => void): void => {
|
||||
if (!projectId) return;
|
||||
const data = liveMilestone(projectId);
|
||||
fn(data);
|
||||
putMilestone(projectId, data);
|
||||
};
|
||||
mid.input.addEventListener("input", () =>
|
||||
edit((d) => {
|
||||
const value = Number(mid.input.value);
|
||||
d.site.mid_station_m = mid.input.value.trim() && value >= 0 ? value : null;
|
||||
}),
|
||||
);
|
||||
midRoad.select.addEventListener("change", () =>
|
||||
edit((d) => (d.site.mid_road = midRoad.select.value as Road)),
|
||||
);
|
||||
has.input.addEventListener("change", () =>
|
||||
edit((d) => {
|
||||
d.base.enabled = has.input.checked;
|
||||
if (d.base.enabled && !d.base.legs.length) d.base.legs = [{ road: "1차포장", km: null }];
|
||||
}),
|
||||
);
|
||||
name.input.addEventListener("input", () => edit((d) => (d.base.name = name.input.value)));
|
||||
address.input.addEventListener("input", () =>
|
||||
edit((d) => (d.base.address = address.input.value)),
|
||||
);
|
||||
|
||||
const host = el("div", {
|
||||
children: [mid.root, midRoad.root, siteHint, has.root, baseFields],
|
||||
});
|
||||
show();
|
||||
onMilestoneChange(host, show); // 첫 알림(읽기 끝)은 패널이 붙은 뒤
|
||||
if (projectId) void loadMilestone(projectId).catch(() => undefined);
|
||||
return [host];
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
* 자재 › 할증 모래 · 자갈 · 시멘트 %(PLAN 124) — 총괄자재수량집계표 그 세 줄 · 0~100
|
||||
* 토공 › 연암 % · 경암 %(암 전체 기준 · 한쪽 고치면 다른 쪽 = 100 − 값 · 0~100)
|
||||
* 수리집수면적표 — 빈 자리(다음 옵션)
|
||||
* 거리이정표 › 공사지(중점 측점 · 시점 → 중점 도로) · 기준점 — `…_UI_Milestone_State`(캐시 `b08-milestone` ·
|
||||
* [저장] 한 번에 같이 PUT)
|
||||
* 거리이정표 — 카드 없음(244-6 · 공사지 · 기준점은 다이어그램 모달) · 캐시 `b08-milestone` 은
|
||||
* [저장] 한 번에 같이 PUT(`…_UI_Milestone_State`)
|
||||
* 조작 = 캐시 `b08-options` 즉시 + 열린 표가 같은 식으로 바로 다시 셈(`…_Options_Calc`) ·
|
||||
* [저장] = PUT …/quantity/options → 캐시 비움 · 열린 탭을 서버 정본으로 다시 그림 · 자동 저장 없음.
|
||||
* ========================================================================== */
|
||||
@@ -27,7 +27,7 @@ import { clearState, readState, writeState } from "../A00_Common/b_page_state";
|
||||
import { request } from "./B08_Quantity_UI_Hydro";
|
||||
import { clampPct } from "./B08_Quantity_Options_Calc";
|
||||
import { PANEL_SURCHARGE, type Cells } from "./B08_Quantity_Material_Calc";
|
||||
import { milestoneCardBody, saveMilestone } from "./B08_Quantity_UI_Milestone_State";
|
||||
import { saveMilestone } from "./B08_Quantity_UI_Milestone_State";
|
||||
|
||||
function L(key: keyof typeof ui_locales): string {
|
||||
return ui_locales[key][currentLanguageIndex];
|
||||
@@ -242,7 +242,6 @@ export function createOptionsPanel(projectId: string | null, onSaved: () => void
|
||||
card("material", "B08_Quantity_Tab_Materials", ...surchargeBlock),
|
||||
card("earthwork", "B08_Quantity_Tab_Earthwork", rockRow, hint("암 전체 기준 · 두 칸 합 100")),
|
||||
card("hydrology", "B08_Quantity_Tab_Hydrology", hint("다음 옵션 자리")),
|
||||
card("milestone", "B08_Quantity_Tab_Milestone", ...milestoneCardBody(projectId)),
|
||||
actions,
|
||||
);
|
||||
attachCollapsible(panel);
|
||||
|
||||
@@ -149,8 +149,10 @@ def test_좌측_패널_컨테이너_셋과_저장_줄():
|
||||
"quantity",
|
||||
] # 자재 탭 안 = 총괄 · 콘크리트소요 · 자재수량
|
||||
assert 'select("material");' in page and 'select("summary");' in page
|
||||
assert [panel.index(f'card("{tab}", "B08_Quantity_Tab_') for tab in tabs] == sorted(
|
||||
panel.index(f'card("{tab}", "B08_Quantity_Tab_') for tab in tabs
|
||||
cards = tabs[:-1] # 244-6 — 거리이정표 카드 없음(다이어그램 모달)
|
||||
assert 'card("milestone"' not in panel
|
||||
assert [panel.index(f'card("{tab}", "B08_Quantity_Tab_') for tab in cards] == sorted(
|
||||
panel.index(f'card("{tab}", "B08_Quantity_Tab_') for tab in cards
|
||||
)
|
||||
assert 'card("summary", "B08_Quantity_Tab_StructureSummary", one.root),' in panel
|
||||
assert 'card("material", "B08_Quantity_Tab_Materials", ...surchargeBlock),' in panel
|
||||
|
||||
@@ -171,10 +171,7 @@ def test_메인_탭_하위_탭_추가_삭제():
|
||||
assert 'writeState("b08-milestone"' in state and 'clearState("b08-milestone"' in state
|
||||
assert state.count('method: "PUT"') == 1 and "setTimeout" not in state
|
||||
options = _read("B08_Quantity/B08_Quantity_UI_Options.ts")
|
||||
assert (
|
||||
'card("milestone", "B08_Quantity_Tab_Milestone", ...milestoneCardBody(projectId))'
|
||||
in options
|
||||
)
|
||||
assert 'card("milestone"' not in options and "milestoneCardBody" not in options # 244-6
|
||||
assert "await saveMilestone(projectId);" in options
|
||||
registry = _read("A00_Common/b_page_state.ts")
|
||||
assert '"b08-milestone": { bucket: "draft", scope: "project" }' in registry
|
||||
|
||||
@@ -144,7 +144,7 @@ def test_화면_모달_팬줌_그림탭():
|
||||
add = modal[
|
||||
modal.index("export function openAddItem") : modal.index("export function openRouteInfo")
|
||||
]
|
||||
for text in ('label: "항목"', 'label: "업체 이름"', 'label: "목적지"', 'label: "등록"',
|
||||
for text in ('label: "항목"', 'label: "거래처 이름"', 'label: "목적지"', 'label: "등록"',
|
||||
"buildAddressField(null)", "placeOf(address,"): # fmt: skip
|
||||
assert text in add
|
||||
assert "방위" not in add.replace("방위는 이 자리로 자동", "") and "dirOptions" not in modal
|
||||
|
||||
@@ -53,7 +53,7 @@ def test_좌표_있음_공사지_기준_방위각():
|
||||
def test_좌측_패널_방위_칸_없음_모달_안내():
|
||||
state = _read("B08_Quantity/B08_Quantity_UI_Milestone_State.ts")
|
||||
assert "공사지에서 본 방위" not in state and "dirOptions" not in state
|
||||
assert "d.base.dir" not in state and 'label: "기준점 주소"' in state
|
||||
assert "d.base.dir" not in state # 244-6 — 기준점 칸은 다이어그램 기준점 모달로
|
||||
modal = _read("B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts")
|
||||
base = modal[modal.index("export function openBaseInfo") : modal.index("interface SiteAddress")]
|
||||
assert "주소를 고르면 다이어그램에 실제 방위로 놓임" in base
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
"""244-6 B08 거리이정표 추가 고침(사용자 132 · 133번) — 표 폭 = 다이어그램 폭 ·
|
||||
거래처 상자 항목 글 · 좌측 패널 카드 없음(공사지 · 기준점 = 다이어그램 모달 · [기준점 +]) ·
|
||||
팬(가운데 · 빈 곳 왼쪽 끌기 · 원배율에서도) · 화면 글 「업체」 → 「거래처」.
|
||||
|
||||
팬은 공용 `A00_Common/b_svg_zoom_pan.ts` + `…_Milestone_Layout.ts`(panStart · panContent)를
|
||||
node 로 돌려 pointerdown · pointermove · pointerup 사건을 실제로 보내
|
||||
그림 묶음 transform 이 바뀌는지 잼.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
|
||||
UI = ("UI_Milestone.ts", "UI_Milestone_Diagram.ts", "UI_Milestone_Modal.ts",
|
||||
"UI_Milestone_Route.ts", "UI_Milestone_State.ts", "Milestone_Layout.ts") # fmt: skip
|
||||
|
||||
|
||||
def _read(rel):
|
||||
return (ROOT / rel).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _node(script):
|
||||
src = "".join(
|
||||
IMPORTS.sub("", _read(rel))
|
||||
for rel in ("A00_Common/b_svg_zoom_pan.ts", "B08_Quantity/B08_Quantity_Milestone_Layout.ts")
|
||||
)
|
||||
result = subprocess.run(
|
||||
["node", "--experimental-strip-types", "--no-warnings",
|
||||
"--input-type=module-typescript", "-e", src + "\n" + script],
|
||||
capture_output=True, text=True, encoding="utf-8", timeout=60,
|
||||
) # fmt: skip
|
||||
assert result.returncode == 0, result.stderr
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
PAN = """
|
||||
const W = 600, H = 400;
|
||||
class Svg extends EventTarget {
|
||||
classList = { add() {}, remove() {} };
|
||||
setPointerCapture() {}
|
||||
releasePointerCapture() {}
|
||||
getBoundingClientRect() { return { left: 0, top: 0, width: W, height: H }; }
|
||||
}
|
||||
const shape = { closest: (s: string) => (s.includes(".b08-milestone__box") ? {} : null) };
|
||||
const empty = { closest: () => null };
|
||||
function fire(svg: EventTarget, type: string, button: number, x: number, y: number,
|
||||
target: unknown) {
|
||||
const ev = new Event(type, { cancelable: true });
|
||||
for (const [k, v] of Object.entries({ button, clientX: x, clientY: y, pointerId: 1, target }))
|
||||
Object.defineProperty(ev, k, { value: v });
|
||||
svg.dispatchEvent(ev);
|
||||
}
|
||||
function drag(button: number, target: unknown) {
|
||||
const svg = new Svg();
|
||||
const layer = { t: "", setAttribute(_k: string, v: string) { this.t = v; } };
|
||||
attachSvgZoomPan({ svg: svg as any, layer: layer as any, widthPx: W, heightPx: H,
|
||||
pad: { left: 0, right: 0, top: 0, bottom: 0 }, wheelZoom: true,
|
||||
content: panContent(W, H), panStart });
|
||||
fire(svg, "pointerdown", button, 100, 100, target);
|
||||
fire(svg, "pointermove", button, 160, 130, target);
|
||||
fire(svg, "pointerup", button, 160, 130, target);
|
||||
return layer.t;
|
||||
}
|
||||
console.log(JSON.stringify({
|
||||
leftEmpty: drag(0, empty), leftShape: drag(0, shape),
|
||||
middleShape: drag(1, shape), right: drag(2, empty),
|
||||
}));
|
||||
"""
|
||||
|
||||
|
||||
def test_실제_포인터_사건으로_팬():
|
||||
got = _node(PAN)
|
||||
# 원배율(1)에서도 빈 곳 왼쪽 끌기 · 가운데 버튼 끌기 = 그림 묶음이 끈 만큼 옮김
|
||||
assert got["leftEmpty"] == "translate(60 30) scale(1)"
|
||||
assert got["middleShape"] == "translate(60 30) scale(1)"
|
||||
assert got["leftShape"] == "" and got["right"] == "" # 상자 위 왼쪽 = 그 화면 고르기 몫
|
||||
|
||||
|
||||
def test_거래처_상자_항목_글():
|
||||
data = {
|
||||
"site": {"mid_station_m": None, "mid_road": "비포장"},
|
||||
"base": {"enabled": False, "name": "", "address": "", "dir": None, "legs": []},
|
||||
"routes": [
|
||||
{"id": "g", "name": "거창군청", "address": "", "via_base": True, "dir": None,
|
||||
"legs": [], "rows": [{"item": "장비", "dest": "start", "note": ""},
|
||||
{"item": " 장비 ", "dest": "mid", "note": ""},
|
||||
{"item": "골재", "dest": "mid", "note": ""}]},
|
||||
{"id": "k", "name": "", "address": "", "via_base": True, "dir": None,
|
||||
"legs": [], "rows": [{"item": "", "dest": "start", "note": ""}]},
|
||||
],
|
||||
} # fmt: skip
|
||||
lay = _node(f"console.log(JSON.stringify(layoutMilestone({json.dumps(data)}, null, 0)));")
|
||||
boxes = {b["id"]: b for b in lay["boxes"]}
|
||||
assert boxes["g"]["items"] == "장비 · 골재" and boxes["g"]["label"] == "거창군청"
|
||||
assert boxes["k"]["items"] == "" and boxes["k"]["label"] == "거래처"
|
||||
ui = _read("B08_Quantity/B08_Quantity_UI_Milestone_Diagram.ts")
|
||||
assert 'svg("tspan", { x, class: "b08-milestone__items" }, box.items)' in ui
|
||||
assert 'svg("tspan", { x, dy: 16 }, box.label)' in ui
|
||||
|
||||
|
||||
def test_표_폭_다이어그램_폭_좌측_카드_없음():
|
||||
css = _read("B08_Quantity/B08_Quantity_UI_Milestone.css")
|
||||
block = css[css.index(".b08-milestone {") :]
|
||||
assert "width: fit-content;" in block[: block.index("}")] # 틀 = 표 너비
|
||||
assert ".b08-milestone > .b08-milestone__diagram {\n contain: inline-size;" in css
|
||||
ui = _read("B08_Quantity/B08_Quantity_UI_Milestone_Diagram.ts")
|
||||
assert 'root.style.width = "100%";' in ui # 다이어그램 = 틀 너비
|
||||
page = _read("B08_Quantity/B08_Quantity_UI_Milestone.ts")
|
||||
assert "children: [diagram.root, none, table.root]" in page
|
||||
# 좌측 패널 카드 없음 · 공사지 · 기준점은 모달
|
||||
options = _read("B08_Quantity/B08_Quantity_UI_Options.ts")
|
||||
assert "milestoneCardBody" not in options and "await saveMilestone(projectId);" in options
|
||||
assert "milestoneCardBody" not in _read("B08_Quantity/B08_Quantity_UI_Milestone_State.ts")
|
||||
modal = _read("B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts")
|
||||
site = modal[modal.index("export function openSiteInfo") :]
|
||||
assert 'label: "중점 측점 (m)"' in site and 'label: "시점 → 중점 도로"' in site
|
||||
assert "fresh.site.mid_station_m =" in site and "putMilestone(projectId, fresh)" in site
|
||||
base = modal[modal.index("export function openBaseInfo") : modal.index("interface SiteAddress")]
|
||||
assert "enabled: true," in base and 'label: "기준점 없앰"' in base
|
||||
assert "fresh.base.enabled = false;" in base
|
||||
assert 'label: "기준점 +"' in ui and "openBaseInfo(projectId, true)" in ui
|
||||
assert "addBase.hidden = data.base.enabled;" in ui
|
||||
assert "content: panContent(vw, vh)" in ui and "panStart," in ui
|
||||
|
||||
|
||||
def test_화면_글_거래처():
|
||||
for name in UI:
|
||||
text = _read(f"B08_Quantity/B08_Quantity_{name}")
|
||||
assert "업체" not in text, name
|
||||
modal = _read("B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts")
|
||||
assert 'label: "거래처 이름"' in modal
|
||||
page = _read("B08_Quantity/B08_Quantity_UI_Milestone.ts")
|
||||
assert '{ key: "company", label: "거래처 이름"' in page and "`거래처 ${" in page
|
||||
for name in UI:
|
||||
assert len(_read(f"B08_Quantity/B08_Quantity_{name}").splitlines()) <= 700
|
||||
Reference in New Issue
Block a user