feat(485-2 B07 축척): ⚙ 모달 X · Y 축척 칸 — 종단 · 횡단 · 토적 · 전개도 · 분모 정수 · 범위 밖 = 칸 붉음 + 저장 꺼짐 · 토적 X · 전개도 「자동」 · 저장본 n 장 물음 · 저장 뒤 화면 다시 세움 · [기본값] · 새 _UI_Settings_Scale.ts · 판정 logic/_Scale_Check.ts · 시험 test_280_5 축척 셋
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -96,6 +96,27 @@ const modals = [];
|
||||
const plans = [];
|
||||
const calls = [];
|
||||
let failConfirm = false;
|
||||
const SCALES = {
|
||||
scales: {
|
||||
longitudinal: { x: 1000, y: 200 },
|
||||
cross: { x: 100, y: 100 },
|
||||
mass_haul: { x: null, y: 50000 },
|
||||
development: { x: null, y: null },
|
||||
},
|
||||
defaults: {
|
||||
longitudinal: { x: 1000, y: 200 },
|
||||
cross: { x: 100, y: 100 },
|
||||
mass_haul: { x: null, y: 50000 },
|
||||
development: { x: null, y: null },
|
||||
},
|
||||
ranges: {
|
||||
longitudinal: { x: [100, 10000], y: [10, 2000] },
|
||||
cross: { x: [10, 1000], y: [10, 1000] },
|
||||
mass_haul: { x: [100, 20000], y: [1000, 1000000] },
|
||||
development: { x: [10, 10000], y: [10, 10000] },
|
||||
},
|
||||
saved: { longitudinal: 2, cross: 0, mass_haul: 0, development: 0 },
|
||||
};
|
||||
let answer = true;
|
||||
|
||||
const button = (opts) => {
|
||||
@@ -118,13 +139,24 @@ const STUBS = {
|
||||
label.className = "ui-field__label";
|
||||
label.textContent = opts.label;
|
||||
root.append(label, input);
|
||||
return { root, input };
|
||||
const setError = (msg) => (input.error = msg ?? null);
|
||||
return { root, input, setError };
|
||||
},
|
||||
showToast: (message, kind) => toasts.push([kind, message]),
|
||||
showLoadingOverlay: () => calls.push(["overlay+"]),
|
||||
hideLoadingOverlay: () => calls.push(["overlay-"]),
|
||||
showConfirmDialog: async (message) => (calls.push(["ask", message]), answer),
|
||||
},
|
||||
"@ui/ui_template_checkbox": {
|
||||
createCheckbox: (text) => {
|
||||
const root = new FakeNode("label");
|
||||
root.className = "ui-checkbox";
|
||||
const input = new FakeNode("input");
|
||||
input.checked = false;
|
||||
root.append(input);
|
||||
return { root, input };
|
||||
},
|
||||
},
|
||||
"@ui/ui_template_modal": {
|
||||
openModal: (opts) => {
|
||||
const body = new FakeNode("div");
|
||||
@@ -152,6 +184,9 @@ const STUBS = {
|
||||
return { all_confirmed: true };
|
||||
},
|
||||
invalidateDesignDrawing: async (projectId, id) => calls.push(["invalidate", id]),
|
||||
// 485-2 축척 — 범위 · 기본은 서버(`_Router_Support_Scale.py`)와 같은 값
|
||||
fetchDrawingScales: async () => JSON.parse(JSON.stringify(SCALES)),
|
||||
saveDrawingScales: async (projectId, scales) => calls.push(["scales", scales]),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -274,12 +309,14 @@ const drawings = [
|
||||
openDrawingSettings("cross", deps);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
const cross = modals.at(-1);
|
||||
const crossInputs = all(cross.body).filter((n) => n.tagName === "INPUT");
|
||||
const layoutNode = byClass(cross.body, "b07-cross-layout")[0];
|
||||
const crossInputs = all(layoutNode).filter((n) => n.tagName === "INPUT");
|
||||
out.crossModal = {
|
||||
title: cross.opts.title,
|
||||
labels: byClass(cross.body, "ui-field__label").map((n) => n.textContent),
|
||||
labels: byClass(layoutNode, "ui-field__label").map((n) => n.textContent),
|
||||
values: crossInputs.map((n) => n.value),
|
||||
buttons: all(cross.body)
|
||||
scaleTitle: texts(byClass(cross.body, "b07-scale")[0])[0],
|
||||
buttons: all(layoutNode)
|
||||
.filter((n) => n.tagName === "BUTTON")
|
||||
.map((n) => [n.textContent, n.disabled]),
|
||||
guideLast: byClass(cross.body, "b07-cross-layout")[0].children.at(-1) === guideToggle,
|
||||
@@ -287,7 +324,7 @@ const drawings = [
|
||||
notes: texts(cross.body).filter((t) => t.startsWith("지금 도면")),
|
||||
};
|
||||
crossInputs[0].value = "2";
|
||||
all(cross.body)
|
||||
all(layoutNode)
|
||||
.find((n) => n.textContent === "이 장")
|
||||
.click();
|
||||
out.crossSave = { saved, closed: !cross.open };
|
||||
@@ -318,6 +355,115 @@ const drawings = [
|
||||
openDrawingSettings("cross", deps);
|
||||
out.standardInCross = !modals.at(-1).opts.actions[0].disabled;
|
||||
|
||||
// ── 축척 칸(485-2) ─────────────────────────────────────────────────────
|
||||
const closedCalls = [];
|
||||
const scaleDeps = { ...deps, plan: { projectId: "p1", onClosed: (...a) => closedCalls.push(a) } };
|
||||
const tick = () => new Promise((r) => setTimeout(r, 0));
|
||||
const scaleOf = async (kind) => {
|
||||
openDrawingSettings(kind, scaleDeps);
|
||||
await tick();
|
||||
const modal = modals.at(-1);
|
||||
const section = byClass(modal.body, "b07-scale")[0];
|
||||
const inputs = all(section).filter(
|
||||
(n) => n.tagName === "INPUT" && !n.parent.classList.contains("ui-checkbox"),
|
||||
);
|
||||
const autos = byClass(section, "ui-checkbox").map((n) => n.children[0]);
|
||||
const btn = (label) =>
|
||||
all(section).find((n) => n.tagName === "BUTTON" && n.textContent === label);
|
||||
const type = (input, value) => {
|
||||
input.value = value;
|
||||
input.listeners.input.forEach((h) => h());
|
||||
};
|
||||
const toggle = (box) => {
|
||||
box.checked = !box.checked;
|
||||
box.listeners.change.forEach((h) => h());
|
||||
};
|
||||
const state = () => ({
|
||||
values: inputs.map((n) => n.value),
|
||||
errors: inputs.map((n) => n.error ?? null),
|
||||
disabled: inputs.map((n) => !!n.disabled),
|
||||
autos: autos.map((n) => n.checked),
|
||||
save: !btn("축척 저장").disabled,
|
||||
});
|
||||
return { modal, section, inputs, autos, btn, type, toggle, state };
|
||||
};
|
||||
const scale = {};
|
||||
current = drawings[2]; // longitudinal
|
||||
let s = await scaleOf("longitudinal");
|
||||
scale.longLabels = byClass(s.section, "ui-field__label").map((n) => n.textContent);
|
||||
scale.longStart = s.state();
|
||||
s.type(s.inputs[0], "50");
|
||||
scale.longLow = s.state();
|
||||
s.type(s.inputs[0], "500.5");
|
||||
scale.longFrac = s.state();
|
||||
s.type(s.inputs[0], "500");
|
||||
scale.longOk = s.state();
|
||||
calls.length = 0;
|
||||
answer = false;
|
||||
s.btn("축척 저장").click();
|
||||
await tick();
|
||||
scale.cancel = { calls: calls.map((c) => c[0]), open: s.modal.open, closed: closedCalls.length };
|
||||
answer = true;
|
||||
calls.length = 0;
|
||||
s.btn("축척 저장").click();
|
||||
await tick();
|
||||
await tick();
|
||||
scale.save = {
|
||||
ask: calls.find((c) => c[0] === "ask")?.[1],
|
||||
sent: calls.find((c) => c[0] === "scales")?.[1],
|
||||
open: s.modal.open,
|
||||
closed: [...closedCalls],
|
||||
};
|
||||
s = await scaleOf("longitudinal");
|
||||
s.type(s.inputs[1], "300");
|
||||
s.btn("기본값").click();
|
||||
scale.reset = s.state();
|
||||
calls.length = 0;
|
||||
const toastAt = toasts.length;
|
||||
s.btn("축척 저장").click();
|
||||
await tick();
|
||||
scale.same = { calls: calls.length, toasts: toasts.slice(toastAt).map((t) => t[1]) };
|
||||
|
||||
s = await scaleOf("mass_haul");
|
||||
scale.massStart = s.state();
|
||||
s.toggle(s.autos[0]);
|
||||
scale.massManual = s.state();
|
||||
s.type(s.inputs[0], "2000");
|
||||
scale.massTyped = s.state();
|
||||
calls.length = 0;
|
||||
s.btn("축척 저장").click();
|
||||
await tick();
|
||||
await tick();
|
||||
scale.massSent = {
|
||||
ask: calls.filter((c) => c[0] === "ask").length,
|
||||
sent: calls.find((c) => c[0] === "scales")?.[1],
|
||||
};
|
||||
s = await scaleOf("mass_haul");
|
||||
s.toggle(s.autos[0]);
|
||||
s.toggle(s.autos[0]);
|
||||
scale.massBack = s.state();
|
||||
|
||||
s = await scaleOf("development");
|
||||
scale.dev = s.state();
|
||||
openDrawingSettings("structure", scaleDeps);
|
||||
scale.structure = byClass(modals.at(-1).body, "b07-scale").length;
|
||||
out.scale = scale;
|
||||
const Check = loadTs(path.join(B07, "logic", "B07_DesignDetail_Scale_Check.ts"));
|
||||
out.readAxis = [
|
||||
["1,000", false],
|
||||
[" 99 ", false],
|
||||
["10001", false],
|
||||
["12.5", false],
|
||||
["", false],
|
||||
["abc", false],
|
||||
["", true],
|
||||
].map(([raw, auto]) => Check.readScaleAxis(raw, auto, [100, 10000]));
|
||||
out.toSend = [
|
||||
Check.scaleToSend(200, 200),
|
||||
Check.scaleToSend(500, 200),
|
||||
Check.scaleToSend(null, null),
|
||||
];
|
||||
|
||||
// ── 저장 묶음 ────────────────────────────────────────────────────────────
|
||||
const tabsOf = (list, activeId) => ({
|
||||
tabs: () => list,
|
||||
|
||||
@@ -83,6 +83,7 @@ def test_횡단_모달은_배치_칸_표_배율_안내선(out):
|
||||
assert cross["guideLast"] is True
|
||||
assert cross["head"] == [["원본에서 다시 그리기", False]]
|
||||
assert cross["notes"] == ["지금 도면: cross_s1"]
|
||||
assert cross["scaleTitle"] == "축척 — 횡단도 장(표준 횡단면도 뺌)"
|
||||
saved = out["crossSave"]
|
||||
assert saved["closed"] is True
|
||||
assert saved["saved"] == [[{"default": [4, 4], "sheets": {"1": [2, 2]}, "table_scale": 1.2}, 1]]
|
||||
@@ -93,7 +94,7 @@ def test_구조물_종단_평면도_모달(out):
|
||||
assert structure["title"] == "구조물 도면 설정"
|
||||
assert structure["notes"] == ["M02 그림 — 축척 없음"]
|
||||
assert structure["head"][0][1] is True # 다른 묶음 도면을 연 중 = 꺼짐
|
||||
assert out["longModal"] == {"title": "종단도 설정", "bodyKids": 0} # 축척 칸은 485-2
|
||||
assert out["longModal"] == {"title": "종단도 설정", "bodyKids": 1} # 축척 칸(485-2)
|
||||
assert out["planModal"] == {"viaManager": True, "head": [["원본에서 다시 그리기", False]]}
|
||||
assert out["planRedraw"] == {"redraws": 1, "closed": True}
|
||||
assert out["standardInCross"] is True
|
||||
@@ -216,3 +217,48 @@ def test_서버_단계_완료(monkeypatch, tmp_path: Path) -> None:
|
||||
|
||||
monkeypatch.setattr(router, "_confirmed_source", blocked)
|
||||
assert asyncio.run(router.complete_design_stage(project)).status_code == 409
|
||||
|
||||
|
||||
RANGE = "1/100~1/10,000 사이 정수"
|
||||
|
||||
|
||||
def test_485_2_축척_칸_판정(out):
|
||||
assert out["readAxis"] == [1000, RANGE, RANGE, RANGE, RANGE, RANGE, None] # 자동 = null
|
||||
assert out["toSend"] == [None, 500, None] # 기본과 같으면 null
|
||||
|
||||
|
||||
def test_485_2_종단_축척_저장(out):
|
||||
scale = out["scale"]
|
||||
assert scale["longLabels"] == ["X(가로) 1/", "Y(세로) 1/"]
|
||||
assert scale["longStart"]["values"] == ["1000", "200"]
|
||||
assert scale["longStart"]["autos"] == [] # 종단 = 자동 없음
|
||||
for bad in ("longLow", "longFrac"): # 범위 밖 · 소수 = 붉음 + 저장 꺼짐
|
||||
assert scale[bad]["errors"] == [RANGE, None]
|
||||
assert scale[bad]["save"] is False
|
||||
assert scale["longOk"]["errors"] == [None, None] and scale["longOk"]["save"] is True
|
||||
# 저장본 2장 — 물음 [취소] = 요청 없음 · 모달 그대로
|
||||
assert scale["cancel"] == {"calls": ["ask"], "open": True, "closed": 0}
|
||||
save = scale["save"]
|
||||
assert "저장본 2장" in save["ask"]
|
||||
assert save["sent"] == {"longitudinal": {"x": 500, "y": None}} # Y = 기본 → null
|
||||
assert save["open"] is False
|
||||
assert save["closed"] == [[True, "longitudinal"]] # 화면 다시 세움 · 같은 도면
|
||||
assert scale["reset"]["values"] == ["1000", "200"] # [기본값] = 칸만
|
||||
assert scale["same"] == {"calls": 0, "toasts": ["바뀐 축척이 없습니다."]}
|
||||
|
||||
|
||||
def test_485_2_토적_전개도_자동(out):
|
||||
scale = out["scale"]
|
||||
assert scale["massStart"] == {
|
||||
"values": ["", "50000"],
|
||||
"errors": [None, None],
|
||||
"disabled": [True, False],
|
||||
"autos": [True],
|
||||
"save": True,
|
||||
}
|
||||
assert scale["massManual"]["errors"][0] and scale["massManual"]["save"] is False # 빈 칸
|
||||
assert scale["massTyped"]["save"] is True
|
||||
assert scale["massSent"] == {"ask": 0, "sent": {"mass_haul": {"x": 2000, "y": None}}}
|
||||
assert scale["massBack"]["autos"] == [True] and scale["massBack"]["save"] is True
|
||||
assert scale["dev"]["autos"] == [True, True] and scale["dev"]["disabled"] == [True, True]
|
||||
assert scale["structure"] == 0 # 구조물 = 축척 칸 없음
|
||||
|
||||
Reference in New Issue
Block a user