merge: dev(254-1 여러 기준점) 합침 — 장소 그림 상자 id = bases id (256-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QND7Co43B24zK2P937rdxt
This commit is contained in:
@@ -118,7 +118,7 @@ def with_haul_routes(data: dict[str, Any], groups: list[dict[str, Any]]) -> dict
|
||||
"id": f"h-{g['key']}"[:40],
|
||||
"name": "",
|
||||
"address": "",
|
||||
"via_base": True,
|
||||
"to": "site",
|
||||
"linked": False,
|
||||
"dir": None,
|
||||
"haul": g["key"],
|
||||
|
||||
@@ -4,14 +4,16 @@
|
||||
거울 시험 `resources/tester/test_244_1_b08_milestone.py`.
|
||||
조작 중은 TS 짝이 바로 셈 · [저장] · 읽기 때 서버가 이 파일로 다시 셈.
|
||||
|
||||
표 한 줄 = 경로(업체) 구간 + (기준점 거침이면 기준점 → 시점 공통 구간) + (목적지 = 중점이면
|
||||
표 한 줄 = 경로(업체) 구간 + 기준점 사슬 구간들(254 · 거래처 `to` → 기준점 `to` → … → 시점 ·
|
||||
`base_chain` · 방문 집합으로 돌기 끊음 · 안 이은 기준점에서 멈춤) + (목적지 = 중점이면
|
||||
시점 → 중점 = 중점 측점 km) · 도로 구분 셋(2차포장 · 1차포장 · 비포장)마다 합 ·
|
||||
포장 = 2차 + 1차 · 비포장 · 합계 = 포장 + 비포장(견본 「거리이정표」 시트 수식) ·
|
||||
끝수 = 소수 셋째 자리(반올림).
|
||||
구간 km = 사용자 값 · 없으면 [길찾기] 자동 값(244-7).
|
||||
도로 구분(152) = 포장 종류는 선 자리로 정함(`road_of`) — 거래처 → 기준점 = 2차포장 · 기준점 → 시점 =
|
||||
1차포장 · 기준점 없이 거래처 → 시점 · 시점 → 중점 = 포장 · 사용자는 구간마다 비포장만 고름 ·
|
||||
포장 = 2차 + 1차 + 포장 · pick = 늘 거짓(옛 「도로 구분 고르기」 자리).
|
||||
도로 구분(152 · 254) = 포장 종류는 선 자리로 정함(`road_of`) — 시점에 닿는 기준점 구간 = 1차포장 ·
|
||||
그 밖 기준점 구간 · 거래처 → 기준점 = 2차포장 · 거래처 → 시점 · 시점 → 중점 = 포장 · 사용자는
|
||||
구간마다 비포장만 고름 · 포장 = 2차 + 1차 + 포장 · pick = 늘 거짓(옛 「도로 구분 고르기」 자리).
|
||||
옛 꼴(단일 `base` · 경로 `via_base`) = 읽을 때 `migrate_milestone` 로 새 꼴(`bases[]` · `to`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -21,8 +23,10 @@ from typing import Any
|
||||
|
||||
ROADS = ("2차포장", "1차포장", "포장", "비포장")
|
||||
KEYS = {"2차포장": "s2", "1차포장": "s1", "포장": "pv", "비포장": "un"}
|
||||
# 구간 자리 → 포장 종류(152) — via = 거래처 → 기준점 · base = 기준점 → 시점 · direct = 거래처 → 시점 · mid = 시점 → 중점
|
||||
# 구간 자리 → 포장 종류(152 · 254) — via = → 기준점 · base = 기준점 → 시점 ·
|
||||
# direct = 거래처 → 시점 · mid = 시점 → 중점
|
||||
PAVED = {"via": "2차포장", "base": "1차포장", "direct": "포장", "mid": "포장"}
|
||||
SITE = "site" # 이음 대상 = 시점(그 밖 = 기준점 id)
|
||||
|
||||
|
||||
def road_of(road: str | None, kind: str) -> str:
|
||||
@@ -55,21 +59,64 @@ def mid_km(data: dict[str, Any], mid_default_m: float | None) -> float:
|
||||
return float(station or 0) / 1000
|
||||
|
||||
|
||||
def base_chain(data: dict[str, Any], to: str) -> list[dict[str, Any]]:
|
||||
"""기준점 사슬(254) — to 부터 이은 기준점을 따라 시점까지 · 방문 집합(돌기 끊음) ·
|
||||
안 이은 · 없는 기준점에서 멈춤."""
|
||||
by_id = {b.get("id"): b for b in data.get("bases") or []}
|
||||
out: list[dict[str, Any]] = []
|
||||
seen: set[str] = set()
|
||||
while to != SITE and to not in seen:
|
||||
seen.add(to)
|
||||
base = by_id.get(to)
|
||||
if base is None or base.get("linked", True) is False:
|
||||
break
|
||||
out.append(base)
|
||||
to = base.get("to") or SITE
|
||||
return out
|
||||
|
||||
|
||||
def migrate_milestone(data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""옛 꼴 → 새 꼴(254) — 옛 `base`(켜짐) = bases[0] id "b1" ·
|
||||
경로 `via_base` = to "b1" | "site" · 새 꼴은 그대로(사본)."""
|
||||
rest = {k: v for k, v in data.items() if k != "base"}
|
||||
old = data.get("base") or {}
|
||||
fresh = isinstance(rest.get("bases"), list)
|
||||
on = not fresh and bool(old.get("enabled"))
|
||||
if fresh:
|
||||
bases = rest["bases"]
|
||||
elif on:
|
||||
keep = ("name", "address", "dir", "lon", "lat", "pos", "manual", "legs")
|
||||
bases = [{**{k: old[k] for k in keep if k in old}, "id": "b1", "to": SITE, "linked": True}]
|
||||
else:
|
||||
bases = []
|
||||
routes = []
|
||||
for route in rest.get("routes") or []:
|
||||
if not isinstance(route.get("to"), str):
|
||||
via = on and route.get("via_base", True) is not False
|
||||
route = {k: v for k, v in route.items() if k != "via_base"}
|
||||
route["to"] = "b1" if via else SITE
|
||||
routes.append(route)
|
||||
return {**rest, "bases": bases, "routes": routes}
|
||||
|
||||
|
||||
def _kind(owner: dict[str, Any], to_site: str, to_base: str) -> str:
|
||||
return to_site if (owner.get("to") or SITE) == SITE else to_base
|
||||
|
||||
|
||||
def compute_milestone(data: dict[str, Any], mid_default_m: float | None) -> list[dict[str, Any]]:
|
||||
"""표 줄 — 경로 차례 · 경로 안 줄 차례."""
|
||||
data = migrate_milestone(data)
|
||||
site = data.get("site") or {}
|
||||
base = data.get("base") or {}
|
||||
mid = {"road": site.get("mid_road") or "비포장", "km": mid_km(data, mid_default_m)}
|
||||
out: list[dict[str, Any]] = []
|
||||
for route in data.get("routes") or []:
|
||||
for index, row in enumerate(route.get("rows") or []):
|
||||
sums = {"s2": 0.0, "s1": 0.0, "pv": 0.0, "un": 0.0}
|
||||
linked = route.get("linked", True) is not False # 안 이은 거래처 = 구간 km 없음(155)
|
||||
via = linked and bool(base.get("enabled")) and route.get("via_base", True) is not False
|
||||
if linked:
|
||||
add_legs(sums, route.get("legs"), "via" if via else "direct")
|
||||
if via:
|
||||
add_legs(sums, base.get("legs"), "base")
|
||||
# 안 이은 거래처 = 구간 km 없음(155) · 이음 = 거래처 구간 + 기준점 사슬(254)
|
||||
if route.get("linked", True) is not False:
|
||||
add_legs(sums, route.get("legs"), _kind(route, "direct", "via"))
|
||||
for base in base_chain(data, route.get("to") or SITE):
|
||||
add_legs(sums, base.get("legs"), _kind(base, "base", "via"))
|
||||
if row.get("dest") == "mid":
|
||||
add_legs(sums, [mid], "mid")
|
||||
paved = sums["s2"] + sums["s1"] + sums["pv"]
|
||||
|
||||
@@ -6,20 +6,23 @@
|
||||
* 거울 시험 `resources/tester/test_244_1_b08_milestone.py`.
|
||||
* 조작 중은 이 파일이 바로 셈 · [저장] · 읽기 때 서버가 파이썬 짝으로 다시 셈.
|
||||
*
|
||||
* 표 한 줄 = 경로(업체) 구간 + (기준점 거침이면 기준점 → 시점 공통 구간) + (목적지 = 중점이면
|
||||
* 표 한 줄 = 경로(업체) 구간 + 기준점 사슬 구간들(254 · 거래처 `to` → 기준점 `to` → … → 시점 ·
|
||||
* `baseChain` · 방문 집합으로 돌기 끊음 · 안 이은 기준점에서 멈춤) + (목적지 = 중점이면
|
||||
* 시점 → 중점 = 중점 측점 km) · 도로 구분 셋마다 합 · 포장 = 2차 + 1차 · 합계 = 포장 + 비포장 ·
|
||||
* 끝수 = 소수 셋째 자리(반올림)
|
||||
* 구간 km = 사용자 값 · 없으면 [길찾기] 자동 값(244-7)
|
||||
* 도로 구분(152) = 포장 종류는 선 자리로 정함(`roadOf`) — 거래처 → 기준점 = 2차포장 · 기준점 → 시점 =
|
||||
* 1차포장 · 기준점 없이 거래처 → 시점 · 시점 → 중점 = 포장 · 사용자는 구간마다 비포장만 고름 ·
|
||||
* 포장 = 2차 + 1차 + 포장 · pick = 늘 거짓(옛 「도로 구분 고르기」 자리)
|
||||
* 도로 구분(152 · 254) = 포장 종류는 선 자리로 정함(`roadOf`) — 시점에 닿는 기준점 구간 = 1차포장 ·
|
||||
* 그 밖 기준점 구간 · 거래처 → 기준점 = 2차포장 · 거래처 → 시점 · 시점 → 중점 = 포장 · 사용자는
|
||||
* 구간마다 비포장만 고름 · 포장 = 2차 + 1차 + 포장 · pick = 늘 거짓(옛 「도로 구분 고르기」 자리)
|
||||
* 옛 꼴(단일 `base` · 경로 `via_base`) = 읽을 때 `migrateMilestone` 로 새 꼴(`bases[]` · `to`)
|
||||
* ========================================================================== */
|
||||
|
||||
export const ROADS = ["2차포장", "1차포장", "포장", "비포장"] as const;
|
||||
export type Road = (typeof ROADS)[number];
|
||||
/** 구간 자리(152) — 거래처 → 기준점(via) · 기준점 → 시점(base) · 거래처 → 시점(direct) · 시점 → 중점(mid) */
|
||||
/** 구간 자리(152 · 254) — 거래처 · 기준점 → 기준점(via) · 기준점 → 시점(base) · 거래처 → 시점(direct) ·
|
||||
* 시점 → 중점(mid) */
|
||||
export type SegKind = "via" | "base" | "direct" | "mid";
|
||||
/** 포장 종류 = 선 자리로 정함(152) — 기준점이 생기면 거래처 → 기준점 = 2차 · 기준점 → 시점 = 1차 · 없으면 포장 */
|
||||
/** 포장 종류 = 선 자리로 정함(152) — → 기준점 = 2차 · 기준점 → 시점 = 1차 · 거래처 → 시점 = 포장 */
|
||||
export const PAVED: Record<SegKind, Road> = {
|
||||
via: "2차포장",
|
||||
base: "1차포장",
|
||||
@@ -36,20 +39,81 @@ export function withRoads(data: MilestoneData): MilestoneData {
|
||||
return {
|
||||
...data,
|
||||
site: { ...data.site, mid_road: roadOf(data.site.mid_road, "mid") },
|
||||
base: { ...data.base, legs: fix(data.base.legs, "base") },
|
||||
routes: data.routes.map((r) => ({
|
||||
...r,
|
||||
legs: fix(r.legs, data.base.enabled && r.via_base !== false ? "via" : "direct"),
|
||||
})),
|
||||
bases: data.bases.map((b) => ({ ...b, legs: fix(b.legs, baseKind(b)) })),
|
||||
routes: data.routes.map((r) => ({ ...r, legs: fix(r.legs, routeKind(r)) })),
|
||||
};
|
||||
}
|
||||
|
||||
/** 선 자리 — from = 거래처 id · "base" · "start"(시점 → 중점) */
|
||||
/** 이음 대상 = 시점(254 · 그 밖 = 기준점 id) */
|
||||
export const SITE_ID = "site";
|
||||
const baseKind = (b: { to: string }): SegKind => (b.to === SITE_ID ? "base" : "via");
|
||||
const routeKind = (r: { to: string }): SegKind => (r.to === SITE_ID ? "direct" : "via");
|
||||
|
||||
/** 선 자리 — from = 거래처 id · 기준점 id · "start"(시점 → 중점) */
|
||||
export function segKind(data: MilestoneData, from: string): SegKind {
|
||||
if (from === "start") return "mid";
|
||||
if (from === "base") return "base";
|
||||
const base = data.bases.find((b) => b.id === from);
|
||||
if (base) return baseKind(base);
|
||||
const route = data.routes.find((r) => r.id === from);
|
||||
return data.base.enabled && route?.via_base !== false ? "via" : "direct";
|
||||
return route ? routeKind(route) : "direct";
|
||||
}
|
||||
|
||||
/** 기준점 사슬(254) — to 부터 이은 기준점을 따라 시점까지 · 방문 집합(돌기 끊음) · 안 이은 · 없는 기준점에서 멈춤 */
|
||||
export function baseChain(data: MilestoneData, to: string): MilestoneBase[] {
|
||||
const out: MilestoneBase[] = [];
|
||||
const seen = new Set<string>();
|
||||
let id = to;
|
||||
while (id !== SITE_ID && !seen.has(id)) {
|
||||
seen.add(id);
|
||||
const base = data.bases.find((b) => b.id === id);
|
||||
if (!base || base.linked === false) break;
|
||||
out.push(base);
|
||||
id = base.to;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
type OldBase = Partial<MilestoneBase> & { enabled?: boolean };
|
||||
type OldData = Partial<Omit<MilestoneData, "routes">> & {
|
||||
base?: OldBase | null;
|
||||
routes?: Array<Partial<MilestoneRoute> & { via_base?: boolean }>;
|
||||
};
|
||||
|
||||
/** 옛 꼴 → 새 꼴(254) — 옛 `base`(켜짐) = bases[0] id "b1" · 경로 `via_base` = to "b1" | "site" ·
|
||||
* 새 꼴은 그대로(사본) */
|
||||
export function migrateMilestone(data: OldData | MilestoneData): MilestoneData {
|
||||
const { base: old, ...rest } = data as OldData;
|
||||
const fresh = Array.isArray(rest.bases);
|
||||
const on = !fresh && !!old?.enabled;
|
||||
const bases: MilestoneBase[] = fresh
|
||||
? rest.bases!
|
||||
: on
|
||||
? [
|
||||
{
|
||||
id: "b1",
|
||||
name: old!.name ?? "",
|
||||
address: old!.address ?? "",
|
||||
dir: old!.dir ?? null,
|
||||
lon: old!.lon ?? null,
|
||||
lat: old!.lat ?? null,
|
||||
pos: old!.pos ?? null,
|
||||
manual: !!old!.manual,
|
||||
legs: old!.legs ?? [],
|
||||
to: SITE_ID,
|
||||
linked: true,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
const routes = (rest.routes ?? []).map((r) => {
|
||||
if (typeof r.to === "string") return r as MilestoneRoute;
|
||||
const { via_base, ...keep } = r;
|
||||
return { ...keep, to: on && via_base !== false ? "b1" : SITE_ID } as MilestoneRoute;
|
||||
});
|
||||
return {
|
||||
site: rest.site ?? { mid_station_m: null, mid_road: "비포장" },
|
||||
bases,
|
||||
routes,
|
||||
};
|
||||
}
|
||||
/** 8방 — 기준점(없으면 공사지) 기준 지도 방위 */
|
||||
export const DIRS = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"] as const;
|
||||
@@ -76,8 +140,8 @@ export interface MilestoneRoute {
|
||||
/** 업체 이름 = 하위 탭 이름 */
|
||||
name: string;
|
||||
address: string;
|
||||
/** 기준점 거침(기준점 없으면 무시 · 시점으로 바로) */
|
||||
via_base: boolean;
|
||||
/** 잇는 곳(254) — "site"(시점으로 바로) · 기준점 id */
|
||||
to: string;
|
||||
dir: Dir | null;
|
||||
lon?: number | null;
|
||||
lat?: number | null;
|
||||
@@ -91,25 +155,32 @@ export interface MilestoneRoute {
|
||||
haul?: string | null;
|
||||
/** 단가연결(251 · dg · sg · logic 꼴 · 정본) */
|
||||
ref?: Record<string, unknown> | null;
|
||||
/** 업체 → 기준점(거침) · 시점 구간 */
|
||||
/** 업체 → to(기준점 · 시점) 구간 */
|
||||
legs: Leg[];
|
||||
rows: MilestoneRow[];
|
||||
}
|
||||
|
||||
/** 기준점(254 · 여럿) — 거래처가 거쳐 가는 자리 · to = 시점 또는 다른 기준점 */
|
||||
export interface MilestoneBase {
|
||||
id: string;
|
||||
name: string;
|
||||
address: string;
|
||||
dir: Dir | null;
|
||||
lon?: number | null;
|
||||
lat?: number | null;
|
||||
pos?: [number, number] | null;
|
||||
manual?: boolean;
|
||||
/** 이 기준점 → to 구간 */
|
||||
legs: Leg[];
|
||||
/** 잇는 곳 — "site"(시점) · 다른 기준점 id */
|
||||
to: string;
|
||||
/** false = 아직 선 안 이음(요소 추가 뒤) · 없음 = 이음 */
|
||||
linked?: boolean;
|
||||
}
|
||||
|
||||
export interface MilestoneData {
|
||||
site: { mid_station_m: number | null; mid_road: Road };
|
||||
base: {
|
||||
enabled: boolean;
|
||||
name: string;
|
||||
address: string;
|
||||
dir: Dir | null;
|
||||
lon?: number | null;
|
||||
lat?: number | null;
|
||||
pos?: [number, number] | null;
|
||||
manual?: boolean;
|
||||
/** 기준점 → 시점 공통 구간 */
|
||||
legs: Leg[];
|
||||
};
|
||||
bases: MilestoneBase[];
|
||||
routes: MilestoneRoute[];
|
||||
}
|
||||
|
||||
@@ -153,19 +224,20 @@ export function midKm(data: MilestoneData, midDefaultM: number | null): number {
|
||||
|
||||
/** 표 줄 — 경로 차례 · 경로 안 줄 차례 */
|
||||
export function computeMilestone(
|
||||
data: MilestoneData,
|
||||
raw: MilestoneData,
|
||||
midDefaultM: number | null,
|
||||
): MilestoneTableRow[] {
|
||||
const base = data.base;
|
||||
const data = migrateMilestone(raw);
|
||||
const mid: Leg = { road: data.site?.mid_road ?? "비포장", km: midKm(data, midDefaultM) };
|
||||
const out: MilestoneTableRow[] = [];
|
||||
for (const route of data.routes ?? []) {
|
||||
(route.rows ?? []).forEach((row, index) => {
|
||||
const sums = { s2: 0, s1: 0, pv: 0, un: 0 };
|
||||
const linked = route.linked !== false; // 안 이은 거래처 = 구간 km 없음(155)
|
||||
const via = linked && !!base?.enabled && route.via_base !== false;
|
||||
if (linked) addLegs(sums, route.legs, via ? "via" : "direct");
|
||||
if (via) addLegs(sums, base.legs, "base");
|
||||
if (route.linked !== false) {
|
||||
// 안 이은 거래처 = 구간 km 없음(155) · 이음 = 거래처 구간 + 기준점 사슬(254)
|
||||
addLegs(sums, route.legs, routeKind(route));
|
||||
for (const b of baseChain(data, route.to)) addLegs(sums, b.legs, baseKind(b));
|
||||
}
|
||||
if (row.dest === "mid") addLegs(sums, [mid], "mid");
|
||||
const paved = sums.s2 + sums.s1 + sums.pv;
|
||||
out.push({
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
* 다른 선 · 다른 글을 피한 자리(`placeLabels`) · 상자 안 글 = 안쪽 여백 넘으면 줄임표
|
||||
* ========================================================================== */
|
||||
|
||||
import type { Dir, Leg, MilestoneData } from "./B08_Quantity_Milestone_Calc";
|
||||
import type { Dir, Leg, MilestoneBase, MilestoneData } from "./B08_Quantity_Milestone_Calc";
|
||||
|
||||
export interface DiagramBox {
|
||||
id: string;
|
||||
@@ -201,7 +201,7 @@ export function setAutoKm(legs: Leg[], km: number): Leg[] {
|
||||
}
|
||||
|
||||
export interface KmSegment {
|
||||
/** route = 거래처 구간 · base = 기준점 → 시점 공통 구간 */
|
||||
/** route = 거래처 구간 · base = 기준점 구간(→ 시점 · 다른 기준점) */
|
||||
key: "route" | "base";
|
||||
text: string;
|
||||
from: [number, number] | null;
|
||||
@@ -211,40 +211,64 @@ export interface KmSegment {
|
||||
const pointOf = (p: { lon?: number | null; lat?: number | null }): [number, number] | null =>
|
||||
p.lon != null && p.lat != null ? [p.lon, p.lat] : null;
|
||||
|
||||
/** 구간 한 줄(244-13 연결선) — from = 거래처 id(거래처 → 기준점(거침) · 시점) · "base"(기준점 → 시점) ·
|
||||
* 없는 구간 = null(시점 → 중점 = 손 입력 · 길찾기 안 함) */
|
||||
/** 이음 대상 = 시점(Calc `SITE_ID` 와 같음 · 형 import 만) */
|
||||
const TO_SITE = "site";
|
||||
const baseOf = (data: MilestoneData, id: string): MilestoneBase | undefined =>
|
||||
data.bases.find((b) => b.id === id);
|
||||
/** 구간 주인 — 기준점 · 거래처(from id) */
|
||||
export const ownerOf = (data: MilestoneData, from: string) =>
|
||||
baseOf(data, from) ?? data.routes.find((r) => r.id === from);
|
||||
|
||||
/** 기준점 사슬(254 · Calc `baseChain` 과 같음) — to 부터 시점까지 · 돌기 · 안 이음 · 없음에서 멈춤 */
|
||||
export function chainOf(data: MilestoneData, to: string): MilestoneBase[] {
|
||||
const out: MilestoneBase[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (let id = to; id !== TO_SITE && !seen.has(id);) {
|
||||
seen.add(id);
|
||||
const base = baseOf(data, id);
|
||||
if (!base || base.linked === false) break;
|
||||
out.push(base);
|
||||
id = base.to;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 돌기(254) — 기준점 from 을 to 에 이으면 to 사슬이 from 으로 돌아옴(A → B → A · 이음 여부 무관) */
|
||||
export function makesLoop(data: MilestoneData, from: string, to: string): boolean {
|
||||
const seen = new Set<string>();
|
||||
for (let id = to; id !== TO_SITE && !seen.has(id); id = baseOf(data, id)?.to ?? TO_SITE) {
|
||||
if (id === from) return true;
|
||||
seen.add(id);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 구간 한 줄(244-13 연결선) — from = 거래처 id · 기준점 id(→ to = 시점 · 기준점) ·
|
||||
* 없는 구간 = null(시점 → 중점 = 손 입력 · 길찾기 안 함 · 안 이음) */
|
||||
export function segmentEnds(data: MilestoneData, from: string, start: LonLat): KmSegment | null {
|
||||
const baseName = data.base.name || "기준점";
|
||||
if (from === "base")
|
||||
return data.base.enabled
|
||||
? { key: "base", text: `${baseName} → 시점`, from: pointOf(data.base), to: start ?? null }
|
||||
: null;
|
||||
const route = data.routes.find((r) => r.id === from);
|
||||
if (!route || route.linked === false) return null; // 155 안 이은 거래처 = 구간 없음
|
||||
const viaBase = data.base.enabled && route.via_base;
|
||||
const base = baseOf(data, from);
|
||||
const owner = base ?? data.routes.find((r) => r.id === from);
|
||||
if (!owner || owner.linked === false) return null; // 155 안 이은 상자 = 구간 없음
|
||||
const target = owner.to === TO_SITE ? undefined : baseOf(data, owner.to);
|
||||
if (owner.to !== TO_SITE && !target) return null;
|
||||
return {
|
||||
key: "route",
|
||||
text: `${route.name || "거래처"} → ${viaBase ? baseName : "시점"}`,
|
||||
from: pointOf(route),
|
||||
to: viaBase ? pointOf(data.base) : (start ?? null),
|
||||
key: base ? "base" : "route",
|
||||
text: `${owner.name || (base ? "기준점" : "거래처")} → ${target ? target.name || "기준점" : "시점"}`,
|
||||
from: pointOf(owner),
|
||||
to: target ? pointOf(target) : (start ?? null),
|
||||
};
|
||||
}
|
||||
|
||||
/** 기준점 거쳐 시점 들어가는 구간(244-13 · 도로 구분 2차 · 1차 · 비포장) — 기준점 → 시점 · 거침 거래처 */
|
||||
export function chainVia(data: MilestoneData, from: string): boolean {
|
||||
if (!data.base.enabled || from === "start") return false;
|
||||
return from === "base" || !!data.routes.find((r) => r.id === from)?.via_base;
|
||||
}
|
||||
|
||||
/** [길찾기] 구간 — 거래처 → 기준점 · 기준점 → 시점(거침) · 기준점 없으면 거래처 → 시점
|
||||
/** [길찾기] 구간 — 거래처 → to · 그 뒤 기준점 사슬 구간들(254)
|
||||
* (시점 → 중점 = 노선 측점 거리 그대로 · 길찾기 안 함) */
|
||||
export function kmSegments(data: MilestoneData, routeId: string, start: LonLat): KmSegment[] {
|
||||
const first = segmentEnds(data, routeId, start);
|
||||
if (!first) return [];
|
||||
return chainVia(data, routeId) ? [first, segmentEnds(data, "base", start)!] : [first];
|
||||
const route = data.routes.find((r) => r.id === routeId);
|
||||
if (!first || !route) return [];
|
||||
return [first, ...chainOf(data, route.to).map((b) => segmentEnds(data, b.id, start)!)];
|
||||
}
|
||||
|
||||
/** 카카오 지도 표지(244-9) — 거래처 · 기준점(거침 · 좌표 있을 때) · 시점 */
|
||||
/** 카카오 지도 표지(244-9) — 거래처 · 기준점 사슬(좌표 있을 때) · 시점 */
|
||||
export function mapMarks(
|
||||
data: MilestoneData,
|
||||
routeId: string,
|
||||
@@ -257,19 +281,15 @@ export function mapMarks(
|
||||
if (p) out.push({ name, lon: p[0], lat: p[1] });
|
||||
};
|
||||
add(route.name || "거래처", pointOf(route));
|
||||
if (data.base.enabled && route.via_base) add(data.base.name || "기준점", pointOf(data.base));
|
||||
if (route.linked !== false)
|
||||
for (const b of chainOf(data, route.to)) add(b.name || "기준점", pointOf(b));
|
||||
add("시점", start ?? null);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 자동 km 를 그 구간에(사용자 km 는 그대로) */
|
||||
export function putAutoKm(
|
||||
data: MilestoneData,
|
||||
routeId: string,
|
||||
key: KmSegment["key"],
|
||||
km: number,
|
||||
): MilestoneData {
|
||||
const legs = key === "base" ? data.base.legs : data.routes.find((r) => r.id === routeId)?.legs;
|
||||
/** 자동 km 를 그 구간에(from = 거래처 · 기준점 id · 사용자 km 는 그대로) */
|
||||
export function putAutoKm(data: MilestoneData, from: string, km: number): MilestoneData {
|
||||
const legs = ownerOf(data, from)?.legs;
|
||||
if (legs) setAutoKm(legs, km);
|
||||
return data;
|
||||
}
|
||||
@@ -363,8 +383,7 @@ export function layoutMilestone(
|
||||
};
|
||||
/** 상자 놓기 — 상자 안 글 줄임(안쪽 여백) · out = 바깥 방향(겹치면 GAP 날 때까지 그쪽으로 밀어 냄) */
|
||||
// 153 사용자가 끌어 놓은 자리(왼 위 · 그림 좌표) — 있으면 방위 자리 · 밀어 냄 대신 그 자리
|
||||
const posOf = (id: string): [number, number] | null | undefined =>
|
||||
id === "base" ? data.base.pos : data.routes.find((r) => r.id === id)?.pos;
|
||||
const posOf = (id: string): [number, number] | null | undefined => ownerOf(data, id)?.pos;
|
||||
const put = (box: DiagramBox, out?: [number, number]): void => {
|
||||
const inner = box.w - 2 * INSET;
|
||||
box.label = clip(box.label, inner, FONT.label);
|
||||
@@ -411,31 +430,60 @@ export function layoutMilestone(
|
||||
line("start", "mid", [{ road: data.site.mid_road, km: Math.round(midKm * 1000) / 1000 }]);
|
||||
|
||||
const startLL = site?.start_lonlat ?? null;
|
||||
const useBase = data.base.enabled;
|
||||
const baseLL: LonLat =
|
||||
data.base.lon != null && data.base.lat != null ? [data.base.lon, data.base.lat] : null;
|
||||
let baseDeg = 270;
|
||||
if (useBase) {
|
||||
baseDeg = placeDeg({ dir: null, lon: data.base.lon, lat: data.base.lat }, startLL) ?? 270;
|
||||
const [ux, uy] = unit(baseDeg);
|
||||
const cx = ux * R_BASE;
|
||||
const cy = uy * R_BASE;
|
||||
const llOf = (p?: { lon?: number | null; lat?: number | null }): LonLat =>
|
||||
p && p.lon != null && p.lat != null ? [p.lon, p.lat] : null;
|
||||
const used = new Set<string>();
|
||||
const keyOf = (anchor: string, deg: number): string => `${anchor}:${Math.round(deg) % 360}`;
|
||||
/** 상자 id → 잇는 곳에서 본 방위(기준점 둘레 빈 방위 = 그 방위에서 돌림 · 되돌아가는 쪽 뺌) */
|
||||
const degOf = new Map<string, number>();
|
||||
let auto = 0;
|
||||
/** 방위 안 정한 상자 — 빈 8방 차례(기준점 둘레 = 잇는 곳 반대편부터 · 잇는 곳으로 향하는 쪽 뺌) */
|
||||
const free = (anchor: string): number => {
|
||||
const turn = anchor === "site" ? 0 : (degOf.get(anchor) ?? 0);
|
||||
const blocked = anchor === "site" ? -1 : turn + 180;
|
||||
const cands = AUTO.map((d) => (DIR_DEG[d] + turn) % 360).filter(
|
||||
(c) => keyOf(anchor, c) !== keyOf(anchor, blocked),
|
||||
);
|
||||
return cands.find((c) => !used.has(keyOf(anchor, c))) ?? cands[auto++ % cands.length];
|
||||
};
|
||||
/** 잇는 곳 상자 — 놓인 기준점이면 그 id · 아니면 공사지 */
|
||||
const anchorOf = (to: string): string => (to !== "site" && byId.has(to) ? to : "site");
|
||||
|
||||
// 기준점(254 · 여럿) — 잇는 곳(시점 · 다른 기준점) 기준 방위(좌표 → 방위각 · 없으면 서부터 빈 방위) ·
|
||||
// 잇는 곳이 먼저 놓이게 사슬 차례 · 돌기 = 공사지 둘레
|
||||
const waiting = [...data.bases];
|
||||
while (waiting.length) {
|
||||
let i = waiting.findIndex(
|
||||
(b) => b.linked === false || b.to === "site" || byId.has(b.to) || !ownerOf(data, b.to),
|
||||
);
|
||||
if (i < 0) i = 0;
|
||||
const [b] = waiting.splice(i, 1);
|
||||
const anchor = b.linked === false ? "site" : anchorOf(b.to);
|
||||
const anchorLL = anchor === "site" ? startLL : (llOf(ownerOf(data, anchor)) ?? startLL);
|
||||
const deg =
|
||||
placeDeg({ dir: null, lon: b.lon, lat: b.lat }, anchorLL) ??
|
||||
(anchor === "site" && !used.has(keyOf("site", 270)) ? 270 : free(anchor));
|
||||
used.add(keyOf(anchor, deg));
|
||||
degOf.set(b.id, deg);
|
||||
const [ax, ay] = center(anchor);
|
||||
const [ux, uy] = unit(deg);
|
||||
put(
|
||||
{
|
||||
id: "base",
|
||||
id: b.id,
|
||||
kind: "base",
|
||||
x: cx - BOX.w / 2,
|
||||
y: cy - BOX.h / 2,
|
||||
x: ax + ux * R_BASE - BOX.w / 2,
|
||||
y: ay + uy * R_BASE - BOX.h / 2,
|
||||
w: BOX.w,
|
||||
h: BOX.h,
|
||||
label: data.base.name || "기준점",
|
||||
label: b.name || "기준점",
|
||||
},
|
||||
[ux, uy],
|
||||
);
|
||||
line("base", "start", data.base.legs);
|
||||
if (b.linked !== false && (b.to === "site" || byId.has(b.to)))
|
||||
line(b.id, b.to === "site" ? "start" : b.to, b.legs);
|
||||
}
|
||||
|
||||
// 거래처 — 기준점(거침) · 공사지 기준 방위 · 같은 방위끼리 옆으로 나란히
|
||||
// 거래처 — 잇는 곳(기준점 · 공사지) 기준 방위 · 같은 방위끼리 옆으로 나란히
|
||||
const groups = new Map<
|
||||
string,
|
||||
Array<{
|
||||
@@ -446,25 +494,15 @@ export function layoutMilestone(
|
||||
anchor: string;
|
||||
deg: number;
|
||||
legs: Leg[];
|
||||
linked: boolean;
|
||||
}>
|
||||
>();
|
||||
const used = new Set<string>();
|
||||
const keyOf = (anchor: string, deg: number): string => `${anchor}:${Math.round(deg) % 360}`;
|
||||
const items = data.routes.map((r) => {
|
||||
const anchor = useBase && r.via_base ? "base" : "site";
|
||||
return { r, anchor, deg: placeDeg(r, anchor === "base" ? (baseLL ?? startLL) : startLL) };
|
||||
const anchor = anchorOf(r.to);
|
||||
const anchorLL = anchor === "site" ? startLL : (llOf(ownerOf(data, anchor)) ?? startLL);
|
||||
return { r, anchor, deg: placeDeg(r, anchorLL) };
|
||||
});
|
||||
for (const it of items) if (it.deg !== null) used.add(keyOf(it.anchor, it.deg));
|
||||
let auto = 0;
|
||||
/** 방위 안 고른 거래처 — 빈 8방 차례(기준점 쪽 = 공사지 반대편부터 · 기준점 · 공사지로 향하는 쪽 뺌) */
|
||||
const free = (anchor: string): number => {
|
||||
const turn = anchor === "base" ? baseDeg : 0;
|
||||
const blocked = anchor === "base" ? baseDeg + 180 : useBase ? baseDeg : -1;
|
||||
const cands = AUTO.map((d) => (DIR_DEG[d] + turn) % 360).filter(
|
||||
(c) => keyOf(anchor, c) !== keyOf(anchor, blocked),
|
||||
);
|
||||
return cands.find((c) => !used.has(keyOf(anchor, c))) ?? cands[auto++ % cands.length];
|
||||
};
|
||||
for (const { r, anchor, deg } of items) {
|
||||
const d = deg ?? free(anchor);
|
||||
used.add(keyOf(anchor, d));
|
||||
@@ -479,11 +517,13 @@ export function layoutMilestone(
|
||||
anchor,
|
||||
deg: d,
|
||||
legs: r.legs,
|
||||
// 155 안 이은 거래처 = 선 없음(연결 점으로 이음) · 없는 기준점에 이음 = 선 없음
|
||||
linked: r.linked !== false && (r.to === "site" || anchor === r.to),
|
||||
});
|
||||
}
|
||||
for (const group of groups.values()) {
|
||||
group.forEach((g, k) => {
|
||||
const [ax, ay] = center(g.anchor === "base" ? "base" : "site");
|
||||
const [ax, ay] = center(g.anchor);
|
||||
const [ux, uy] = unit(g.deg);
|
||||
// 나란히 = 방위에 직각 · 상자 직각 폭 + 여유만큼(④)
|
||||
const spread = Math.abs(ux) * BOX.h + Math.abs(uy) * BOX.w + GAP;
|
||||
@@ -506,9 +546,7 @@ export function layoutMilestone(
|
||||
},
|
||||
[ux, uy],
|
||||
);
|
||||
// 155 안 이은 거래처 = 선 없음(연결 점으로 이음)
|
||||
if (data.routes.find((r) => r.id === g.id)?.linked !== false)
|
||||
line(g.id, g.anchor === "base" ? "base" : "start", g.legs);
|
||||
if (g.linked) line(g.id, g.anchor === "site" ? "start" : g.anchor, g.legs);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -530,11 +568,10 @@ export function layoutMilestone(
|
||||
};
|
||||
}
|
||||
|
||||
/** 선 글 한 줄 → 그 구간 — from = 거래처 id(거래처 구간) · "base"(공통 구간) · "start"(시점 → 중점) */
|
||||
/** 선 글 한 줄 → 그 구간 — from = 거래처 id · 기준점 id · "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;
|
||||
return legs?.[i] ?? null;
|
||||
return ownerOf(data, from)?.legs[i] ?? null;
|
||||
}
|
||||
|
||||
/** 선 글에서 고친 값 → 데이터(시점 → 중점 km = 중점 측점 m) · 고친 것만 */
|
||||
@@ -550,8 +587,7 @@ export function setLeg(
|
||||
data.site.mid_station_m = patch.km == null ? null : Math.round(patch.km * 1000 * 1000) / 1000;
|
||||
return data;
|
||||
}
|
||||
const legs = from === "base" ? data.base.legs : data.routes.find((r) => r.id === from)?.legs;
|
||||
const leg = legs?.[i];
|
||||
const leg = ownerOf(data, from)?.legs[i];
|
||||
if (leg) Object.assign(leg, patch);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ GET 답 `place_images` {상자 id: "place/<id>.png"} · `place_times` {상자 id
|
||||
공사지 = 시점(노선 첫 점 · 측점 0) · 중점 기본 = 노선 연장 ÷ 2 — 노선은 설계 노선 정본 차례
|
||||
(수정본 → 초기 폴리라인 → 예상노선 → 초기값 · `load_design_route` 와 같은 차례).
|
||||
표 합 = `B08_Quantity_Milestone_Calc`(파이썬 · TS 짝).
|
||||
기준점 = `bases[]`(254 · 여럿 · to = 시점 또는 다른 기준점) · 경로 `to` — 옛 `base` · `via_base` 는
|
||||
읽을 때 · 받을 때 새 꼴로 옮김(`migrate_milestone` · 저장 = 새 꼴).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -60,7 +62,7 @@ from B01_Dashboard.B01_Dashboard_Map import reverse_geocode
|
||||
from B02_ProjRegister.B02_ProjRegister_Repository import get_project_by_id
|
||||
from B03_FileInput.B03_FileInput_Engine import save_upload_stream
|
||||
from B08_Quantity.B08_Quantity_Haul import haul_groups, with_haul_routes
|
||||
from B08_Quantity.B08_Quantity_Milestone_Calc import compute_milestone, r3
|
||||
from B08_Quantity.B08_Quantity_Milestone_Calc import SITE, compute_milestone, migrate_milestone, r3
|
||||
from B08_Quantity.B08_Quantity_Milestone_Capture import make_place_image, make_route_image
|
||||
from B08_Quantity.B08_Quantity_Router_Hydro import _project_root
|
||||
from B08_Quantity.B08_Quantity_Router_Material import material_sheets
|
||||
@@ -83,6 +85,7 @@ PLACE = "place" # 장소 그림 하위 폴더(256-1)
|
||||
PRINTS = ".prints.json" # 그림 지문 {"place/<id>": 해시} — 바뀐 것만 다시 캡처
|
||||
IMAGE_EXT = (".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp")
|
||||
ROUTE_ID = r"^[A-Za-z0-9_-]{1,40}$"
|
||||
BOX_IDS = {SITE, "start", "mid"} # 다이어그램 공사지 상자 id — 기준점 · 경로 id 로 못 씀
|
||||
|
||||
Road = Literal["2차포장", "1차포장", "포장", "비포장"] # 포장 = 기준점 안 거친 구간(244-13)
|
||||
Dir = Literal["N", "NE", "E", "SE", "S", "SW", "W", "NW"]
|
||||
@@ -114,7 +117,7 @@ class Place(BaseModel):
|
||||
|
||||
class Route(Place):
|
||||
id: str = Field(pattern=ROUTE_ID)
|
||||
via_base: bool = True
|
||||
to: str = Field(default=SITE, pattern=ROUTE_ID) # 254 잇는 곳 = "site"(시점) · 기준점 id
|
||||
# 251 운반 묶음 키(총괄자재 · B09 운반 줄과 이음) · 단가연결(dg · sg · logic 꼴 · 정본)
|
||||
haul: str | None = Field(default=None, max_length=60, pattern=r"^[A-Za-z0-9_-]+$")
|
||||
ref: dict[str, Any] | None = None
|
||||
@@ -123,7 +126,11 @@ class Route(Place):
|
||||
|
||||
|
||||
class Base(Place):
|
||||
enabled: bool = False
|
||||
"""기준점(254 · 여럿) — to = 시점 또는 다른 기준점 id(돌기는 화면이 막고 셈이 끊음)."""
|
||||
|
||||
id: str = Field(pattern=ROUTE_ID)
|
||||
to: str = Field(default=SITE, pattern=ROUTE_ID)
|
||||
linked: bool = True # False = 아직 선 안 이음(요소 추가 뒤)
|
||||
|
||||
|
||||
class Site(BaseModel):
|
||||
@@ -133,14 +140,22 @@ class Site(BaseModel):
|
||||
|
||||
class Milestone(BaseModel):
|
||||
site: Site = Field(default_factory=Site)
|
||||
base: Base = Field(default_factory=Base)
|
||||
bases: list[Base] = Field(default_factory=list, max_length=20)
|
||||
routes: list[Route] = Field(default_factory=list, max_length=100)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def _migrate(cls, data: Any) -> Any:
|
||||
"""옛 꼴(단일 base · via_base) → 새 꼴(254)."""
|
||||
return migrate_milestone(data) if isinstance(data, dict) else data
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _unique_ids(self) -> Milestone:
|
||||
ids = [route.id for route in self.routes]
|
||||
ids = [b.id for b in self.bases] + [route.id for route in self.routes]
|
||||
if len(ids) != len(set(ids)):
|
||||
raise ValueError("경로 id 가 겹칩니다.")
|
||||
raise ValueError("경로 · 기준점 id 가 겹칩니다.")
|
||||
if BOX_IDS & set(ids):
|
||||
raise ValueError("site · start · mid 는 id 로 못 씁니다.")
|
||||
return self
|
||||
|
||||
|
||||
@@ -225,11 +240,11 @@ def images(root: str | Path, sub: str = "") -> dict[str, str]:
|
||||
|
||||
|
||||
def places(data: dict[str, Any]) -> list[tuple[str, str, float, float]]:
|
||||
"""좌표가 정해진 상자 → [(상자 id, 이름, 경도, 위도)] — 거래처 = 경로 id · 기준점 = "b1"."""
|
||||
# ponytail: 단일 기준점 — 254-1 bases[] 합친 뒤 bases[].id 로
|
||||
boxes = [(r["id"], r.get("name") or "", r.get("lon"), r.get("lat")) for r in data["routes"]]
|
||||
base = data.get("base") or {}
|
||||
boxes.append(("b1", base.get("name") or "기준점", base.get("lon"), base.get("lat")))
|
||||
"""좌표가 정해진 상자 → [(상자 id, 이름, 경도, 위도)] — 거래처 = 경로 id · 기준점 = bases id."""
|
||||
boxes = [
|
||||
(b["id"], b.get("name") or "", b.get("lon"), b.get("lat"))
|
||||
for b in [*data.get("routes", []), *data.get("bases", [])]
|
||||
]
|
||||
return [(i, n, lon, lat) for i, n, lon, lat in boxes if lon is not None and lat is not None]
|
||||
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ export function addRoute(projectId: string, init: Partial<MilestoneRoute> = {}):
|
||||
id,
|
||||
name: `거래처 ${data.routes.length + 1}`,
|
||||
address: "",
|
||||
via_base: true,
|
||||
to: "site",
|
||||
dir: null,
|
||||
legs: [{ road: null, km: null }], // 도로 구분 = 사용자 고름(244-7)
|
||||
rows: [{ item: "", dest: "start", note: "" }],
|
||||
@@ -168,12 +168,12 @@ function tableView(projectId: string): MilestoneView {
|
||||
const data = liveMilestone(projectId);
|
||||
const rows = computeMilestone(data, siteInfo()?.mid_default_m ?? null);
|
||||
// 기준점 생김 · 없앰 = 열 바뀜 → 표 새로
|
||||
const next = `${data.base.enabled}:${rows.some((r) => r.pv > 0)}`;
|
||||
const next = `${data.bases.length > 0}:${rows.some((r) => r.pv > 0)}`;
|
||||
if (next !== mode) {
|
||||
mode = next;
|
||||
const fresh = makeTable(
|
||||
milestoneColumns(
|
||||
data.base.enabled,
|
||||
data.bases.length > 0,
|
||||
rows.some((r) => r.pv > 0),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
import {
|
||||
layoutMilestone,
|
||||
legAt,
|
||||
ownerOf,
|
||||
panContent,
|
||||
panStart,
|
||||
setLeg,
|
||||
@@ -158,7 +159,7 @@ export function createDiagram(
|
||||
site: () => openSiteInfo(projectId),
|
||||
start: () => openSiteInfo(projectId),
|
||||
mid: () => openSiteInfo(projectId),
|
||||
base: () => openPlaceModal(projectId, { add: false, id: "base" }),
|
||||
base: (id) => openPlaceModal(projectId, { add: false, id }),
|
||||
};
|
||||
|
||||
/** 153 상자 끌어 옮김 — 4px 넘게 움직이면 끌기(누름 = 모달은 그대로) · 놓으면 그 자리 저장(캐시 · [저장] 때 서버)
|
||||
@@ -189,7 +190,7 @@ export function createDiagram(
|
||||
Math.round(box.y + (e.clientY - down.clientY) / k),
|
||||
];
|
||||
const fresh = liveMilestone(projectId);
|
||||
const target = box.id === "base" ? fresh.base : fresh.routes.find((r) => r.id === box.id);
|
||||
const target = ownerOf(fresh, box.id);
|
||||
if (!target) return;
|
||||
target.pos = pos;
|
||||
userZoomed = true; // 옮긴 뒤 배율 그대로
|
||||
@@ -200,8 +201,9 @@ export function createDiagram(
|
||||
});
|
||||
};
|
||||
|
||||
/** 154 연결 점 ● — 거래처 상자 오른쪽 가운데 · 끌어 기준점 상자 = 기준점 거침 · 공사지(시점 · 중점) = 시점으로 바로 ·
|
||||
* 빈 곳 · 같은 연결 = 취소 · 바뀌면 그 거래처 구간 새로(km 비움) + 길찾기 한 번(km · 경로 그림) */
|
||||
/** 154 연결 점 ● — 거래처 상자 오른쪽 가운데 · 끌어 기준점 상자 = 그 기준점 거침(254 · to = 기준점 id) ·
|
||||
* 공사지(시점 · 중점) = 시점으로 바로 · 빈 곳 · 같은 연결 = 취소 · 바뀌면 그 거래처 구간 새로(km 비움) +
|
||||
* 길찾기 한 번(km · 경로 그림) */
|
||||
const connectDot = (box: DiagramBox, shift: SVGGElement): SVGCircleElement => {
|
||||
const cx = box.x + box.w;
|
||||
const cy = box.y + box.h / 2;
|
||||
@@ -221,12 +223,13 @@ export function createDiagram(
|
||||
const wire = svg("line", { class: "b08-milestone__wire", x1: cx, y1: cy, x2: cx, y2: cy });
|
||||
shift.append(wire);
|
||||
let over: Element | null = null;
|
||||
const targetOf = (e: PointerEvent): "base" | "site" | null => {
|
||||
/** 놓을 곳 — 기준점 id · "site"(공사지 · 시점 · 중점) · 없음 */
|
||||
const targetOf = (e: PointerEvent): string | null => {
|
||||
const hit = document.elementFromPoint(e.clientX, e.clientY);
|
||||
const g = hit?.closest<SVGGElement>("[data-b08-milestone-box]");
|
||||
const id = g?.dataset.b08MilestoneBox;
|
||||
if (id === "base") return "base";
|
||||
return id === "site" || id === "start" || id === "mid" ? "site" : null;
|
||||
if (id === "site" || id === "start" || id === "mid") return "site";
|
||||
return id && liveMilestone(projectId).bases.some((b) => b.id === id) ? id : null;
|
||||
};
|
||||
const move = (e: PointerEvent): void => {
|
||||
const [x, y] = toLocal(e);
|
||||
@@ -249,10 +252,8 @@ export function createDiagram(
|
||||
const fresh = liveMilestone(projectId);
|
||||
const route = fresh.routes.find((r) => r.id === box.id);
|
||||
if (!t || !route) return;
|
||||
const via = t === "base";
|
||||
const now = route.linked === false ? null : fresh.base.enabled && route.via_base !== false;
|
||||
if (via === now) return; // 같은 연결 = 그대로
|
||||
route.via_base = via;
|
||||
if (route.linked !== false && route.to === t) return; // 같은 연결 = 그대로
|
||||
route.to = t;
|
||||
route.linked = true; // 155 요소 추가 뒤 첫 이음
|
||||
route.legs = [{ road: null, km: null }]; // 옛 구간 km = 다른 길 — 새로 길찾기
|
||||
putMilestone(projectId, fresh);
|
||||
@@ -270,7 +271,7 @@ export function createDiagram(
|
||||
variant: "ghost",
|
||||
onClick: () => {
|
||||
const fresh = liveMilestone(projectId);
|
||||
fresh.base.pos = null;
|
||||
fresh.bases.forEach((b) => (b.pos = null));
|
||||
fresh.routes.forEach((r) => (r.pos = null));
|
||||
userZoomed = false;
|
||||
putMilestone(projectId, fresh);
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* 거리이정표 연결선 모달(PLAN 244-13 · 사용자 147번 8) — 다이어그램 선 · 선 글 누름.
|
||||
*
|
||||
* 값 = 도로 구분 ▾ · 거리(km) 구간 줄(빈칸 = [길찾기] 자동 값 · 사용자 값 우선 · 244-7)
|
||||
* 도로 구분(152) = 포장 종류는 선 자리로 정함 — 거래처 → 기준점 = 2차포장 · 기준점 → 시점 = 1차포장 ·
|
||||
* 기준점 없이 거래처 → 시점 · 시점 → 중점 = 포장 · ▾ 는 그 포장 · 비포장 둘(`roadOptions` · `roadOf`)
|
||||
* 도로 구분(152 · 254) = 포장 종류는 선 자리로 정함 — → 기준점 = 2차포장 · 기준점 → 시점 = 1차포장 ·
|
||||
* 거래처 → 시점 · 시점 → 중점 = 포장 · ▾ 는 그 포장 · 비포장 둘(`roadOptions` · `roadOf`)
|
||||
* 아래 = 다음(카카오) 지도 경로(그 구간 두 끝 · 마지막 길찾기 길 선 · 없으면 열 때 한 번 받음 · 자동 그림 없음)
|
||||
* 거래처 · 기준점 표지 끌어 옮김 = 소재지 고침(좌표 → VWorld 주소) + 그 구간 다시 길찾기 · 시점 = 노선(고정)
|
||||
* 기준점 → 시점 = 도로가 안 이어질 수 있음 — 거리 손 입력 · 시점 → 중점 = 늘 손 입력(길찾기 · 지도 없음)
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
type Road,
|
||||
type SegKind,
|
||||
} from "./B08_Quantity_Milestone_Calc";
|
||||
import { chainVia, segmentEnds, setLeg } from "./B08_Quantity_Milestone_Layout";
|
||||
import { ownerOf, segmentEnds, setLeg } from "./B08_Quantity_Milestone_Layout";
|
||||
import { editMap, type MapPin } from "./B08_Quantity_UI_Milestone_KakaoMap";
|
||||
import { toastKm } from "./B08_Quantity_UI_Milestone_Modal";
|
||||
import { reverseAddress } from "./B08_Quantity_UI_Milestone_Place";
|
||||
@@ -144,7 +144,7 @@ function openMidLeg(projectId: string): void {
|
||||
});
|
||||
}
|
||||
|
||||
/** 연결선 누름 — from = 거래처 id(거래처 → 기준점 · 시점) · "base"(기준점 → 시점) · "start"(시점 → 중점) */
|
||||
/** 연결선 누름 — from = 거래처 · 기준점 id(→ 기준점 · 시점 · 254) · "start"(시점 → 중점) */
|
||||
export function openLegInfo(projectId: string, from: string): void {
|
||||
if (from === "start") return openMidLeg(projectId);
|
||||
const start = (): [number, number] | null => siteInfo()?.start_lonlat ?? null;
|
||||
@@ -155,11 +155,8 @@ export function openLegInfo(projectId: string, from: string): void {
|
||||
closeLabel: "닫기",
|
||||
dialogClass: "b08-milestone__modal",
|
||||
mount: (body, close, actions) => {
|
||||
const legsOf = (): Leg[] => {
|
||||
const d = liveMilestone(projectId);
|
||||
return (from === "base" ? d.base.legs : d.routes.find((r) => r.id === from)?.legs) ?? [];
|
||||
};
|
||||
const via = chainVia(liveMilestone(projectId), from);
|
||||
const legsOf = (): Leg[] => ownerOf(liveMilestone(projectId), from)?.legs ?? [];
|
||||
const isBase = liveMilestone(projectId).bases.some((b) => b.id === from);
|
||||
const kind = segKind(liveMilestone(projectId), from);
|
||||
let legs = legsEditor(legsOf(), kind);
|
||||
const status = hint("");
|
||||
@@ -167,7 +164,7 @@ export function openLegInfo(projectId: string, from: string): void {
|
||||
/** 모달 구간 값 → 캐시(길찾기 · 옮김 앞) */
|
||||
const apply = (): void => {
|
||||
const fresh = liveMilestone(projectId);
|
||||
const target = from === "base" ? fresh.base : fresh.routes.find((r) => r.id === from);
|
||||
const target = ownerOf(fresh, from);
|
||||
if (!target) return;
|
||||
target.legs = legs.value();
|
||||
putMilestone(projectId, fresh);
|
||||
@@ -177,18 +174,20 @@ export function openLegInfo(projectId: string, from: string): void {
|
||||
legs.root.replaceWith(next.root);
|
||||
legs = next;
|
||||
};
|
||||
/** 표지 = 그 구간 두 끝(주인 · to = 기준점 또는 시점) */
|
||||
const pins = (): MapPin[] => {
|
||||
const d = liveMilestone(projectId);
|
||||
const route = d.routes.find((r) => r.id === from);
|
||||
const owner = ownerOf(d, from);
|
||||
const out: MapPin[] = [];
|
||||
const add = (key: string, name: string, lon?: number | null, lat?: number | null): void => {
|
||||
if (lon != null && lat != null)
|
||||
out.push({ key, name, lon, lat, movable: key !== "start" });
|
||||
};
|
||||
if (route) add(route.id, route.name || "거래처", route.lon, route.lat);
|
||||
if (from === "base" || via) add("base", d.base.name || "기준점", d.base.lon, d.base.lat);
|
||||
if (!owner) return out;
|
||||
add(owner.id, owner.name || (isBase ? "기준점" : "거래처"), owner.lon, owner.lat);
|
||||
const target = d.bases.find((b) => b.id === owner.to);
|
||||
if (target) add(target.id, target.name || "기준점", target.lon, target.lat);
|
||||
else add("start", "시점", start()?.[0], start()?.[1]);
|
||||
if (from === "base") add("start", "시점", start()?.[0], start()?.[1]);
|
||||
return out;
|
||||
};
|
||||
const drawMap = (): void => {
|
||||
@@ -214,7 +213,7 @@ export function openLegInfo(projectId: string, from: string): void {
|
||||
apply();
|
||||
void reverseAddress(projectId, lon, lat).then((address) => {
|
||||
const fresh = liveMilestone(projectId);
|
||||
const target = key === "base" ? fresh.base : fresh.routes.find((r) => r.id === key);
|
||||
const target = ownerOf(fresh, key);
|
||||
if (!target) return;
|
||||
Object.assign(target, { lon, lat, address });
|
||||
putMilestone(projectId, fresh);
|
||||
@@ -242,14 +241,13 @@ export function openLegInfo(projectId: string, from: string): void {
|
||||
save.dataset.b08MilestoneLegSave = "";
|
||||
actions.append(find, save);
|
||||
// 155 [수기 입력] — 카카오 길찾기 연결 끊음(자동 값 지움 · 길찾기 · 지도 숨김 · km 손 입력) · 풀면 다시 길찾기
|
||||
const owner = (d = liveMilestone(projectId)) =>
|
||||
from === "base" ? d.base : d.routes.find((r) => r.id === from);
|
||||
const owner = (d = liveMilestone(projectId)) => ownerOf(d, from);
|
||||
const manual = createCheckbox("수기 입력 — 카카오 길찾기 안 함(거리 손 입력)");
|
||||
manual.input.checked = !!owner()?.manual;
|
||||
manual.input.dataset.b08MilestoneManual = "";
|
||||
const auto = hint(
|
||||
from === "base"
|
||||
? "기준점 → 시점 = 도로가 안 이어질 수 있음 — 거리를 손으로 넣어도 됨(빈칸 = 길찾기 자동 값)"
|
||||
isBase
|
||||
? "기준점 구간 = 도로가 안 이어질 수 있음 — 거리를 손으로 넣어도 됨(빈칸 = 길찾기 자동 값)"
|
||||
: "빈칸 = 길찾기 자동 값 · 넣은 값이 우선",
|
||||
);
|
||||
const mapHead = el("h4", { text: "경로 — 다음(카카오) 지도" });
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
* 상자 하나 = 항목 하나(149 · [+ 줄] 없음 · 같은 거래처라도 항목마다 따로 등록) · 경로 값은 연결선 모달
|
||||
* 연결(기준점 거침 · 시점으로 바로) = 다이어그램 연결 점 ● 끌기(154 · 155 · 등록 = 상자만 · 선 없음)
|
||||
* 열기 = 다이어그램 [요소 추가](155 · 공사지 · 기준점 [+] 없앰)
|
||||
* 분류 기준위치 = 기준점(하나 · 항목 · 목적지 없음 · 있으면 고르기 막음) · 방위 = 소재지 좌표로 자동 자리
|
||||
* 분류 기준위치 = 기준점(254 · 여럿 · 새 `bases` 원소 · 항목 · 목적지 없음 · 등록 = 상자만 · 연결 점으로 이음) ·
|
||||
* 방위 = 소재지 좌표로 자동 자리 · 기준점 [삭제] = 그 기준점에 이은 거래처 · 기준점은 안 이음으로
|
||||
* 구간 km · 도로 구분 · 길찾기 = 연결선 모달(`…_UI_Milestone_LegModal`) · [등록] · 소재지 옮김 뒤 그 구간 한 번
|
||||
* 시점(공사지) 상자 = 프로젝트 등록 주소 · VWorld 주소(좌표 → 주소 · 서버) · 아래 지도
|
||||
* 조작 = 캐시 · [저장] 때 서버
|
||||
@@ -87,20 +88,21 @@ function rowField(r: MilestoneRow, kind: "route" | "base") {
|
||||
|
||||
export type PlaceTarget = { add: true } | { add: false; id: string };
|
||||
|
||||
/** 등록 모달 — 추가([+]) · 고침(거래처 · 기준점 상자) 한 벌 · id "base" = 기준점 */
|
||||
/** 등록 모달 — 추가([+]) · 고침(거래처 · 기준점 상자) 한 벌 · id = 거래처 · 기준점 id */
|
||||
export function openPlaceModal(
|
||||
projectId: string,
|
||||
target: PlaceTarget,
|
||||
addRoute?: (init: Partial<MilestoneRoute>) => string,
|
||||
): void {
|
||||
const data = liveMilestone(projectId);
|
||||
const editBase = !target.add && target.id === "base";
|
||||
const base = target.add ? undefined : data.bases.find((b) => b.id === target.id);
|
||||
const editBase = !!base;
|
||||
const route = !target.add && !editBase ? data.routes.find((r) => r.id === target.id) : undefined;
|
||||
if (!target.add && !editBase && !route) return;
|
||||
const title = target.add
|
||||
? "요소 추가 — 기준점 · 거래처"
|
||||
: editBase
|
||||
? `기준점 — ${data.base.name || "기준점"}`
|
||||
: base
|
||||
? `기준점 — ${base.name || "기준점"}`
|
||||
: `항목 정보 — ${route!.name || "거래처"}`;
|
||||
openModal({
|
||||
title,
|
||||
@@ -118,20 +120,14 @@ export function openPlaceModal(
|
||||
});
|
||||
sort.select.value = kind;
|
||||
sort.select.disabled = !target.add;
|
||||
// 기준점 = 하나 — 이미 있으면 [+]에서 기준위치 못 고름(기준점 상자에서 고침)
|
||||
const baseOption = sort.select.querySelector<HTMLOptionElement>('option[value="base"]');
|
||||
if (target.add && data.base.enabled && baseOption) {
|
||||
baseOption.disabled = true;
|
||||
baseOption.textContent = "기준위치(있음 — 기준점 상자에서 수정)";
|
||||
}
|
||||
sort.select.dataset.b08MilestoneAdd = "sort";
|
||||
const name = createInputField({ label: "거래처 이름", compact: true });
|
||||
name.input.value = editBase ? data.base.name : (route?.name ?? "");
|
||||
name.input.value = base ? base.name : (route?.name ?? "");
|
||||
name.input.dataset.b08MilestoneAdd = "company";
|
||||
const nameLabel = name.root.querySelector(".ui-field__label");
|
||||
// 상자 하나 = 항목 하나(149) — 옛 자료의 둘째 줄부터는 손대지 않고 그대로 둠
|
||||
const first = rowField(route?.rows[0] ?? { item: "", dest: "start", note: "" }, kind);
|
||||
const old = editBase ? data.base : route;
|
||||
const old = base ?? route;
|
||||
const place = placePicker(
|
||||
projectId,
|
||||
{ address: old?.address ?? "", lon: old?.lon ?? null, lat: old?.lat ?? null },
|
||||
@@ -163,11 +159,24 @@ export function openPlaceModal(
|
||||
const where = place.value();
|
||||
const fresh = liveMilestone(projectId);
|
||||
let segment: string | undefined; // [등록] · 소재지 옮김 뒤 그 구간 길찾기 한 번
|
||||
if (kind === "base") {
|
||||
Object.assign(fresh.base, where, { enabled: true, name: text, dir: null });
|
||||
if (!fresh.base.legs.length) fresh.base.legs = [{ road: null, km: null }];
|
||||
if (kind === "base" && base) {
|
||||
const b = fresh.bases.find((x) => x.id === base.id);
|
||||
if (!b) return close();
|
||||
Object.assign(b, where, { name: text, dir: null });
|
||||
putMilestone(projectId, fresh);
|
||||
if (moved(where)) segment = b.id;
|
||||
} else if (kind === "base") {
|
||||
// 254 새 기준점 = 상자만 · 선은 연결 점 ● 로 이음(이을 때 길찾기)
|
||||
fresh.bases.push({
|
||||
id: `b${Date.now().toString(36)}${Math.floor(Math.random() * 1296).toString(36)}`,
|
||||
name: text,
|
||||
dir: null,
|
||||
legs: [{ road: null, km: null }],
|
||||
to: "site",
|
||||
linked: false,
|
||||
...where,
|
||||
});
|
||||
putMilestone(projectId, fresh);
|
||||
if (moved(where)) segment = "base";
|
||||
} else if (route) {
|
||||
const r = fresh.routes.find((x) => x.id === route.id);
|
||||
if (!r) return close();
|
||||
@@ -180,9 +189,9 @@ export function openPlaceModal(
|
||||
} else {
|
||||
if (!text) return name.setError("거래처 이름을 넣으세요.");
|
||||
// 155 등록 = 상자만 · 선은 다이어그램 연결 점 ● 로 이음(이을 때 길찾기)
|
||||
const id = addRoute?.({
|
||||
addRoute?.({
|
||||
name: text,
|
||||
via_base: true,
|
||||
to: "site",
|
||||
linked: false,
|
||||
dir: null,
|
||||
rows: rowValues(),
|
||||
@@ -198,12 +207,17 @@ export function openPlaceModal(
|
||||
label: "삭제",
|
||||
variant: "danger",
|
||||
onClick: async () => {
|
||||
const label = editBase ? data.base.name || "기준점" : route?.name || "거래처";
|
||||
const label = base ? base.name || "기준점" : route?.name || "거래처";
|
||||
if (!(await showConfirmDialog(`「${label}」 항목을 지울까요? — [저장] 때 반영`, "삭제")))
|
||||
return;
|
||||
const fresh = liveMilestone(projectId);
|
||||
if (editBase) fresh.base = { enabled: false, name: "", address: "", dir: null, legs: [] };
|
||||
else fresh.routes = fresh.routes.filter((r) => r.id !== route!.id); // 경로 탭도 같이
|
||||
if (base) {
|
||||
fresh.bases = fresh.bases.filter((b) => b.id !== base.id);
|
||||
// 254 그 기준점에 이은 상자 = 안 이음(옛 구간 km = 다른 길 · 연결 점으로 다시 이음)
|
||||
for (const o of [...fresh.bases, ...fresh.routes])
|
||||
if (o.to === base.id)
|
||||
Object.assign(o, { to: "site", linked: false, legs: [{ road: null, km: null }] });
|
||||
} else fresh.routes = fresh.routes.filter((r) => r.id !== route!.id); // 경로 탭도 같이
|
||||
putMilestone(projectId, fresh);
|
||||
close();
|
||||
},
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from "@ui/ui_template_context_menu";
|
||||
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
import type { Dest, MilestoneData, MilestoneRoute } from "./B08_Quantity_Milestone_Calc";
|
||||
import { chainOf, ownerOf } from "./B08_Quantity_Milestone_Layout";
|
||||
import { editMap, type MapPin } from "./B08_Quantity_UI_Milestone_KakaoMap";
|
||||
import { reverseAddress } from "./B08_Quantity_UI_Milestone_Place";
|
||||
import {
|
||||
@@ -131,28 +132,26 @@ export function routeView(projectId: string, routeId: string): MilestoneView {
|
||||
if (!r) return out;
|
||||
add(r.id, r.name || "거래처", r.lon, r.lat);
|
||||
if (r.linked === false) return out;
|
||||
if (d.base.enabled && r.via_base) add("base", d.base.name || "기준점", d.base.lon, d.base.lat);
|
||||
for (const b of chainOf(d, r.to)) add(b.id, b.name || "기준점", b.lon, b.lat); // 254 기준점 사슬
|
||||
add("start", "시점", start()?.[0], start()?.[1]);
|
||||
return out;
|
||||
};
|
||||
const lines = (): Array<Array<[number, number]>> => {
|
||||
const d = liveMilestone(projectId);
|
||||
const r = d.routes.find((x) => x.id === routeId);
|
||||
const via = !!r && r.linked !== false && d.base.enabled && r.via_base;
|
||||
return [segmentPath(routeId), ...(via ? [segmentPath("base")] : [])].filter(
|
||||
(p) => p.length >= 2,
|
||||
);
|
||||
const chain = r && r.linked !== false ? chainOf(d, r.to).map((b) => b.id) : [];
|
||||
return [routeId, ...chain].map(segmentPath).filter((p) => p.length >= 2);
|
||||
};
|
||||
const map = editMap(projectId, {
|
||||
hint: "",
|
||||
onMove: (key, lon, lat) =>
|
||||
void reverseAddress(projectId, lon, lat).then((addr) => {
|
||||
const fresh = liveMilestone(projectId);
|
||||
const target = key === "base" ? fresh.base : fresh.routes.find((x) => x.id === key);
|
||||
const target = ownerOf(fresh, key);
|
||||
if (!target) return;
|
||||
Object.assign(target, { lon, lat, address: addr });
|
||||
putMilestone(projectId, fresh);
|
||||
void findSegmentKm(projectId, key === "base" ? "base" : routeId).then(() => drawMap());
|
||||
void findSegmentKm(projectId, key).then(() => drawMap());
|
||||
}),
|
||||
});
|
||||
const drawMap = (): void => map.set(pins(), lines());
|
||||
|
||||
@@ -11,18 +11,24 @@
|
||||
* 경로 좌표(244-9) = 마지막 [길찾기] 응답을 메모리에만(카카오 지도 길 선 · 새로 고침이면 사라짐)
|
||||
* 경로 그림 자동(244-11) = [길찾기] 뒤 길 선 · 표지를 서버 POST …/image/auto → 서버 헤드리스 캡처 그림
|
||||
* (뒤에서 · 화면 안 잠금) · 사용자 그림 우선 · 실패 = 까닭 한 줄(알림 · 경로 탭) · 좌표는 서버도 안 남김
|
||||
* 길찾기 = 연결선 한 구간씩(244-13 · `findSegmentKm` · 거래처 → 기준점(또는 시점) · 기준점 → 시점) ·
|
||||
* 길찾기 = 연결선 한 구간씩(244-13 · `findSegmentKm` · 거래처 · 기준점 → to(기준점 · 시점) · 254) ·
|
||||
* 자동 그림 = 거래처 쪽 구간 길 선 · 표지 둘(거래처 · 기준점 또는 시점)
|
||||
* 장소 그림(256-1) = 상자 좌표가 정해지면 `capturePlace` → 서버 캡처 `place/<상자 id>.png` ·
|
||||
* [저장] 뒤엔 서버가 바뀐 상자만 다시(다음 읽기에 보임) · `placeImageKey` · `placeImageBlobUrl`
|
||||
* 기준점 여럿(254) = `bases[]` · 길 선 표 키 = 거래처 · 기준점 id · 옛 꼴 캐시 = 읽을 때 새 꼴(`migrateMilestone`)
|
||||
* ========================================================================== */
|
||||
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { clearState, readState, writeState } from "../A00_Common/b_page_state";
|
||||
import { request } from "./B08_Quantity_UI_Hydro";
|
||||
import { PAVED, type MilestoneData, type SegKind } from "./B08_Quantity_Milestone_Calc";
|
||||
import { mapMarks, putAutoKm, segmentEnds } from "./B08_Quantity_Milestone_Layout";
|
||||
import {
|
||||
PAVED,
|
||||
migrateMilestone,
|
||||
type MilestoneData,
|
||||
type SegKind,
|
||||
} from "./B08_Quantity_Milestone_Calc";
|
||||
import { mapMarks, ownerOf, putAutoKm, segmentEnds } from "./B08_Quantity_Milestone_Layout";
|
||||
import type { SiteAddress } from "./B08_Quantity_UI_Milestone_Modal";
|
||||
|
||||
export interface SiteInfo {
|
||||
@@ -43,7 +49,7 @@ interface Payload {
|
||||
|
||||
export const EMPTY: MilestoneData = {
|
||||
site: { mid_station_m: null, mid_road: "비포장" },
|
||||
base: { enabled: false, name: "", address: "", dir: null, legs: [] },
|
||||
bases: [],
|
||||
routes: [],
|
||||
};
|
||||
|
||||
@@ -101,9 +107,11 @@ export const routeImageKey = (routeId: string): string | null => {
|
||||
export const hasMilestoneDraft = (projectId: string): boolean =>
|
||||
readState<MilestoneData>("b08-milestone", projectId) !== null;
|
||||
|
||||
/** 지금 값 — 캐시 → 저장본 → 빈 꼴(늘 복사본) */
|
||||
/** 지금 값 — 캐시 → 저장본 → 빈 꼴(늘 복사본 · 옛 꼴 캐시 = 새 꼴 · 254) */
|
||||
export function liveMilestone(projectId: string): MilestoneData {
|
||||
return clone(readState<MilestoneData>("b08-milestone", projectId) ?? saved?.data ?? EMPTY);
|
||||
return migrateMilestone(
|
||||
clone(readState<MilestoneData>("b08-milestone", projectId) ?? saved?.data ?? EMPTY),
|
||||
);
|
||||
}
|
||||
|
||||
/** 고친 한 벌을 캐시에 · 화면에 알림 */
|
||||
@@ -255,7 +263,7 @@ export function roadOptions(kind: SegKind): Array<{ value: string; text: string
|
||||
return [PAVED[kind], "비포장"].map((r) => ({ value: r, text: r }));
|
||||
}
|
||||
|
||||
/** 마지막 [길찾기] 길 선 — 구간(from = 거래처 id · "base") → [위도, 경도] 목록(메모리만 · 저장 안 함) */
|
||||
/** 마지막 [길찾기] 길 선 — 구간(from = 거래처 · 기준점 id) → [위도, 경도] 목록(메모리만 · 저장 안 함) */
|
||||
const paths = new Map<string, Array<[number, number]>>();
|
||||
export const segmentPath = (from: string): Array<[number, number]> => paths.get(from) ?? [];
|
||||
/** 경로 탭 · 자동 그림 길 선 = 거래처 쪽 구간 */
|
||||
@@ -270,7 +278,7 @@ export async function findSegmentKm(
|
||||
image = true,
|
||||
): Promise<string> {
|
||||
const data = liveMilestone(projectId);
|
||||
const owner = from === "base" ? data.base : data.routes.find((r) => r.id === from);
|
||||
const owner = ownerOf(data, from);
|
||||
if (owner?.manual) return ""; // 155 수기 입력 = 카카오 길찾기 안 함
|
||||
const seg = segmentEnds(data, from, siteInfo()?.start_lonlat ?? null);
|
||||
if (!seg) return "";
|
||||
@@ -294,9 +302,8 @@ export async function findSegmentKm(
|
||||
if ((res.path?.length ?? 0) >= 2) paths.set(from, res.path!);
|
||||
else paths.delete(from);
|
||||
const fresh = liveMilestone(projectId); // 기다리는 동안 고친 값 위에
|
||||
const legs = from === "base" ? fresh.base.legs : fresh.routes.find((r) => r.id === from)?.legs;
|
||||
if (legs?.[0]?.auto_km !== res.km) {
|
||||
putAutoKm(fresh, from, seg.key, res.km);
|
||||
if (ownerOf(fresh, from)?.legs[0]?.auto_km !== res.km) {
|
||||
putAutoKm(fresh, from, res.km);
|
||||
putMilestone(projectId, fresh);
|
||||
} else changes.dispatchEvent(new Event("change"));
|
||||
if (image && seg.key === "route" && paths.has(from))
|
||||
|
||||
@@ -16,6 +16,7 @@ if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B08_Quantity import B08_Quantity_Router_Milestone as ms # noqa: E402
|
||||
from B08_Quantity.B08_Quantity_Milestone_Calc import migrate_milestone # noqa: E402
|
||||
|
||||
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
|
||||
LONG = "경상남도 거창군 북상면 병곡리 968-31"
|
||||
@@ -58,6 +59,7 @@ def _clipped(note, box_w):
|
||||
|
||||
|
||||
def _lay(data, site=None):
|
||||
data = migrate_milestone(data) # 254 옛 꼴 → bases · to(옛 기준점 = b1)
|
||||
return _node(f"layoutMilestone({json.dumps(data, ensure_ascii=False)}, {json.dumps(site)}, 1)")
|
||||
|
||||
|
||||
|
||||
@@ -21,7 +21,10 @@ if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B08_Quantity import B08_Quantity_Router_Milestone as ms # noqa: E402
|
||||
from B08_Quantity.B08_Quantity_Milestone_Calc import compute_milestone # noqa: E402
|
||||
from B08_Quantity.B08_Quantity_Milestone_Calc import ( # noqa: E402
|
||||
compute_milestone,
|
||||
migrate_milestone,
|
||||
)
|
||||
|
||||
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
|
||||
B08 = "B08_Quantity/B08_Quantity_"
|
||||
@@ -90,29 +93,29 @@ def test_포장_도로_구분_거울():
|
||||
|
||||
|
||||
def test_도로_구분_갈래_연결선_구간_끝():
|
||||
d = json.dumps(WITH_BASE)
|
||||
off = json.dumps({**WITH_BASE, "base": {**WITH_BASE["base"], "enabled": False}})
|
||||
# 254 옛 꼴 → bases · to(옛 기준점 = b1)
|
||||
d = json.dumps(migrate_milestone(WITH_BASE))
|
||||
off = {**WITH_BASE, "base": {**WITH_BASE["base"], "enabled": False}}
|
||||
off = json.dumps(migrate_milestone(off))
|
||||
got = _node(
|
||||
[CALC, LAYOUT],
|
||||
f"""const D = {d}; const O = {off}; const S = [127.83, 35.64];
|
||||
console.log(JSON.stringify([["g", "d", "base", "start"].map((f) => PAVED[segKind(D, f)]),
|
||||
["g", "base"].map((f) => segKind(O, f)),
|
||||
["g", "d", "base", "start"].map((f) => chainVia(D, f)), ["g", "base"].map((f) => chainVia(O, f)),
|
||||
segmentEnds(D, "g", S), segmentEnds(D, "d", S), segmentEnds(D, "base", S),
|
||||
segmentEnds(D, "start", S), segmentEnds(O, "base", S), segmentEnds(O, "g", S),
|
||||
console.log(JSON.stringify([["g", "d", "b1", "start"].map((f) => PAVED[segKind(D, f)]),
|
||||
["g", "b1"].map((f) => segKind(O, f)),
|
||||
segmentEnds(D, "g", S), segmentEnds(D, "d", S), segmentEnds(D, "b1", S),
|
||||
segmentEnds(D, "start", S), segmentEnds(O, "b1", S), segmentEnds(O, "g", S),
|
||||
kmSegments(D, "g", S).map((s) => s.key), kmSegments(D, "d", S).map((s) => s.key)]));""",
|
||||
)
|
||||
# 152 포장 종류 = 선 자리 — 기준점 거침 거래처 2차 · 안 거침 포장 · 기준점 → 시점 1차 · 시점 → 중점 포장
|
||||
assert got[0] == ["2차포장", "포장", "1차포장", "포장"] and got[1] == ["direct", "base"]
|
||||
assert got[2] == [True, False, True, False] and got[3] == [False, False]
|
||||
assert got[4] == {"key": "route", "text": "g → 병곡리", "from": [127.9, 35.68],
|
||||
assert got[0] == ["2차포장", "포장", "1차포장", "포장"] and got[1] == ["direct", "direct"]
|
||||
assert got[2] == {"key": "route", "text": "g → 병곡리", "from": [127.9, 35.68],
|
||||
"to": [127.85, 35.66]} # fmt: skip
|
||||
assert got[5]["text"] == "d → 시점" and got[5]["to"] == [127.83, 35.64]
|
||||
assert got[6] == {"key": "base", "text": "병곡리 → 시점", "from": [127.85, 35.66],
|
||||
assert got[3]["text"] == "d → 시점" and got[3]["to"] == [127.83, 35.64]
|
||||
assert got[4] == {"key": "base", "text": "병곡리 → 시점", "from": [127.85, 35.66],
|
||||
"to": [127.83, 35.64]} # fmt: skip
|
||||
assert got[7] is None and got[8] is None # 시점 → 중점 = 손 입력 · 기준점 없음
|
||||
assert got[9]["text"] == "g → 시점" # 기준점 꺼짐 = 시점으로 바로
|
||||
assert got[10] == ["route", "base"] and got[11] == ["route"]
|
||||
assert got[5] is None and got[6] is None # 시점 → 중점 = 손 입력 · 기준점 없음
|
||||
assert got[7]["text"] == "g → 시점" # 기준점 꺼짐 = 시점으로 바로
|
||||
assert got[8] == ["route", "base"] and got[9] == ["route"]
|
||||
|
||||
|
||||
def test_표_열_기준점_따라():
|
||||
@@ -237,13 +240,13 @@ await loadMilestone("p1");
|
||||
const a = await findSegmentKm("p1", "g");
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
const afterRoute = CALLS.length;
|
||||
const b = await findSegmentKm("p1", "base");
|
||||
const b = await findSegmentKm("p1", "b1"); // 254 옛 꼴 캐시 → 기준점 id b1
|
||||
const writes = WRITES.n;
|
||||
await findSegmentKm("p1", "base"); // 같은 km = 캐시 안 씀
|
||||
await findSegmentKm("p1", "b1"); // 같은 km = 캐시 안 씀
|
||||
const live = liveMilestone("p1");
|
||||
console.log(JSON.stringify({{ a, b, calls: CALLS.slice(1), afterRoute, writes, again: WRITES.n,
|
||||
route: live.routes[0].legs, base: live.base.legs, path: segmentPath("g"),
|
||||
routeLines: routePaths("g"), baseLines: routePaths("base"),
|
||||
route: live.routes[0].legs, base: live.bases[0].legs, path: segmentPath("g"),
|
||||
routeLines: routePaths("g"), baseLines: routePaths("b1"),
|
||||
start: await findSegmentKm("p1", "start") }}));"""
|
||||
got = _node([CALC, LAYOUT, STATE], script, STATE_STUBS)
|
||||
assert got["a"] == "g → 병곡리 10.5 km" and got["b"] == "병곡리 → 시점 10.5 km"
|
||||
@@ -290,7 +293,7 @@ def test_화면_등록_모달_연결선_모달():
|
||||
assert "editMap(projectId" in place and "onClick: (lon, lat) => void moveTo(lon, lat)" in place
|
||||
assert "/quantity/milestone/reverse?lon=" in place
|
||||
# ② 상자 누름 = 같은 등록 모달 · [기준점 +] 없음
|
||||
assert 'base: () => openPlaceModal(projectId, { add: false, id: "base" }),' in diagram
|
||||
assert "base: (id) => openPlaceModal(projectId, { add: false, id })," in diagram
|
||||
assert 'label: "기준점 +"' not in diagram and "addBase" not in diagram
|
||||
# ③ 연결선 누름 = 연결선 모달 · 시점 → 중점 = 손 입력만
|
||||
assert "openLegInfo(projectId, ln.from);" in diagram and "b08-milestone__line-hit" in diagram
|
||||
|
||||
@@ -14,10 +14,13 @@ ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B08_Quantity.B08_Quantity_Milestone_Calc import migrate_milestone # noqa: E402
|
||||
|
||||
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
|
||||
|
||||
|
||||
def _layout(data, site=None, mid_km=0.75):
|
||||
data = migrate_milestone(data) # 254 옛 꼴(base · via_base) → bases · to(옛 기준점 = b1)
|
||||
src = (ROOT / "B08_Quantity" / "B08_Quantity_Milestone_Layout.ts").read_text(encoding="utf-8")
|
||||
body = (
|
||||
f"\nconst d = {json.dumps(data, ensure_ascii=False)};"
|
||||
@@ -95,17 +98,17 @@ def test_기준점_기준_방위_선_글():
|
||||
), # fmt: skip
|
||||
)
|
||||
cx, cy = _center(lay, "site")
|
||||
bx, by = _center(lay, "base")
|
||||
bx, by = _center(lay, "b1")
|
||||
assert bx < cx - 200 and abs(by - cy) < 1 # 기준점 = 공사지 서쪽
|
||||
gx, gy = _center(lay, "g")
|
||||
assert abs(gx - bx) < 1 and gy < by - 200 # 거창군청 = 기준점 북쪽
|
||||
kx, _ = _center(lay, "k")
|
||||
assert kx > cx + 200 # 기준점 안 거침 = 공사지 동쪽
|
||||
labels = {(ln["from"], ln["to"]): ln["labels"] for ln in lay["lines"]}
|
||||
assert labels[("g", "base")] == ["2차포장 23.8km"]
|
||||
assert labels[("base", "start")] == ["1차포장 0.6km", "비포장 0.8km"]
|
||||
assert labels[("g", "b1")] == ["2차포장 23.8km"]
|
||||
assert labels[("b1", "start")] == ["1차포장 0.6km", "비포장 0.8km"]
|
||||
assert labels[("k", "start")] == []
|
||||
assert next(b for b in lay["boxes"] if b["id"] == "base")["label"] == "병곡리 968-31"
|
||||
assert next(b for b in lay["boxes"] if b["id"] == "b1")["label"] == "병곡리 968-31"
|
||||
|
||||
|
||||
def test_경위도_있으면_방위각이_8방보다_앞섬():
|
||||
|
||||
@@ -18,6 +18,7 @@ if str(ROOT) not in sys.path:
|
||||
|
||||
from B01_Dashboard import B01_Dashboard_Map as vmap # noqa: E402
|
||||
from B08_Quantity import B08_Quantity_Router_Milestone as ms # noqa: E402
|
||||
from B08_Quantity.B08_Quantity_Milestone_Calc import migrate_milestone # noqa: E402
|
||||
|
||||
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
|
||||
|
||||
@@ -58,8 +59,9 @@ def test_방위_좌표_자동():
|
||||
# 기준점 좌표 없음 → 시점 좌표 기준 · 업체 = 시점 남쪽 · 기준점 안 거침 업체 = 동쪽
|
||||
"routes": [_route("s", 127.8, 35.6), _route("e", 127.9, 35.7, via=False)],
|
||||
}
|
||||
lay = _node(f"layoutMilestone({json.dumps(data)}, {json.dumps(site)}, 0)")
|
||||
bx, by = _center(lay, "base")
|
||||
# 254 옛 꼴 → bases · to(옛 기준점 = b1)
|
||||
lay = _node(f"layoutMilestone({json.dumps(migrate_milestone(data))}, {json.dumps(site)}, 0)")
|
||||
bx, by = _center(lay, "b1")
|
||||
sx, sy = _center(lay, "s")
|
||||
assert abs(sx - bx) < 1 and sy > by + 200 # 남 = 아래
|
||||
cx, cy = _center(lay, "site")
|
||||
@@ -68,8 +70,9 @@ def test_방위_좌표_자동():
|
||||
# 기준점 좌표 있음 → 기준점 기준(업체가 기준점 북쪽)
|
||||
data["base"] = {**base, "lon": 127.7, "lat": 35.7}
|
||||
data["routes"] = [_route("n", 127.7, 35.8)]
|
||||
lay = _node(f"layoutMilestone({json.dumps(data)}, {json.dumps(site)}, 0)")
|
||||
bx, by = _center(lay, "base")
|
||||
# 254 옛 꼴 → bases · to(옛 기준점 = b1)
|
||||
lay = _node(f"layoutMilestone({json.dumps(migrate_milestone(data))}, {json.dumps(site)}, 0)")
|
||||
bx, by = _center(lay, "b1")
|
||||
nx, ny = _center(lay, "n")
|
||||
assert bx < cx - 200 # 기준점 = 시점 서쪽(좌표)
|
||||
assert abs(nx - bx) < 1 and ny < by - 200
|
||||
@@ -188,17 +191,17 @@ def test_선_글_바로_고침_모달과_한_벌():
|
||||
"legs": [{"road": "1차포장", "km": 0.6}]},
|
||||
"routes": [{**_route("g"), "legs": [{"road": "2차포장", "km": None}]}],
|
||||
} # fmt: skip
|
||||
d = json.dumps(data)
|
||||
d = json.dumps(migrate_milestone(data)) # 254 옛 꼴 → bases · to(옛 기준점 = b1)
|
||||
got = _node(
|
||||
f"[legAt({d}, 'g', 0, 0.75), legAt({d}, 'start', 0, 0.75), legAt({d}, 'x', 0, 0),"
|
||||
f" setLeg(setLeg(setLeg({d}, 'g', 0, {{road: '1차포장', km: 23.8}}),"
|
||||
f" 'base', 0, {{km: null}}), 'start', 0, {{road: '2차포장', km: 1.2345}})]"
|
||||
f" 'b1', 0, {{km: null}}), 'start', 0, {{road: '2차포장', km: 1.2345}})]"
|
||||
)
|
||||
assert got[0] == {"road": "2차포장", "km": None}
|
||||
assert got[1] == {"road": "비포장", "km": 0.75} and got[2] is None
|
||||
after = got[3]
|
||||
assert after["routes"][0]["legs"] == [{"road": "1차포장", "km": 23.8}]
|
||||
assert after["base"]["legs"] == [{"road": "1차포장", "km": None}]
|
||||
assert after["bases"][0]["legs"] == [{"road": "1차포장", "km": None}]
|
||||
assert after["site"] == {"mid_station_m": 1234.5, "mid_road": "2차포장"}
|
||||
# 표 셈 — 포장 종류 = 선 자리(152 · 기준점 거침 거래처 = 2차포장 · 저장 값 1차포장 이어도)
|
||||
(row,) = compute_milestone(after, 1500.0)
|
||||
|
||||
@@ -5,9 +5,15 @@
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B08_Quantity.B08_Quantity_Milestone_Calc import migrate_milestone # noqa: E402
|
||||
|
||||
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
|
||||
|
||||
|
||||
@@ -17,6 +23,7 @@ def _read(rel):
|
||||
|
||||
def _layout(base, site):
|
||||
data = {"site": {"mid_station_m": None, "mid_road": "비포장"}, "base": base, "routes": []}
|
||||
data = migrate_milestone(data) # 254 옛 꼴 → bases(옛 기준점 = b1)
|
||||
src = IMPORTS.sub("", _read("B08_Quantity/B08_Quantity_Milestone_Layout.ts"))
|
||||
expr = f"layoutMilestone({json.dumps(data)}, {json.dumps(site)}, 0)"
|
||||
result = subprocess.run(
|
||||
@@ -27,7 +34,7 @@ def _layout(base, site):
|
||||
assert result.returncode == 0, result.stderr
|
||||
lay = json.loads(result.stdout)
|
||||
center = {b["id"]: (b["x"] + b["w"] / 2, b["y"] + b["h"] / 2) for b in lay["boxes"]}
|
||||
return center["site"], center["base"]
|
||||
return center["site"], center["b1"]
|
||||
|
||||
|
||||
BASE = {"enabled": True, "name": "기준", "address": "", "legs": []}
|
||||
@@ -60,6 +67,5 @@ def test_좌측_패널_방위_칸_없음_모달_안내():
|
||||
]
|
||||
assert "방위는 이 자리로 자동" in base # 244-13 — 기준점 = 등록 모달 분류 기준위치
|
||||
layout = _read("B08_Quantity/B08_Quantity_Milestone_Layout.ts")
|
||||
assert (
|
||||
"placeDeg({ dir: null, lon: data.base.lon, lat: data.base.lat }, startLL) ?? 270" in layout
|
||||
)
|
||||
assert "placeDeg({ dir: null, lon: b.lon, lat: b.lat }, anchorLL) ??" in layout
|
||||
assert '(anchor === "site" && !used.has(keyOf("site", 270)) ? 270 : free(anchor))' in layout
|
||||
|
||||
@@ -82,13 +82,13 @@ def test_실제_포인터_사건으로_팬():
|
||||
def test_거래처_상자_항목_글():
|
||||
data = {
|
||||
"site": {"mid_station_m": None, "mid_road": "비포장"},
|
||||
"base": {"enabled": False, "name": "", "address": "", "dir": None, "legs": []},
|
||||
"bases": [],
|
||||
"routes": [
|
||||
{"id": "g", "name": "거창군청", "address": "", "via_base": True, "dir": None,
|
||||
{"id": "g", "name": "거창군청", "address": "", "to": "site", "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,
|
||||
{"id": "k", "name": "", "address": "", "to": "site", "dir": None,
|
||||
"legs": [], "rows": [{"item": "", "dest": "start", "note": ""}]},
|
||||
],
|
||||
} # fmt: skip
|
||||
@@ -118,12 +118,12 @@ def test_표_폭_다이어그램_폭_좌측_카드_없음():
|
||||
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
|
||||
# 244-13 — 기준점 = 등록 모달 분류 「기준위치」 · [기준점 +] 없앰 · 삭제 = 기준점 없음
|
||||
# 244-13 — 기준점 = 등록 모달 분류 「기준위치」 · [기준점 +] 없앰 · 254 여럿 · 삭제 = 그 기준점만
|
||||
base = modal[
|
||||
modal.index("export function openPlaceModal") : modal.index("interface SiteAddress")
|
||||
]
|
||||
assert "enabled: true," in base and 'label: "삭제"' in base
|
||||
assert "fresh.base = { enabled: false," in base
|
||||
assert "fresh.bases.push({" in base and 'label: "삭제"' in base
|
||||
assert "fresh.bases = fresh.bases.filter((b) => b.id !== base.id);" in base
|
||||
assert 'label: "기준점 +"' not in ui and "openBaseInfo" not in ui and "addBase" not in ui
|
||||
assert "content: panContent(vw, vh)" in ui and "panStart," in ui
|
||||
|
||||
|
||||
@@ -24,7 +24,10 @@ if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B08_Quantity import B08_Quantity_Router_Milestone as ms # noqa: E402
|
||||
from B08_Quantity.B08_Quantity_Milestone_Calc import compute_milestone # noqa: E402
|
||||
from B08_Quantity.B08_Quantity_Milestone_Calc import ( # noqa: E402
|
||||
compute_milestone,
|
||||
migrate_milestone,
|
||||
)
|
||||
|
||||
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
|
||||
FAKE_KEY = "fake-kakao-key-0123456789"
|
||||
@@ -144,7 +147,8 @@ def test_키_값_응답_로그에_안_나옴(monkeypatch, caplog):
|
||||
|
||||
|
||||
def _data(base_enabled=True, base_ll=(127.85, 35.66)):
|
||||
return {
|
||||
"""254 옛 꼴로 적고 새 꼴(bases · to · 옛 기준점 = b1)로 옮겨 씀."""
|
||||
return migrate_milestone({
|
||||
"site": {"mid_station_m": None, "mid_road": "비포장"},
|
||||
"base": {"enabled": base_enabled, "name": "병곡리 968-31", "address": "", "dir": None,
|
||||
"lon": base_ll[0], "lat": base_ll[1], "legs": []},
|
||||
@@ -156,7 +160,7 @@ def _data(base_enabled=True, base_ll=(127.85, 35.66)):
|
||||
"lon": None, "lat": None, "legs": [{"road": None, "km": None}],
|
||||
"rows": [{"item": "철근", "dest": "start", "note": ""}]},
|
||||
],
|
||||
} # fmt: skip
|
||||
}) # fmt: skip
|
||||
|
||||
|
||||
LAYOUT = "B08_Quantity/B08_Quantity_Milestone_Layout.ts"
|
||||
@@ -179,27 +183,27 @@ def test_구간_채움():
|
||||
two = _node(LAYOUT, f"kmSegments({d}, 'r2', null)")
|
||||
assert two == [{"key": "route", "text": "김천역 → 시점", "from": None, "to": None}]
|
||||
# 자동 km 넣기 — 빈 구간 = 안 고른 줄 하나 · 사용자 km 는 그대로
|
||||
put = _node(LAYOUT, f"putAutoKm(putAutoKm({d}, 'r1', 'route', 10.5), 'r1', 'base', 1.4)")
|
||||
put = _node(LAYOUT, f"putAutoKm(putAutoKm({d}, 'r1', 10.5), 'b1', 1.4)")
|
||||
assert put["routes"][0]["legs"] == [{"road": "2차포장", "km": 23.8, "auto_km": 10.5}]
|
||||
assert put["base"]["legs"] == [{"road": None, "km": None, "auto_km": 1.4}]
|
||||
assert put["bases"][0]["legs"] == [{"road": None, "km": None, "auto_km": 1.4}]
|
||||
# 선 글 = 사용자 값 우선 · 다르면 호버 · 안 고름 = 「도로 구분 고르기」 + 자동 값
|
||||
lay = _node(LAYOUT, f"layoutMilestone({json.dumps(put)}, {{start_lonlat: {START}}}, 0.75)")
|
||||
line = {ln["from"]: ln for ln in lay["lines"]}
|
||||
assert line["r1"]["labels"] == ["2차포장 23.8km"] and line["r1"]["tips"] == ["자동 값 10.5 km"]
|
||||
assert line["base"]["labels"] == ["도로 구분 고르기 1.4km"] and line["base"]["tips"] == [""]
|
||||
assert line["b1"]["labels"] == ["도로 구분 고르기 1.4km"] and line["b1"]["tips"] == [""]
|
||||
assert line["start"]["labels"] == ["비포장 0.75km"] # 시점 → 중점 = 노선 측점 거리 그대로
|
||||
|
||||
|
||||
def test_사용자_값_우선_고르기_표시():
|
||||
data = _data()
|
||||
data["routes"][0]["legs"] = [{"road": "2차포장", "km": None, "auto_km": 10.5}]
|
||||
data["base"]["legs"] = [{"road": "1차포장", "km": 0.6, "auto_km": 1.4}]
|
||||
data["bases"][0]["legs"] = [{"road": "1차포장", "km": 0.6, "auto_km": 1.4}]
|
||||
a, b = compute_milestone(data, 1500.0)
|
||||
# 거래처 = 자동 10.5 · 기준점 = 사용자 0.6(자동 1.4 무시) · 중점 1.5 비포장
|
||||
assert (a["s2"], a["s1"], a["un"], a["total"], a["pick"]) == (10.5, 0.6, 1.5, 12.6, False)
|
||||
# 152 도로 구분 안 고름 = 선 자리 포장 종류로 셈(pick 늘 거짓) — 빈 km = 0
|
||||
assert (b["total"], b["pick"]) == (0, False)
|
||||
data["base"]["legs"][0]["road"] = None # 기준점 → 시점 = 1차포장(자리)
|
||||
data["bases"][0]["legs"][0]["road"] = None # 기준점 → 시점 = 1차포장(자리)
|
||||
a2, _ = compute_milestone(data, 1500.0)
|
||||
assert (a2["s1"], a2["total"], a2["pick"]) == (0.6, 12.6, False)
|
||||
# TS 짝 거울
|
||||
@@ -210,7 +214,7 @@ def test_사용자_값_우선_고르기_표시():
|
||||
# 저장 꼴 — auto_km · road null 받음 · 빠진 road = 옛 기본 2차포장
|
||||
saved = ms.Milestone.model_validate(data).model_dump()
|
||||
assert saved["routes"][0]["legs"][0]["auto_km"] == 10.5
|
||||
assert saved["base"]["legs"][0]["road"] is None
|
||||
assert saved["bases"][0]["legs"][0]["road"] is None
|
||||
assert ms.Leg.model_validate({"km": 1}).road == "2차포장"
|
||||
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B08_Quantity import B08_Quantity_Router_Milestone as ms # noqa: E402
|
||||
from B08_Quantity.B08_Quantity_Milestone_Calc import migrate_milestone # noqa: E402
|
||||
|
||||
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
|
||||
BASE = "/api/projects/00000000-0000-0000-0000-000000000001/quantity/milestone"
|
||||
@@ -118,14 +119,14 @@ def test_표지_셋():
|
||||
"routes": [{"id": "r1", "name": "거창군청", "lon": 127.9, "lat": 35.68, "via_base": True},
|
||||
{"id": "r2", "name": "", "lon": None, "lat": None, "via_base": False}],
|
||||
} # fmt: skip
|
||||
d = json.dumps(data)
|
||||
d = json.dumps(migrate_milestone(data)) # 254 옛 꼴 → bases · to(옛 기준점 = b1)
|
||||
got = _node(
|
||||
[LAYOUT],
|
||||
f"""const D = {d};
|
||||
console.log(JSON.stringify([
|
||||
mapMarks(D, "r1", [127.83, 35.64]),
|
||||
mapMarks(D, "r2", [127.83, 35.64]),
|
||||
mapMarks({{ ...D, base: {{ ...D.base, enabled: false }} }}, "r1", null),
|
||||
mapMarks({{ ...D, bases: [] }}, "r1", null),
|
||||
]));""",
|
||||
)
|
||||
assert got[0] == [
|
||||
|
||||
@@ -9,7 +9,11 @@ from pathlib import Path
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B08_Quantity.B08_Quantity_Milestone_Calc import compute_milestone, road_of # noqa: E402
|
||||
from B08_Quantity.B08_Quantity_Milestone_Calc import ( # noqa: E402
|
||||
compute_milestone,
|
||||
migrate_milestone,
|
||||
road_of,
|
||||
)
|
||||
|
||||
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
|
||||
|
||||
@@ -87,7 +91,9 @@ def test_153_지도_보기_유지_상자_색_끌어_옮김():
|
||||
assert var in block[: block.index("}")], kind
|
||||
ui = _read("B08_Quantity/B08_Quantity_UI_Milestone_Diagram.ts")
|
||||
assert 'if (box.kind === "route" || box.kind === "base") dragBox(g, box, shift);' in ui
|
||||
assert 'label: "방위 배치"' in ui and "fresh.routes.forEach((r) => (r.pos = null));" in ui # 159 이름
|
||||
assert (
|
||||
'label: "방위 배치"' in ui and "fresh.routes.forEach((r) => (r.pos = null));" in ui
|
||||
) # 159 이름
|
||||
|
||||
|
||||
def test_153_끌어_놓은_자리_레이아웃():
|
||||
@@ -96,23 +102,24 @@ def test_153_끌어_놓은_자리_레이아웃():
|
||||
"base": {"enabled": True, "name": "기준", "address": "", "dir": "W", "legs": [], "pos": [500, -40]},
|
||||
"routes": [{"id": "r", "name": "a", "address": "", "via_base": False, "dir": "N", "legs": [],
|
||||
"rows": [], "pos": [-700, 300]}]} # fmt: skip
|
||||
expr = f"layoutMilestone({json.dumps(data)}, {{}}, 0.5).boxes.filter((b) => b.id === 'base' || b.id === 'r').map((b) => [b.id, b.x, b.y])"
|
||||
data = migrate_milestone(data) # 254 옛 꼴 → bases(옛 기준점 = b1)
|
||||
expr = f"layoutMilestone({json.dumps(data)}, {{}}, 0.5).boxes.filter((b) => b.id === 'b1' || b.id === 'r').map((b) => [b.id, b.x, b.y])"
|
||||
out = subprocess.run(
|
||||
["node", "--experimental-strip-types", "--no-warnings", "--input-type=module-typescript",
|
||||
"-e", src + f"\nconsole.log(JSON.stringify({expr}));"],
|
||||
capture_output=True, text=True, encoding="utf-8", timeout=60,
|
||||
) # fmt: skip
|
||||
assert out.returncode == 0, out.stderr
|
||||
assert sorted(json.loads(out.stdout)) == [["base", 500, -40], ["r", -700, 300]]
|
||||
assert sorted(json.loads(out.stdout)) == [["b1", 500, -40], ["r", -700, 300]]
|
||||
|
||||
|
||||
def test_154_연결_점_끌어_잇기():
|
||||
ui = _read("B08_Quantity/B08_Quantity_UI_Milestone_Diagram.ts")
|
||||
assert 'if (box.kind === "route") g.append(connectDot(box, shift));' in ui
|
||||
assert 'const via = t === "base";' in ui and "route.via_base = via;" in ui
|
||||
assert "route.to = t;" in ui # 254 놓은 기준점 id · 공사지 = "site"
|
||||
assert "route.legs = [{ road: null, km: null }];" in ui # 옛 구간 km 비움
|
||||
assert "void findSegmentKm(projectId, route.id).then(toastKm);" in ui # 새 길찾기 한 번
|
||||
assert "if (via === now) return;" in ui # 같은 연결 = 그대로
|
||||
assert "if (route.linked !== false && route.to === t) return;" in ui # 같은 연결 = 그대로
|
||||
modal = _read("B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts")
|
||||
assert (
|
||||
'createCheckbox("기준점 거침")' not in modal and "via_base: via.input.checked" not in modal
|
||||
@@ -145,7 +152,7 @@ def test_155_요소_추가_안_이은_거래처_수기_입력():
|
||||
state = _read("B08_Quantity/B08_Quantity_UI_Milestone_State.ts")
|
||||
assert "if (owner?.manual) return" in state # 수기 입력 = 카카오 길찾기 안 함
|
||||
layout = _read("B08_Quantity/B08_Quantity_Milestone_Layout.ts")
|
||||
assert "if (!route || route.linked === false) return null;" in layout
|
||||
assert "if (!owner || owner.linked === false) return null;" in layout # 254 거래처 · 기준점
|
||||
|
||||
|
||||
def test_157_도구_왼쪽_아래_경로_탭_지도_그림_변경_초기화():
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
"""254-1 B08 거리이정표 여러 기준점(사용자 162-10) — `bases[]` · 경로 `to` · 옛 꼴 옮김 ·
|
||||
기준점 사슬 셈(파이썬 · TS 거울) · 돌기 막음 ·
|
||||
도로 구분(시점에 닿는 기준점 구간 = 1차 · 그 밖 = 2차).
|
||||
|
||||
견본 사슬 = 거창군청 → 기준점 A(병곡리) → 기준점 B(신원) → 시점 · 김천역 → B → 시점 ·
|
||||
철근 상회 → 시점(직접).
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B08_Quantity import B08_Quantity_Router_Milestone as ms # noqa: E402
|
||||
from B08_Quantity.B08_Quantity_Haul import with_haul_routes # noqa: E402
|
||||
from B08_Quantity.B08_Quantity_Milestone_Calc import ( # noqa: E402
|
||||
base_chain,
|
||||
compute_milestone,
|
||||
migrate_milestone,
|
||||
)
|
||||
|
||||
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
|
||||
B08 = "B08_Quantity/B08_Quantity_"
|
||||
|
||||
|
||||
def _read(rel):
|
||||
return (ROOT / rel).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _node(files, expr):
|
||||
src = "\n".join(IMPORTS.sub("", _read(f)) for f in files)
|
||||
result = subprocess.run(
|
||||
["node", "--experimental-strip-types", "--no-warnings",
|
||||
"--input-type=module-typescript", "-e", src + f"\nconsole.log(JSON.stringify({expr}));"],
|
||||
capture_output=True, text=True, encoding="utf-8", timeout=60,
|
||||
) # fmt: skip
|
||||
assert result.returncode == 0, result.stderr
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def _leg(road, km):
|
||||
return {"road": road, "km": km}
|
||||
|
||||
|
||||
def _base(bid, to, legs, linked=True):
|
||||
return {"id": bid, "name": bid, "address": "", "dir": None, "legs": legs, "to": to,
|
||||
"linked": linked} # fmt: skip
|
||||
|
||||
|
||||
def _route(rid, to, legs, dest="start"):
|
||||
return {"id": rid, "name": rid, "address": "", "dir": None, "to": to, "legs": legs,
|
||||
"rows": [{"item": "장비", "dest": dest, "note": ""}]} # fmt: skip
|
||||
|
||||
|
||||
CHAIN = {
|
||||
"site": {"mid_station_m": 800, "mid_road": "비포장"},
|
||||
"bases": [
|
||||
_base("A", "B", [_leg(None, 4.5), _leg("비포장", 0.3)]),
|
||||
_base("B", "site", [_leg(None, 0.6)]),
|
||||
],
|
||||
"routes": [
|
||||
_route("g", "A", [_leg(None, 23.8)]),
|
||||
_route("k", "B", [_leg(None, 61.2)], dest="mid"),
|
||||
_route("c", "site", [_leg(None, 2.0)]),
|
||||
],
|
||||
}
|
||||
|
||||
OLD = {
|
||||
"site": {"mid_station_m": None, "mid_road": "비포장"},
|
||||
"base": {"enabled": True, "name": "병곡리", "address": "주소", "dir": "E", "lon": 127.85,
|
||||
"lat": 35.66, "pos": [10, 20], "legs": [_leg("1차포장", 0.6)]},
|
||||
"routes": [
|
||||
{"id": "r1", "name": "거창군청", "address": "", "via_base": True, "dir": None,
|
||||
"legs": [_leg("2차포장", 23.8)], "rows": [{"item": "장비", "dest": "start", "note": ""}]},
|
||||
{"id": "r2", "name": "김천역", "address": "", "via_base": False, "dir": None,
|
||||
"legs": [_leg(None, 5)], "rows": [{"item": "철근", "dest": "start", "note": ""}]},
|
||||
],
|
||||
} # fmt: skip
|
||||
|
||||
|
||||
def test_옛_꼴_옮김_거울():
|
||||
new = migrate_milestone(OLD)
|
||||
assert "base" not in new and [b["id"] for b in new["bases"]] == ["b1"]
|
||||
b1 = new["bases"][0]
|
||||
assert (b1["to"], b1["linked"], b1["name"], b1["pos"]) == ("site", True, "병곡리", [10, 20])
|
||||
assert b1["legs"] == [_leg("1차포장", 0.6)] and b1["lon"] == 127.85
|
||||
assert [(r["id"], r["to"], "via_base" in r) for r in new["routes"]] == [
|
||||
("r1", "b1", False),
|
||||
("r2", "site", False),
|
||||
]
|
||||
# 꺼진 기준점 = bases 없음 · 거침 경로도 시점으로
|
||||
off = migrate_milestone({**OLD, "base": {**OLD["base"], "enabled": False}})
|
||||
assert off["bases"] == [] and [r["to"] for r in off["routes"]] == ["site", "site"]
|
||||
assert migrate_milestone(new) == new # 새 꼴 = 그대로
|
||||
# TS 짝 — 옮긴 꼴 · 표 합이 같음
|
||||
calc = [B08 + "Milestone_Calc.ts"]
|
||||
ts = _node(calc, f"migrateMilestone({json.dumps(OLD)})")
|
||||
assert ts["bases"][0]["id"] == "b1" and [r["to"] for r in ts["routes"]] == ["b1", "site"]
|
||||
assert ts["bases"][0]["legs"] == b1["legs"] and ts["bases"][0]["pos"] == [10, 20]
|
||||
assert _node(calc, f"computeMilestone({json.dumps(OLD)}, 1500)") == compute_milestone(
|
||||
OLD, 1500.0
|
||||
)
|
||||
# 옛 표 합 그대로 — 2차 23.8 + 1차 0.6
|
||||
(a, b) = compute_milestone(OLD, 1500.0)
|
||||
assert (a["s2"], a["s1"], a["total"]) == (23.8, 0.6, 24.4) and (b["pv"], b["total"]) == (5, 5)
|
||||
|
||||
|
||||
def test_서버_읽기_받기_새_꼴(tmp_path, monkeypatch):
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
# 옛 파일 읽기 = 새 꼴(지우지 않음 · 저장 때 새 꼴)
|
||||
folder = tmp_path / "B08_Quantity" / "milestone"
|
||||
folder.mkdir(parents=True)
|
||||
(folder / "milestone.json").write_text(json.dumps(OLD, ensure_ascii=False), "utf-8")
|
||||
data = ms.read_data(tmp_path)
|
||||
assert [b["id"] for b in data["bases"]] == ["b1"] and data["routes"][0]["to"] == "b1"
|
||||
assert "base" not in data and "via_base" not in data["routes"][0]
|
||||
|
||||
async def root(_pid):
|
||||
return tmp_path
|
||||
|
||||
monkeypatch.setattr(ms, "_project_root", root)
|
||||
app = FastAPI()
|
||||
app.include_router(ms.router)
|
||||
client = TestClient(app)
|
||||
url = f"/api/projects/{uuid4()}/quantity/milestone"
|
||||
saved = client.put(url, json=OLD).json() # 옛 꼴 받기 = 새 꼴로 저장
|
||||
stored = json.loads((folder / "milestone.json").read_text("utf-8"))
|
||||
assert "base" not in stored and stored["bases"][0]["id"] == "b1"
|
||||
assert stored["routes"][0]["to"] == "b1" and "via_base" not in stored["routes"][0]
|
||||
assert saved["table"] == compute_milestone(OLD, None)
|
||||
# 새 꼴 사슬 왕복 · 겹친 id · 공사지 상자 id = 거절
|
||||
assert client.put(url, json=CHAIN).json()["data"]["bases"][0]["to"] == "B"
|
||||
twice = {**CHAIN, "bases": [*CHAIN["bases"], _base("g", "site", [])]}
|
||||
assert client.put(url, json=twice).status_code == 422
|
||||
box = {**CHAIN, "bases": [_base("site", "site", [])]}
|
||||
assert client.put(url, json=box).status_code == 422
|
||||
# 운반 자동 상자 = 시점으로(안 이음)
|
||||
(auto,) = with_haul_routes({"routes": []}, [{"key": "1", "name": "골재"}])["routes"]
|
||||
assert auto["to"] == "site" and auto["linked"] is False and "via_base" not in auto
|
||||
|
||||
|
||||
def test_기준점_둘_사슬_셈_거울():
|
||||
g, k, c = compute_milestone(CHAIN, None)
|
||||
# 거래처 → A = 2차 · A → B(기준점 → 기준점) = 2차 · B → 시점 = 1차 · 비포장은 사용자 값
|
||||
assert (g["s2"], g["s1"], g["pv"], g["un"]) == (28.3, 0.6, 0, 0.3)
|
||||
assert (g["paved"], g["unpaved"], g["total"]) == (28.9, 0.3, 29.2)
|
||||
# 김천역 → B = 2차 · B → 시점 = 1차 · 목적지 중점 = 시점 → 중점 0.8 비포장
|
||||
assert (k["s2"], k["s1"], k["un"], k["total"]) == (61.2, 0.6, 0.8, 62.6)
|
||||
# 시점으로 바로 = 포장
|
||||
assert (c["s2"], c["s1"], c["pv"], c["total"]) == (0, 0, 2.0, 2.0)
|
||||
calc = [B08 + "Milestone_Calc.ts"]
|
||||
assert _node(calc, f"computeMilestone({json.dumps(CHAIN)}, null)") == [g, k, c]
|
||||
# 안 이은 기준점에서 사슬 멈춤(그 구간 · 뒤 구간 안 셈)
|
||||
cut = {**CHAIN, "bases": [CHAIN["bases"][0], {**CHAIN["bases"][1], "linked": False}]}
|
||||
g2, k2, _ = compute_milestone(cut, None)
|
||||
assert (g2["s2"], g2["s1"], g2["un"], g2["total"]) == (28.3, 0, 0.3, 28.6)
|
||||
assert (k2["s2"], k2["total"]) == (61.2, 62.0)
|
||||
assert _node(calc, f"computeMilestone({json.dumps(cut)}, null)") == compute_milestone(cut, None)
|
||||
# 선 자리 — 시점에 닿는 기준점 = base(1차) · 기준점 → 기준점 · 거래처 → 기준점 = via(2차)
|
||||
kinds = _node(
|
||||
calc, f"['g', 'A', 'B', 'c', 'start'].map((f) => segKind({json.dumps(CHAIN)}, f))"
|
||||
)
|
||||
assert kinds == ["via", "via", "base", "direct", "mid"]
|
||||
|
||||
|
||||
def test_돌기_막음():
|
||||
loop = {**CHAIN, "bases": [_base("A", "B", [_leg(None, 1)]), _base("B", "A", [_leg(None, 2)])]}
|
||||
# 셈 = 방문 집합으로 끊음(A → B → A 멈춤 · 끝없이 안 돎)
|
||||
assert [b["id"] for b in base_chain(loop, "A")] == ["A", "B"]
|
||||
(g, *_) = compute_milestone(loop, None)
|
||||
assert g["total"] == 26.8
|
||||
calc = [B08 + "Milestone_Calc.ts"]
|
||||
assert _node(calc, f"computeMilestone({json.dumps(loop)}, null)") == compute_milestone(
|
||||
loop, None
|
||||
)
|
||||
self_loop = {**CHAIN, "bases": [_base("A", "A", [_leg(None, 1)])]}
|
||||
assert [b["id"] for b in base_chain(self_loop, "A")] == ["A"]
|
||||
# 연결 놓을 때 = 돌기면 취소(makesLoop) — A → B 사슬에서 B 를 A 에 이으면 돎
|
||||
layout = [B08 + "Milestone_Layout.ts"]
|
||||
d = json.dumps(CHAIN)
|
||||
got = _node(
|
||||
layout,
|
||||
f"[makesLoop({d}, 'B', 'A'), makesLoop({d}, 'A', 'B'), makesLoop({d}, 'B', 'site'),"
|
||||
f" makesLoop({d}, 'A', 'A'), chainOf({d}, 'A').map((b) => b.id),"
|
||||
f" kmSegments({d}, 'g', [127.8, 35.6]).map((s) => s.text)]",
|
||||
)
|
||||
assert got[:4] == [True, False, False, True]
|
||||
assert got[4] == ["A", "B"] and got[5] == ["g → A", "A → B", "B → 시점"]
|
||||
|
||||
|
||||
def test_화면_기준점_여럿_등록_삭제():
|
||||
modal = _read(B08 + "UI_Milestone_Modal.ts")
|
||||
assert "fresh.bases.push({" in modal and 'to: "site",' in modal
|
||||
assert "기준위치(있음" not in modal # 254 기준점 여럿 — 고르기 막음 없음
|
||||
# 지운 기준점에 이은 상자 = 안 이음
|
||||
assert "if (o.to === base.id)" in modal
|
||||
state = _read(B08 + "UI_Milestone_State.ts")
|
||||
assert "migrateMilestone(" in state and "bases: []," in state
|
||||
page = _read(B08 + "UI_Milestone.ts")
|
||||
assert "data.bases.length > 0" in page and 'to: "site",' in page
|
||||
for name in ("Milestone_Calc.ts", "Milestone_Layout.ts", "UI_Milestone_Diagram.ts",
|
||||
"UI_Milestone_Modal.ts", "UI_Milestone_State.ts", "UI_Milestone_LegModal.ts",
|
||||
"UI_Milestone_Route.ts", "UI_Milestone.ts"): # fmt: skip
|
||||
text = _read(B08 + name)
|
||||
assert "data.base." not in text and ".via_base" not in text and ".base.legs" not in text
|
||||
@@ -144,3 +144,17 @@ def test_화면_캡처_길_그림_파일_길(tmp_path, monkeypatch):
|
||||
assert client.get(base + "/file", params={"name": "place/zz.png"}).status_code == 404
|
||||
for name in ("../milestone.json", "place/../x.png", "x/r1.png", "place/r1.json", "/etc/x.png"):
|
||||
assert client.get(base + "/file", params={"name": name}).status_code == 422, name
|
||||
|
||||
|
||||
def test_여러_기준점_상자_id(tmp_path, monkeypatch):
|
||||
fake = _fake(monkeypatch)
|
||||
bases = [
|
||||
{"id": "b1", "name": "거창IC", "lon": 127.91, "lat": 35.68, "to": "b2"},
|
||||
{"id": "b2", "name": "함양JC", "lon": 127.72, "lat": 35.52},
|
||||
{"id": "b3", "name": "좌표 없음"},
|
||||
]
|
||||
_save(tmp_path, {"bases": bases, "routes": [{**DATA["routes"][0], "to": "b1"}]})
|
||||
ms.refresh_images(tmp_path)
|
||||
assert sorted(c[0] for c in fake.calls) == ["b1", "b2", "r1"]
|
||||
got = _save(tmp_path, {"bases": bases[1:], "routes": []}) # b1 · r1 지움
|
||||
assert got["place_images"] == {"b2": "place/b2.png"}
|
||||
|
||||
Reference in New Issue
Block a user