Merge remote-tracking branch 'origin/sub_laptop_1' into main_laptop_1
This commit is contained in:
@@ -160,7 +160,7 @@ export interface InitialMetrics {
|
||||
violations: InitialViolation[] | null;
|
||||
}
|
||||
|
||||
/** 제약 못 지킨 구간 하나 — 종단기울기 · 곡선반지름 · 계류 이격. */
|
||||
/** 서버 목록 한 줄 — 곡선반지름 · 계류 이격 · 측점 밖(못 지킴) · 원지반 기울기(`grade` · 참고만 · 419). */
|
||||
export interface InitialViolation {
|
||||
kind: string;
|
||||
station: string;
|
||||
@@ -205,6 +205,9 @@ export interface InitialChoice {
|
||||
offStationCurves?: number | null;
|
||||
/** 추천 창 「종단 반영」 값(421) — [적용] 균형 종단 · 다시 열 때 단추. 옛 캐시면 없음. */
|
||||
withProfile?: boolean;
|
||||
/** 시종점 높이 차 ÷ 연장(%) · 상한을 넘나(종단으로 못 풂) — 서버 `ends_grade_pct` · `ends_over_limit`(419). */
|
||||
endsGradePct?: number | null;
|
||||
endsOverLimit?: boolean;
|
||||
}
|
||||
|
||||
/** 거의 같은 노선인 다른 갈래 하나 — 두 노선 사이 가장 먼 거리(m). */
|
||||
@@ -328,7 +331,12 @@ export function normalizeRouteInitial(
|
||||
gradeDevPct: pick(m, "grade_dev_pct"),
|
||||
costIndex: pick(m, "cost_index"),
|
||||
surplusM3: pick(m, "surplus_m3"),
|
||||
overLimitCount: pick(over, "total"),
|
||||
// 「못 지킨 구간」과 같은 목록에서 셈(원지반 기울기 뺌 · 421-2) — 목록 없는 옛 답만 서버 수
|
||||
overLimitCount: Array.isArray(m.violations)
|
||||
? (m.violations as Array<Record<string, unknown>>).filter(
|
||||
(item) => item.kind !== "grade",
|
||||
).length
|
||||
: pick(over, "total"),
|
||||
offsetM: pick(m, "offset_avg_m"),
|
||||
violations: Array.isArray(m.violations)
|
||||
? (m.violations as Array<Record<string, unknown>>).map((item) => ({
|
||||
@@ -346,6 +354,8 @@ export function normalizeRouteInitial(
|
||||
bandWidthM: num(body.band_width_m),
|
||||
stored: body.stored === true,
|
||||
offStationCurves: num(body.off_station_curves),
|
||||
endsGradePct: num(body.ends_grade_pct),
|
||||
endsOverLimit: body.ends_over_limit === true,
|
||||
// 서버가 셈에 쓴 값이 먼저(420) · 옛 서버면 보낸 값
|
||||
withProfile: typeof body.with_profile === "boolean" ? body.with_profile : withProfile,
|
||||
balance: parseBalance(m.profile),
|
||||
@@ -555,9 +565,17 @@ export function violationText(item: InitialViolation): string {
|
||||
return `${item.station} ${value}${head ? `(${head})` : ""}`;
|
||||
}
|
||||
|
||||
/** 원지반 기울기 — 참고만(419 · 계획선 높이로 푸는 값) · 「못 지킨 구간」 · 「기준 초과」에 안 듦. */
|
||||
const isGroundGrade = (item: InitialViolation): boolean => item.kind === "grade";
|
||||
|
||||
/** 제약 못 지킨 구간(R · 계류 · 측점) — 서버 목록에서 원지반 기울기를 뺀 것. 옛 답(목록 없음)이면 null. */
|
||||
export function brokenItems(metrics: InitialMetrics | null): InitialViolation[] | null {
|
||||
return metrics?.violations?.filter((item) => !isGroundGrade(item)) ?? null;
|
||||
}
|
||||
|
||||
/** 제약 못 지킨 구간 한 줄 — 없으면 「모두 지킴」. 서버가 목록을 안 줬으면 빈 글. */
|
||||
export function violationsLine(metrics: InitialMetrics | null): string {
|
||||
const items = metrics?.violations;
|
||||
const items = brokenItems(metrics);
|
||||
if (!items) return "";
|
||||
if (!items.length) return "제약 못 지킨 구간 없음 — 모두 지킴";
|
||||
const shown = items.slice(0, VIOLATION_SHOWN).map(violationText).join(" · ");
|
||||
@@ -565,6 +583,25 @@ export function violationsLine(metrics: InitialMetrics | null): string {
|
||||
return `제약 못 지킨 구간 ${items.length}곳: ${shown}${more}`;
|
||||
}
|
||||
|
||||
/** 원지반 기울기(참고) 한 줄 — 최대 · 상한 넘는 곳 수. 서버가 목록을 안 줬으면 빈 글. */
|
||||
export function groundGradeLine(metrics: InitialMetrics | null): string {
|
||||
const items = metrics?.violations;
|
||||
if (!items) return "";
|
||||
const over = items.filter(isGroundGrade).length;
|
||||
return `원지반 기울기(참고) 최대 ${fixed(metrics.maxGradePct, 1, "%")} · 상한 넘는 곳 ${over}`;
|
||||
}
|
||||
|
||||
/** 원지반 기울기 넘는 곳 전부 — 참고 줄 풍선. */
|
||||
export function groundGradeTitle(metrics: InitialMetrics | null): string {
|
||||
return (metrics?.violations ?? []).filter(isGroundGrade).map(violationText).join("\n");
|
||||
}
|
||||
|
||||
/** 시종점 높이 차가 상한을 넘을 때만 — 종단으로 못 맞춤(노선을 늘려야 함 · 419). 아니면 빈 글. */
|
||||
export function endsLine(choice: InitialChoice | null): string {
|
||||
if (!choice?.endsOverLimit) return "";
|
||||
return `시종점 높이 차가 상한을 넘음(평균 ${fixed(choice.endsGradePct ?? null, 1, "%")}) — 종단으로 못 맞춤 · 노선을 늘려야 함`;
|
||||
}
|
||||
|
||||
/** 상한을 풀었을 때만 — 「지형상 14% 로 못 이어져 상한을 23.5% 로 풂」. 아니면 빈 글. */
|
||||
export function relaxedLine(choice: InitialChoice | null): string {
|
||||
const bound = choice?.terrainBoundPct;
|
||||
|
||||
@@ -39,6 +39,10 @@ import {
|
||||
SIMILAR_HINT,
|
||||
similarLine,
|
||||
violationsLine,
|
||||
brokenItems,
|
||||
endsLine,
|
||||
groundGradeLine,
|
||||
groundGradeTitle,
|
||||
violationText,
|
||||
WEIGHT_KEYS,
|
||||
type CriteriaKey,
|
||||
@@ -219,6 +223,8 @@ export function openRecommendModal(params: RecommendParams): void {
|
||||
<div class="b05-routerec__cands" data-line="candidates" hidden></div>
|
||||
</div>
|
||||
<div data-line="violations" hidden></div>
|
||||
<div data-line="ground" hidden></div>
|
||||
<div class="is-warn" data-line="ends" hidden></div>
|
||||
<div class="is-warn" data-line="relaxed" hidden></div>
|
||||
<div class="is-warn" data-line="stations" hidden></div>
|
||||
<div data-line="similar" hidden></div>
|
||||
@@ -423,10 +429,18 @@ export function openRecommendModal(params: RecommendParams): void {
|
||||
line("candidates").hidden = seen.size === 0;
|
||||
const violations = violationsLine(view?.metrics ?? null);
|
||||
line("violations").textContent = violations;
|
||||
line("violations").title = (view?.metrics?.violations ?? [])
|
||||
line("violations").title = (brokenItems(view?.metrics ?? null) ?? [])
|
||||
.map((item) => `${violationText(item)} — ${item.reason}`)
|
||||
.join("\n");
|
||||
line("violations").hidden = !violations;
|
||||
// 원지반 기울기 = 참고 줄 따로 · 시종점 높이 차 알림(421-2 · 엔진 419)
|
||||
const ground = groundGradeLine(view?.metrics ?? null);
|
||||
line("ground").textContent = ground;
|
||||
line("ground").title = groundGradeTitle(view?.metrics ?? null);
|
||||
line("ground").hidden = !ground;
|
||||
const ends = endsLine(view);
|
||||
line("ends").textContent = ends;
|
||||
line("ends").hidden = !ends;
|
||||
const relaxed = relaxedLine(view);
|
||||
line("relaxed").textContent = relaxed;
|
||||
line("relaxed").hidden = !relaxed;
|
||||
|
||||
@@ -31,10 +31,11 @@ const LOGIC_HELP: Record<ShownLogic, HelpEntry> = {
|
||||
how: [
|
||||
"새로 길을 찾지 않음 — 예상노선 꺾임점에 맞춘 곡선반지름(최소 이상 · 예상노선에 가장 가까운 값)으로 곡선만 넣음",
|
||||
"전처리 초기 계획노선과 같은 규칙 · 곡선 가운데(MC)는 일반측점(20m) 위로 맞춤",
|
||||
"종단기울기 · 계류 이격은 막지 않고, 넘은 곳만 「못 지킨 구간」에 적음",
|
||||
"계류 이격은 막지 않고, 넘은 곳만 「못 지킨 구간」에 적음 · 원지반 기울기는 참고 줄",
|
||||
],
|
||||
when: "현장 답사로 정한 노선을 출발점으로 삼을 때 · 가장 빨리 나옴",
|
||||
caution: "법정 기울기 · 계류 이격을 보장하지 않음 — 못 지킨 구간을 꼭 확인",
|
||||
caution:
|
||||
"계류 이격 · 기울기를 보장하지 않음 — 못 지킨 구간 · 원지반 기울기(참고) · 시종점 알림을 꼭 확인",
|
||||
profile: "켬 = 균형 종단 · 끔 = 전처리 계획선처럼 계획고 = 지반고",
|
||||
},
|
||||
gradeline: {
|
||||
@@ -87,7 +88,7 @@ const CRITERIA_HELP: Array<{ name: string; text: string }> = [
|
||||
},
|
||||
{
|
||||
name: "종단기울기 상한(%)",
|
||||
text: "이보다 가파른 구간을 만들지 않음. 비우면 임도 종류 · 지형 · 설계속도 법정 값(20km/h 일반 9% · 특수 14%). 곡선을 넣은 뒤 넘으면 상한을 조금씩 낮춰 다시 찾음",
|
||||
text: "지반이 이보다 가파른 쪽으로 곧장 오르는 길은 찾지 않음. 측점(20m) 사이 원지반 기울기는 참고만 — 계획선 높이(종단)로 푸는 값이라 「못 지킨 구간」에 안 듦. 시종점 높이 차가 상한을 넘으면 종단으로도 못 맞춰 알림. 비우면 임도 종류 · 지형 · 설계속도 법정 값(20km/h 일반 9% · 특수 14%)",
|
||||
},
|
||||
{
|
||||
name: "최소 반지름(m)",
|
||||
@@ -118,7 +119,15 @@ const LINES_HELP: Array<{ name: string; text: string }> = [
|
||||
},
|
||||
{
|
||||
name: "못 지킨 구간",
|
||||
text: "제약을 넘은 측점(5곳까지 · 나머지는 「외 N곳」, 전체는 마우스를 올리면 보임)",
|
||||
text: "최소 반지름 · 계류 이격 · 측점 밖 곡선을 넘은 곳만(「기준 초과」 수와 같음 · 5곳까지 · 나머지는 「외 N곳」, 전체는 마우스를 올리면 보임)",
|
||||
},
|
||||
{
|
||||
name: "원지반 기울기(참고)",
|
||||
text: "측점(20m) 사이 원지반 기울기 최대 · 상한 넘는 곳 수 — 종단으로 푸는 값이라 판정에 안 듦(넘는 곳은 마우스를 올리면 보임)",
|
||||
},
|
||||
{
|
||||
name: "시종점 높이 차",
|
||||
text: "시점 · 종점 높이 차 ÷ 노선 연장이 상한을 넘으면 알림 — 종단으로 못 맞춤 · 노선을 늘려야 함",
|
||||
},
|
||||
{
|
||||
name: "상한을 풂",
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/* 421-2 추천 창 「못 지킨 구간」 · 원지반 기울기(참고) · 시종점 알림 헬퍼 — `_Initial_Model` 을
|
||||
* 그 자리에서 트랜스파일해 Node 로 돌려 줄 글을 JSON 한 벌로.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const cache = new Map();
|
||||
|
||||
function memoryStorage() {
|
||||
const map = new Map();
|
||||
return {
|
||||
getItem: (key) => (map.has(key) ? map.get(key) : null),
|
||||
setItem: (key, value) => map.set(key, String(value)),
|
||||
removeItem: (key) => map.delete(key),
|
||||
key: (index) => [...map.keys()][index] ?? null,
|
||||
get length() {
|
||||
return map.size;
|
||||
},
|
||||
_map: map,
|
||||
};
|
||||
}
|
||||
const session = memoryStorage();
|
||||
globalThis.window = { localStorage: memoryStorage(), sessionStorage: session };
|
||||
globalThis.fetch = () => Promise.resolve({ ok: false });
|
||||
|
||||
const STUBS = { "@ui/ui_template_palette": { themeColor: (_name, fallback) => fallback } };
|
||||
|
||||
function loadTs(file) {
|
||||
const full = path.resolve(file);
|
||||
if (cache.has(full)) return cache.get(full);
|
||||
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box.exports);
|
||||
const localRequire = (request) => {
|
||||
if (STUBS[request]) return STUBS[request];
|
||||
if (request.endsWith(".css")) return {};
|
||||
if (request.startsWith("@ui/"))
|
||||
return loadTs(path.join(ROOT, "ui_template", `${request.slice(4)}.ts`));
|
||||
if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`));
|
||||
return require(request);
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
cache.set(full, box.exports);
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
const model = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_RouteEdit_Initial_Model.ts"));
|
||||
|
||||
const nodes = [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 10, y: 5 },
|
||||
];
|
||||
const item = (kind, station, value, limit, reason) => ({ kind, station, value, limit, reason });
|
||||
const now = model.normalizeRouteInitial(
|
||||
{
|
||||
nodes,
|
||||
curves: [],
|
||||
metrics: {
|
||||
max_grade_pct: 35.8,
|
||||
over_limit: { grade: 2, radius: 1, station: 1, stream: 0, total: 4 },
|
||||
violations: [
|
||||
item("grade", "No.1", 28.8, 14, "원지반 기울기 — 참고"),
|
||||
item("radius", "No.3", 10, 12, "지형상 불가능 — 곡선"),
|
||||
item("grade", "No.4", 18.9, 14, "원지반 기울기"),
|
||||
item("station", "No.5+3.0", 3, 0, "측점 밖"),
|
||||
],
|
||||
},
|
||||
ends_grade_pct: 16.24,
|
||||
ends_over_limit: true,
|
||||
},
|
||||
"earthwork",
|
||||
null,
|
||||
);
|
||||
const clean = model.normalizeRouteInitial(
|
||||
{
|
||||
nodes,
|
||||
curves: [],
|
||||
metrics: {
|
||||
max_grade_pct: 12,
|
||||
over_limit: { total: 1, grade: 1 },
|
||||
violations: [item("grade", "No.2", 15, 14, "")],
|
||||
},
|
||||
ends_grade_pct: 3,
|
||||
ends_over_limit: false,
|
||||
},
|
||||
"follow",
|
||||
null,
|
||||
);
|
||||
const old = model.normalizeRouteInitial(
|
||||
{ nodes, curves: [], metrics: { max_grade_pct: 9, over_limit: { total: 3 } } },
|
||||
"gentle",
|
||||
null,
|
||||
);
|
||||
|
||||
const lines = (choice) => ({
|
||||
violations: model.violationsLine(choice.metrics),
|
||||
ground: model.groundGradeLine(choice.metrics),
|
||||
groundTitle: model.groundGradeTitle(choice.metrics),
|
||||
ends: model.endsLine(choice),
|
||||
over: choice.metrics.overLimitCount,
|
||||
route: model.metricsLines(choice.metrics).route,
|
||||
});
|
||||
process.stdout.write(JSON.stringify({ now: lines(now), clean: lines(clean), old: lines(old) }));
|
||||
@@ -174,12 +174,13 @@ const labels = {
|
||||
violationsOld: model.violationsLine({ ...plain.metrics, violations: null }),
|
||||
violationsMany: model.violationsLine({
|
||||
...plain.metrics,
|
||||
// 못 지킨 곳(R) 일곱 — 원지반 기울기는 참고 줄이라 이 줄에 안 듦(421-2)
|
||||
violations: Array.from({ length: 7 }, (_, i) => ({
|
||||
kind: "grade",
|
||||
kind: "radius",
|
||||
station: `No.${i}+0.0`,
|
||||
value: 15 + i,
|
||||
limit: 14,
|
||||
reason: "곡선 맞춤 뒤 지반 차 — 측점 사이 지반이 상한을 넘음",
|
||||
value: 5 + i,
|
||||
limit: 12,
|
||||
reason: "곡선 둘 자리 모자람 — 교점 사이가 짧음",
|
||||
})),
|
||||
}),
|
||||
relaxed: model.relaxedLine(plain),
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
"""421-2 추천 창 — 「못 지킨 구간」 = R · 계류 · 측점만 · 원지반 기울기(참고) 줄 · 시종점 알림.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_421_2_violation_lines.py -q
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
B05 = ROOT / "B05_Profile"
|
||||
HELPER = Path(__file__).with_name("helper_421_2_violation_lines.cjs")
|
||||
|
||||
|
||||
def _lines() -> dict:
|
||||
run = subprocess.run(
|
||||
["node", str(HELPER)], capture_output=True, text=True, encoding="utf-8", check=True
|
||||
)
|
||||
return json.loads(run.stdout)
|
||||
|
||||
|
||||
def test_못_지킨_구간은_기울기_빼고_기준_초과와_같음() -> None:
|
||||
now = _lines()["now"]
|
||||
assert now["violations"].startswith("제약 못 지킨 구간 2곳: No.3 R 10.0m")
|
||||
assert "28.8%" not in now["violations"] and "No.5+3.0 측점 밖 3.0m" in now["violations"]
|
||||
assert now["over"] == 2 and "기준 초과 2구간" in now["route"] # 서버 total 4 아님
|
||||
assert now["ground"] == "원지반 기울기(참고) 최대 35.8% · 상한 넘는 곳 2"
|
||||
assert now["groundTitle"].split("\n") == [
|
||||
"No.1 28.8%(원지반 기울기)",
|
||||
"No.4 18.9%(원지반 기울기)",
|
||||
]
|
||||
assert now["ends"] == (
|
||||
"시종점 높이 차가 상한을 넘음(평균 16.2%) — 종단으로 못 맞춤 · 노선을 늘려야 함"
|
||||
)
|
||||
|
||||
|
||||
def test_기울기만_넘으면_모두_지킴_시종점_알림_없음() -> None:
|
||||
clean = _lines()["clean"]
|
||||
assert clean["violations"] == "제약 못 지킨 구간 없음 — 모두 지킴"
|
||||
assert clean["over"] == 0 and clean["ground"].endswith("상한 넘는 곳 1")
|
||||
assert clean["ends"] == ""
|
||||
|
||||
|
||||
def test_옛_답은_서버_수_그대로_줄_없음() -> None:
|
||||
old = _lines()["old"]
|
||||
assert (
|
||||
old["over"] == 3 and old["violations"] == "" and old["ground"] == "" and old["ends"] == ""
|
||||
)
|
||||
|
||||
|
||||
def test_창에_줄_두_개와_도움말() -> None:
|
||||
rec = (B05 / "B05_Profile_UI_RouteEdit_Recommend.ts").read_text(encoding="utf-8")
|
||||
assert '<div data-line="ground" hidden></div>' in rec
|
||||
assert '<div class="is-warn" data-line="ends" hidden></div>' in rec
|
||||
assert 'line("violations").title = (brokenItems(view?.metrics ?? null) ?? [])' in rec
|
||||
assert "const ends = endsLine(view);" in rec
|
||||
help_ts = (B05 / "B05_Profile_UI_RouteEdit_Recommend_Help.ts").read_text(encoding="utf-8")
|
||||
for name in ('name: "원지반 기울기(참고)",', 'name: "시종점 높이 차",'):
|
||||
assert name in help_ts
|
||||
assert "이보다 가파른 구간을 만들지 않음" not in help_ts
|
||||
assert "「기준 초과」 수와 같음" in help_ts
|
||||
@@ -94,14 +94,15 @@ def test_비교값과_기준값_글(results):
|
||||
|
||||
def test_제약_못_지킨_구간과_상한_풂(results):
|
||||
labels = results["labels"]
|
||||
# 원지반 기울기(No.9 29.1%)는 참고 줄로 — 못 지킨 구간에 안 듦(421-2)
|
||||
assert labels["violations"] == (
|
||||
"제약 못 지킨 구간 3곳: No.9+0.0 29.1%(지형상 불가능)"
|
||||
" · No.20+0.0 R 9.5m(곡선 둘 자리 모자람) · No.30+0.0 계류 32.0m(계류 버퍼 안)"
|
||||
"제약 못 지킨 구간 2곳: No.20+0.0 R 9.5m(곡선 둘 자리 모자람)"
|
||||
" · No.30+0.0 계류 32.0m(계류 버퍼 안)"
|
||||
)
|
||||
assert labels["violationsNone"] == "제약 못 지킨 구간 없음 — 모두 지킴"
|
||||
assert labels["violationsOld"] == "" # 옛 답(목록 칸 없음)은 줄을 숨긴다
|
||||
assert labels["violationsMany"].startswith(
|
||||
"제약 못 지킨 구간 7곳: No.0+0.0 15.0%(곡선 맞춤 뒤 지반 차)"
|
||||
"제약 못 지킨 구간 7곳: No.0+0.0 R 5.0m(곡선 둘 자리 모자람)"
|
||||
)
|
||||
assert labels["violationsMany"].endswith(" 외 2곳")
|
||||
assert labels["relaxed"] == "지형상 14.0% 로 못 이어져 상한을 23.5% 로 풂"
|
||||
@@ -382,5 +383,6 @@ def test_같은_노선_알림_줄(results):
|
||||
|
||||
def test_비중_계산_넘은_구간_사유_그대로(results):
|
||||
six = results["sixth"]
|
||||
assert six["weightedViolation"] == "제약 못 지킨 구간 1곳: No.9+0.0 29.1%(비중 계산)"
|
||||
# 원지반 기울기만 넘음 = 못 지킨 곳 없음(421-2) · 사유 글은 참고 줄 풍선에 그대로
|
||||
assert six["weightedViolation"] == "제약 못 지킨 구간 없음 — 모두 지킴"
|
||||
assert six["weightedViolationReason"] == "No.9+0.0 29.1%(비중 계산)"
|
||||
|
||||
Reference in New Issue
Block a user