Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LLCk2y5xE8YGVgjbTntWsY
191 lines
7.7 KiB
TypeScript
191 lines
7.7 KiB
TypeScript
/* =============================================================================
|
||
* B08_Quantity_Milestone_Calc.ts
|
||
* B08 거리이정표 표 합(PLAN 244-1) — 화면 없음 · 저장 안 함 · import 없음(시험이 node 로 돌림).
|
||
*
|
||
* ⚠ 짝 = `B08_Quantity_Milestone_Calc.py`(같은 식) — 한쪽을 고치면 다른 쪽도 ·
|
||
* 거울 시험 `resources/tester/test_244_1_b08_milestone.py`.
|
||
* 조작 중은 이 파일이 바로 셈 · [저장] · 읽기 때 서버가 파이썬 짝으로 다시 셈.
|
||
*
|
||
* 표 한 줄 = 경로(업체) 구간 + (기준점 거침이면 기준점 → 시점 공통 구간) + (목적지 = 중점이면
|
||
* 시점 → 중점 = 중점 측점 km) · 도로 구분 셋마다 합 · 포장 = 2차 + 1차 · 합계 = 포장 + 비포장 ·
|
||
* 끝수 = 소수 셋째 자리(반올림)
|
||
* 구간 km = 사용자 값 · 없으면 [길찾기] 자동 값(244-7)
|
||
* 도로 구분(152) = 포장 종류는 선 자리로 정함(`roadOf`) — 거래처 → 기준점 = 2차포장 · 기준점 → 시점 =
|
||
* 1차포장 · 기준점 없이 거래처 → 시점 · 시점 → 중점 = 포장 · 사용자는 구간마다 비포장만 고름 ·
|
||
* 포장 = 2차 + 1차 + 포장 · pick = 늘 거짓(옛 「도로 구분 고르기」 자리)
|
||
* ========================================================================== */
|
||
|
||
export const ROADS = ["2차포장", "1차포장", "포장", "비포장"] as const;
|
||
export type Road = (typeof ROADS)[number];
|
||
/** 구간 자리(152) — 거래처 → 기준점(via) · 기준점 → 시점(base) · 거래처 → 시점(direct) · 시점 → 중점(mid) */
|
||
export type SegKind = "via" | "base" | "direct" | "mid";
|
||
/** 포장 종류 = 선 자리로 정함(152) — 기준점이 생기면 거래처 → 기준점 = 2차 · 기준점 → 시점 = 1차 · 없으면 포장 */
|
||
export const PAVED: Record<SegKind, Road> = {
|
||
via: "2차포장",
|
||
base: "1차포장",
|
||
direct: "포장",
|
||
mid: "포장",
|
||
};
|
||
/** 그 구간 도로 구분 — 사용자는 비포장만 고름 · 그 밖(빈 값 · 옛 값) = 자리의 포장 종류 */
|
||
export const roadOf = (road: Road | null | undefined, kind: SegKind): Road =>
|
||
road === "비포장" ? "비포장" : PAVED[kind];
|
||
/** 도로 구분을 선 자리 포장 종류로 바꾼 사본(152) — 다이어그램 선 글 · 그림용(저장 값은 그대로) */
|
||
export function withRoads(data: MilestoneData): MilestoneData {
|
||
const fix = (legs: Leg[], kind: SegKind): Leg[] =>
|
||
legs.map((leg) => ({ ...leg, road: roadOf(leg.road, kind) }));
|
||
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"),
|
||
})),
|
||
};
|
||
}
|
||
|
||
/** 선 자리 — from = 거래처 id · "base" · "start"(시점 → 중점) */
|
||
export function segKind(data: MilestoneData, from: string): SegKind {
|
||
if (from === "start") return "mid";
|
||
if (from === "base") return "base";
|
||
const route = data.routes.find((r) => r.id === from);
|
||
return data.base.enabled && route?.via_base !== false ? "via" : "direct";
|
||
}
|
||
/** 8방 — 기준점(없으면 공사지) 기준 지도 방위 */
|
||
export const DIRS = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"] as const;
|
||
export type Dir = (typeof DIRS)[number];
|
||
export type Dest = "start" | "mid";
|
||
|
||
export interface Leg {
|
||
/** null = 사용자가 안 고름(자동 판정 없음 · 244-7) */
|
||
road: Road | null;
|
||
/** 사용자 값 — 있으면 우선 */
|
||
km: number | null;
|
||
/** [길찾기] 자동 값(km 만 저장 · 경로 좌표 · 응답은 저장 안 함) */
|
||
auto_km?: number | null;
|
||
}
|
||
|
||
export interface MilestoneRow {
|
||
item: string;
|
||
dest: Dest;
|
||
note: string;
|
||
}
|
||
|
||
export interface MilestoneRoute {
|
||
id: string;
|
||
/** 업체 이름 = 하위 탭 이름 */
|
||
name: string;
|
||
address: string;
|
||
/** 기준점 거침(기준점 없으면 무시 · 시점으로 바로) */
|
||
via_base: boolean;
|
||
dir: Dir | null;
|
||
lon?: number | null;
|
||
lat?: number | null;
|
||
/** 다이어그램에 끌어 놓은 자리(153 · 왼 위 · 그림 좌표) — 없으면 방위 자동 자리 */
|
||
pos?: [number, number] | null;
|
||
/** 선 이음(155) — false = 아직 안 이음(요소 추가 뒤 · 연결 점으로 이음 · 구간 km 안 셈) · 없음 = 이음(옛 자료) */
|
||
linked?: boolean;
|
||
/** 수기 입력(155) — 카카오 길찾기 안 함 · km 손 입력만 */
|
||
manual?: boolean;
|
||
/** 운반 묶음 키(251 · 총괄자재 · B09 운반 줄과 이음) — 없음 = 사용자 [요소 추가] 상자 */
|
||
haul?: string | null;
|
||
/** 단가연결(251 · dg · sg · logic 꼴 · 정본) */
|
||
ref?: Record<string, unknown> | null;
|
||
/** 업체 → 기준점(거침) · 시점 구간 */
|
||
legs: Leg[];
|
||
rows: MilestoneRow[];
|
||
}
|
||
|
||
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[];
|
||
};
|
||
routes: MilestoneRoute[];
|
||
}
|
||
|
||
export interface MilestoneTableRow {
|
||
route: string;
|
||
index: number;
|
||
item: string;
|
||
company: string;
|
||
dest: Dest;
|
||
s2: number;
|
||
s1: number;
|
||
pv: number;
|
||
un: number;
|
||
paved: number;
|
||
unpaved: number;
|
||
total: number;
|
||
note: string;
|
||
/** 도로 구분 안 고른 구간이 있음 */
|
||
pick: boolean;
|
||
}
|
||
|
||
type Key = "s2" | "s1" | "pv" | "un";
|
||
const KEYS: Record<string, Key> = { "2차포장": "s2", "1차포장": "s1", 포장: "pv", 비포장: "un" };
|
||
|
||
/** 소수 셋째 자리 반올림(파이썬 짝 floor(x × 1000 + 0.5) 과 같음) */
|
||
export const r3 = (value: number): number => Math.floor(value * 1000 + 0.5) / 1000;
|
||
|
||
/** 구간 km — 사용자 값 우선 · 없으면 자동 값 */
|
||
export const legKm = (leg: Leg): number | null => leg.km ?? leg.auto_km ?? null;
|
||
|
||
/** 도로 구분마다 더함 — 구간 자리로 포장 종류(152 · 안 고른 구간 없음) */
|
||
function addLegs(sums: Record<Key, number>, legs: Leg[] | undefined, kind: SegKind): void {
|
||
for (const leg of legs ?? []) sums[KEYS[roadOf(leg.road, kind)]] += Number(legKm(leg) ?? 0);
|
||
}
|
||
|
||
/** 시점 → 중점 km — 사용자 측점(없으면 기본 = 노선 ÷ 2) ÷ 1000 */
|
||
export function midKm(data: MilestoneData, midDefaultM: number | null): number {
|
||
const station = data.site?.mid_station_m ?? midDefaultM;
|
||
return Number(station ?? 0) / 1000;
|
||
}
|
||
|
||
/** 표 줄 — 경로 차례 · 경로 안 줄 차례 */
|
||
export function computeMilestone(
|
||
data: MilestoneData,
|
||
midDefaultM: number | null,
|
||
): MilestoneTableRow[] {
|
||
const base = data.base;
|
||
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 (row.dest === "mid") addLegs(sums, [mid], "mid");
|
||
const paved = sums.s2 + sums.s1 + sums.pv;
|
||
out.push({
|
||
route: route.id,
|
||
index,
|
||
item: row.item ?? "",
|
||
company: route.name ?? "",
|
||
dest: row.dest ?? "start",
|
||
s2: r3(sums.s2),
|
||
s1: r3(sums.s1),
|
||
pv: r3(sums.pv),
|
||
un: r3(sums.un),
|
||
paved: r3(paved),
|
||
unpaved: r3(sums.un),
|
||
total: r3(paved + sums.un),
|
||
note: row.note ?? "",
|
||
pick: false,
|
||
});
|
||
});
|
||
}
|
||
return out;
|
||
}
|