fix(b08): 거리이정표 km 칸 끝수 정리 — 3자리 반올림 · 끝 0 지움 · 안 고치면 저장 m 그대로 (261-1)

- 시점 창 · 구간 창 · 다이어그램 선 위 km 칸 · 자동 값 글 = 같은 도우미(fmtKm)
- 안 고친 칸은 반올림 값으로 덮지 않음
- 시험 test_261_1 추가 · test_255_1 기대값만 고침

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018jonfdxn1Xit9bdkNJpE4G
This commit is contained in:
2026-10-11 15:17:40 +09:00
co-authored by Claude Sonnet 5.5
parent 6e53acfcac
commit 4737756502
5 changed files with 106 additions and 20 deletions
@@ -39,7 +39,7 @@ import {
type DiagramBox,
} from "./B08_Quantity_Milestone_Layout";
import { openLegInfo } from "./B08_Quantity_UI_Milestone_LegModal";
import { openPlaceModal, openSiteInfo, toastKm } from "./B08_Quantity_UI_Milestone_Modal";
import { fmtKm, openPlaceModal, openSiteInfo, toastKm } from "./B08_Quantity_UI_Milestone_Modal";
import {
findSegmentKm,
liveMilestone,
@@ -87,11 +87,12 @@ function editLegAt(
className: "ui-input",
attrs: { type: "number", min: "0", step: "any" },
});
km.value = leg.km == null ? "" : String(leg.km);
km.value = leg.km == null ? "" : fmtKm(leg.km);
const shown = km.value; // 안 고치면 저장 값 그대로 · 반올림 값으로 안 덮음
km.setAttribute("aria-label", "km");
if (leg.auto_km != null) {
km.placeholder = String(leg.auto_km); // 빈칸 = 자동 값
km.title = `자동 값 ${leg.auto_km} km`;
km.placeholder = fmtKm(leg.auto_km); // 빈칸 = 자동 값
km.title = `자동 값 ${fmtKm(leg.auto_km)} km`;
}
const box = el("div", { className: "b08-milestone__leg-edit", children: [road.root, km] });
box.dataset.b08MilestoneLegEdit = `${from}:${i}`;
@@ -108,7 +109,7 @@ function editLegAt(
const value = km.value.trim() && Number.isFinite(n) && n >= 0 ? n : null;
const patch = {
road: (road.select.value || null) as Road | null,
...(km.value.trim() && value === null ? {} : { km: value }),
...(km.value === shown || (km.value.trim() && value === null) ? {} : { km: value }),
};
putMilestone(projectId, setLeg(liveMilestone(projectId), from, i, patch));
};
@@ -24,7 +24,7 @@ import {
} from "./B08_Quantity_Milestone_Calc";
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 { fmtKm, toastKm } from "./B08_Quantity_UI_Milestone_Modal";
import { reverseAddress } from "./B08_Quantity_UI_Milestone_Place";
import {
findSegmentKm,
@@ -47,7 +47,13 @@ export function legsEditor(
kind: SegKind,
): { root: HTMLElement; value: () => Leg[] } {
const list = el("div");
const rows: Array<{ road: HTMLSelectElement; km: HTMLInputElement; auto: number | null }> = [];
const rows: Array<{
road: HTMLSelectElement;
km: HTMLInputElement;
auto: number | null;
orig: number | null; // 안 고친 칸 = 원래 값 그대로(반올림 값으로 안 덮음)
shown: string;
}> = [];
const add = (leg: Leg): void => {
const road = createSelectField({
label: "도로 구분",
@@ -56,13 +62,13 @@ export function legsEditor(
});
const dist = createInputField({ label: "거리 (km)", type: "number", min: 0, compact: true });
road.select.value = roadOf(leg.road, kind);
dist.input.value = leg.km == null ? "" : String(leg.km);
dist.input.value = leg.km == null ? "" : fmtKm(leg.km);
const auto = leg.auto_km ?? null;
if (auto != null) {
dist.input.placeholder = String(auto); // 빈칸 = 자동 값 · 고치면 그 값 우선
dist.input.title = `자동 값 ${auto} km`;
dist.input.placeholder = fmtKm(auto); // 빈칸 = 자동 값 · 고치면 그 값 우선
dist.input.title = `자동 값 ${fmtKm(auto)} km`;
}
const ref = { road: road.select, km: dist.input, auto };
const ref = { road: road.select, km: dist.input, auto, orig: leg.km, shown: dist.input.value };
rows.push(ref);
const drop = createButton({
label: "×",
@@ -90,9 +96,9 @@ export function legsEditor(
return {
root: el("div", { children: [list, more] }),
value: () =>
rows.map(({ road, km: dist, auto }) => ({
rows.map(({ road, km: dist, auto, orig, shown }) => ({
road: (ROADS as readonly string[]).includes(road.value) ? (road.value as Road) : null,
km: km(dist.value),
km: dist.value === shown ? orig : km(dist.value),
auto_km: auto,
})),
};
@@ -117,8 +123,9 @@ function openMidLeg(projectId: string): void {
road.select.value = roadOf(data.site.mid_road, "mid");
const dist = createInputField({ label: "거리 (km)", type: "number", min: 0, compact: true });
dist.input.value =
data.site.mid_station_m == null ? "" : String(data.site.mid_station_m / 1000);
dist.input.placeholder = site?.mid_default_m == null ? "" : String(site.mid_default_m / 1000); // 빈칸 = 노선 ÷ 2
data.site.mid_station_m == null ? "" : fmtKm(data.site.mid_station_m / 1000);
dist.input.placeholder = site?.mid_default_m == null ? "" : fmtKm(site.mid_default_m / 1000); // 빈칸 = 노선 ÷ 2
const shown = dist.input.value; // 안 고치면 저장 값(m) 그대로
dist.input.dataset.b08MilestoneLegKm = "start";
const save = createButton({
label: "수정",
@@ -129,7 +136,7 @@ function openMidLeg(projectId: string): void {
projectId,
setLeg(fresh, "start", 0, {
road: road.select.value as Road,
km: km(dist.input.value),
...(dist.input.value === shown ? {} : { km: km(dist.input.value) }),
}),
);
close();
@@ -64,6 +64,9 @@ export function toastKm(text: string): void {
if (text) showToast(`길찾기 — ${text}`, text.includes(" — ") ? "warning" : "success");
}
/** 칸 · 글에 보이는 km — 3자리(1 m) 반올림 · 끝 0 지움(셈 값은 안 바꿈 · 261-1) */
export const fmtKm = (km: number): string => String(Math.round(km * 1000) / 1000);
let listSeq = 0;
/** 항목(손 입력 + 제안 목록 · 244-14) · 목적지 ▾ 한 줄 — 목록 밖 글도 받음 · 분류 바꾸면 제안만 바뀜 */
@@ -270,9 +273,10 @@ export function openSiteInfo(projectId: string): void {
compact: true,
});
mid.input.step = "0.001";
mid.input.placeholder = site?.mid_default_m != null ? String(site.mid_default_m / 1000) : "";
mid.input.placeholder = site?.mid_default_m != null ? fmtKm(site.mid_default_m / 1000) : "";
mid.input.value =
data.site.mid_station_m == null ? "" : String(data.site.mid_station_m / 1000);
data.site.mid_station_m == null ? "" : fmtKm(data.site.mid_station_m / 1000);
const shown = mid.input.value; // 안 고치면 저장 값(m) 그대로 · 반올림 값으로 안 덮음
mid.input.dataset.b08Milestone = "mid_station_m";
const road = createSelectField({
label: "시점 → 중점 도로",
@@ -290,7 +294,10 @@ export function openSiteInfo(projectId: string): void {
const fresh = liveMilestone(projectId);
putMilestone(
projectId,
setLeg(fresh, "start", 0, { road: road.select.value as Road, km }),
setLeg(fresh, "start", 0, {
road: road.select.value as Road,
...(text === shown ? {} : { km }),
}),
);
close();
},
@@ -41,7 +41,7 @@ def test_시점_모달_km_카카오_지도():
modal = _read("B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts")
site = modal[modal.index("export function openSiteInfo") :]
assert 'label: "중점 측점 (km)"' in site
assert "String(data.site.mid_station_m / 1000)" in site # 보임 = km
assert "fmtKm(data.site.mid_station_m / 1000)" in site # 보임 = km(261-1 끝수 정리)
assert 'setLeg(fresh, "start", 0, {' in site # 받음 km → 저장 m
# 설명 줄 · 등록 주소 · 브이월드 주소 줄 · B01 타일 지도 없음
for gone in ("시점 = 노선 첫 점", "프로젝트 등록 주소", "브이월드 주소", "buildAddressMap"):
@@ -0,0 +1,71 @@
"""261-1 거리이정표 km 칸 끝수 — 보이는 km = 3자리(1 m) 반올림 · 끝 0 지움 · 안 고치면 저장 m 그대로 ·
고치면 km × 1000 → m(끝수 없이)."""
import json
import re
import subprocess
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
UI = "B08_Quantity/B08_Quantity_UI_Milestone"
def _read(rel):
return (ROOT / rel).read_text(encoding="utf-8")
def _node(src, expr):
result = subprocess.run(
["node", "--experimental-strip-types", "--no-warnings",
"--input-type=module-typescript", "-e",
IMPORTS.sub("", 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 _fmt(values):
modal = _read(f"{UI}_Modal.ts")
line = next(ln for ln in modal.splitlines() if ln.startswith("export const fmtKm"))
return _node(line.replace("export ", ""), f"{json.dumps(values)}.map(fmtKm)")
def test_km_글_3자리_끝0_지움():
assert _fmt([576.595 / 1000, 0.3, 0.1 + 0.2, 12, 0, 1.2345, 0.0005, 2 / 3]) == [
"0.577", "0.3", "0.3", "12", "0", "1.235", "0.001", "0.667",
] # fmt: skip
def test_고치면_km_곱하기_1000_끝수_없이():
layout = _read("B08_Quantity/B08_Quantity_Milestone_Layout.ts")
data = {"site": {"mid_station_m": 576.595, "mid_road": "비포장"}, "base": {}, "routes": []}
expr = f'setLeg({json.dumps(data)}, "start", 0, {{ km: 0.3 }}).site.mid_station_m'
assert _node(layout, expr) == 300
# 안 고침 = km 키 없는 patch → 저장 m 그대로
expr = f'setLeg({json.dumps(data)}, "start", 0, {{ road: "비포장" }}).site.mid_station_m'
assert _node(layout, expr) == 576.595
def test_모든_km_칸_같은_규칙():
modal = _read(f"{UI}_Modal.ts")
site = modal[modal.index("export function openSiteInfo") :]
assert "fmtKm(data.site.mid_station_m / 1000)" in site
assert "fmtKm(site.mid_default_m / 1000)" in site
assert "...(text === shown ? {} : { km })" in site # 안 고침 = 덮지 않음
leg = _read(f"{UI}_LegModal.ts")
assert "fmtKm(data.site.mid_station_m / 1000)" in leg
assert "fmtKm(site.mid_default_m / 1000)" in leg
assert "...(dist.input.value === shown ? {} : { km: km(dist.input.value) })" in leg
assert "dist.value === shown ? orig : km(dist.value)" in leg # 구간 줄
assert "fmtKm(leg.km)" in leg and "fmtKm(auto)" in leg
diagram = _read(f"{UI}_Diagram.ts")
assert "fmtKm(leg.km)" in diagram and "fmtKm(leg.auto_km)" in diagram
assert "km.value === shown" in diagram # 선 위 km 고치기 칸 · 안 고침 = 덮지 않음
for src in (modal, leg, diagram):
assert "String(leg.km)" not in src and "mid_station_m / 1000)" not in src.replace(
"fmtKm(data.site.mid_station_m / 1000)", ""
) # 맨 String 변환 없음