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:
@@ -4,7 +4,7 @@
|
||||
*
|
||||
* 평면도 = 평면도 관리 모달 그대로(`openPlanManager`)
|
||||
* 횡단도 = 장 배치(열 · 행 · [이 장] · [모든 장]) · 표 배율 · [안내선 숨기기] — 좌측 카드 밑에서 옮김
|
||||
* 구조물 도면 = 「M02 그림 — 축척 없음」 한 줄 · 종단 · 토적 · 전개도 = 몸 비움(축척 칸은 485-2)
|
||||
* 구조물 도면 = 「M02 그림 — 축척 없음」 한 줄 · 종단 · 횡단 · 토적 · 전개도 = X · Y 축척 칸(`_UI_Settings_Scale`)
|
||||
* [원본에서 다시 그리기] = 지금 연 도면이 그 묶음일 때만 켬
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
isCrossSheet,
|
||||
} from "./B07_DesignDetail_UI_Panels";
|
||||
import { openPlanManager, type PlanManageControls } from "./B07_DesignDetail_UI_PlanManage";
|
||||
import { mountDrawingScale } from "./B07_DesignDetail_UI_Settings_Scale";
|
||||
|
||||
type Kind = DesignDrawingItem["kind"];
|
||||
|
||||
@@ -81,6 +82,7 @@ export function openDrawingSettings(kind: Kind, deps: DrawingSettingsDeps): void
|
||||
closeModal = close;
|
||||
if (mine) body.append(note(`지금 도면: ${current?.label}`));
|
||||
if (kind === "structure") body.append(note("M02 그림 — 축척 없음"));
|
||||
mountDrawingScale(body, kind, deps, close);
|
||||
if (kind === "cross") mountCross(body, deps, close);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
/* =============================================================================
|
||||
* B07_DesignDetail_UI_Settings_Scale.ts
|
||||
* ⚙ 모달 X · Y 축척 칸(485-2 · 요구 280-5) — 종단 · 횡단 · 토적 · 전개도만.
|
||||
*
|
||||
* 칸 = 분모 정수 · 범위 밖 = 칸 붉게 + [축척 저장] 꺼짐 · 기본이 자동인 칸(토적 X · 전개도) = 「자동」
|
||||
* [축척 저장] = 그 종류 저장본이 있으면 n 장 물음(취소 = 그대로) → PUT → 화면 다시 세움(도면 다시 그림)
|
||||
* [기본값] = 칸만 기본으로 채움 — 저장은 [축척 저장]
|
||||
* ========================================================================== */
|
||||
|
||||
import { createCheckbox } from "@ui/ui_template_checkbox";
|
||||
import {
|
||||
createButton,
|
||||
createInputField,
|
||||
hideLoadingOverlay,
|
||||
showConfirmDialog,
|
||||
showLoadingOverlay,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import {
|
||||
type DrawingScaleAxes,
|
||||
type DrawingScaleKind,
|
||||
type DrawingScales,
|
||||
fetchDrawingScales,
|
||||
saveDrawingScales,
|
||||
} from "./B07_DesignDetail_Api_Fetch";
|
||||
import type { DrawingSettingsDeps } from "./B07_DesignDetail_UI_Settings";
|
||||
import { readScaleAxis, scaleToSend } from "./logic/B07_DesignDetail_Scale_Check";
|
||||
|
||||
const LABEL: Record<DrawingScaleKind, string> = {
|
||||
longitudinal: "종단도",
|
||||
cross: "횡단도",
|
||||
mass_haul: "토적도",
|
||||
development: "전개도",
|
||||
};
|
||||
const AXES = [
|
||||
["x", "X(가로) 1/"],
|
||||
["y", "Y(세로) 1/"],
|
||||
] as const;
|
||||
|
||||
const isScaleKind = (kind: string): kind is DrawingScaleKind => kind in LABEL;
|
||||
|
||||
/** ⚙ 모달 몸에 축척 칸을 붙임 — 축척 없는 종류는 아무것도 안 함. */
|
||||
export function mountDrawingScale(
|
||||
body: HTMLElement,
|
||||
kind: string,
|
||||
deps: DrawingSettingsDeps,
|
||||
close: () => void,
|
||||
): void {
|
||||
if (!isScaleKind(kind) || !deps.plan) return;
|
||||
const { projectId, onClosed } = deps.plan;
|
||||
const section = document.createElement("div");
|
||||
section.className = "b07-scale";
|
||||
section.textContent = "축척 받는 중…";
|
||||
body.append(section);
|
||||
void fetchDrawingScales(projectId)
|
||||
.then((data) => {
|
||||
section.textContent = "";
|
||||
buildScaleFields(section, kind, data, async (next) => {
|
||||
const saved = data.saved[kind] ?? 0;
|
||||
const ask = `${LABEL[kind]} 축척을 바꾸면 저장본 ${saved}장이 풀립니다(원본에서 다시 그림). 바꿀까요?`;
|
||||
if (saved > 0 && !(await showConfirmDialog(ask, "바꿈"))) return;
|
||||
showLoadingOverlay();
|
||||
try {
|
||||
await saveDrawingScales(projectId, { [kind]: next });
|
||||
close();
|
||||
showToast(`${LABEL[kind]} 축척을 바꿨습니다.`, "success");
|
||||
onClosed(true, deps.current()?.id); // 목록 · 도면을 서버에서 다시 받아 다시 그림
|
||||
} catch (error) {
|
||||
showToast(
|
||||
error instanceof Error ? error.message : "축척을 저장하지 못했습니다.",
|
||||
"error",
|
||||
);
|
||||
} finally {
|
||||
hideLoadingOverlay();
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
section.textContent = "축척을 받지 못했습니다.";
|
||||
});
|
||||
}
|
||||
|
||||
function buildScaleFields(
|
||||
section: HTMLElement,
|
||||
kind: DrawingScaleKind,
|
||||
data: DrawingScales,
|
||||
save: (next: DrawingScaleAxes) => Promise<void>,
|
||||
): void {
|
||||
const fields = AXES.map(([axis, label]) => {
|
||||
const fallback = data.defaults[kind][axis];
|
||||
const range = data.ranges[kind][axis];
|
||||
const [min, max] = range;
|
||||
const field = createInputField({ label, type: "number", compact: true, min, max });
|
||||
field.root.classList.add("b07-scale__field");
|
||||
const auto = fallback === null ? createCheckbox("자동") : undefined;
|
||||
if (auto) field.root.append(auto.root);
|
||||
const set = (value: number | null): void => {
|
||||
field.input.value = value === null ? "" : String(value);
|
||||
if (auto) auto.input.checked = value === null;
|
||||
field.input.disabled = value === null && !!auto;
|
||||
};
|
||||
set(data.scales[kind][axis]);
|
||||
const read = () => readScaleAxis(field.input.value, !!auto?.input.checked, range);
|
||||
return { axis, field, auto, fallback, set, read };
|
||||
});
|
||||
|
||||
const saveButton = createButton({
|
||||
label: "축척 저장",
|
||||
onClick: () => {
|
||||
const [x, y] = fields.map((f) => f.read()) as [number | null, number | null];
|
||||
const now = data.scales[kind];
|
||||
if (x === now.x && y === now.y) {
|
||||
showToast("바뀐 축척이 없습니다.", "info");
|
||||
return;
|
||||
}
|
||||
void save({ x: scaleToSend(x, fields[0].fallback), y: scaleToSend(y, fields[1].fallback) });
|
||||
},
|
||||
});
|
||||
const check = (): void => {
|
||||
let bad = false;
|
||||
for (const f of fields) {
|
||||
const result = f.read();
|
||||
f.field.setError(typeof result === "string" ? result : undefined);
|
||||
bad ||= typeof result === "string";
|
||||
}
|
||||
saveButton.disabled = bad;
|
||||
};
|
||||
for (const f of fields) {
|
||||
f.field.input.addEventListener("input", check);
|
||||
f.auto?.input.addEventListener("change", () => {
|
||||
if (f.auto!.input.checked) f.set(null);
|
||||
else f.field.input.disabled = false; // 빈 칸 — 숫자를 적어야 저장
|
||||
check();
|
||||
});
|
||||
}
|
||||
const reset = createButton({
|
||||
label: "기본값",
|
||||
variant: "ghost",
|
||||
onClick: () => {
|
||||
fields.forEach((f) => f.set(f.fallback));
|
||||
check();
|
||||
},
|
||||
});
|
||||
reset.title = "칸을 기본 축척으로 채움 — [축척 저장]으로 씀";
|
||||
|
||||
const title = document.createElement("p");
|
||||
title.className = "b07-settings__note";
|
||||
title.textContent =
|
||||
kind === "cross" ? "축척 — 횡단도 장(표준 횡단면도 뺌)" : `축척 — ${LABEL[kind]}`;
|
||||
const row = document.createElement("div");
|
||||
row.className = "b07-scale__row";
|
||||
row.append(...fields.map((f) => f.field.root));
|
||||
const buttons = document.createElement("div");
|
||||
buttons.className = "b07-scale__actions";
|
||||
buttons.append(reset, saveButton);
|
||||
section.append(title, row, buttons);
|
||||
}
|
||||
@@ -503,3 +503,27 @@ html.is-bm-route .b07-cad-stack > .cad-tabs {
|
||||
gap: var(--spacing-8);
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
/* ⚙ 축척 칸(485-2) — X · Y 두 칸 한 줄 · 단추 오른쪽 */
|
||||
.b07-scale {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
min-width: 280px;
|
||||
margin-bottom: var(--spacing-8);
|
||||
}
|
||||
|
||||
.b07-scale__row {
|
||||
display: flex;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.b07-scale__field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.b07-scale__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/* =============================================================================
|
||||
* B07_DesignDetail_Scale_Check.ts
|
||||
* ⚙ 축척 칸(485-2) 판정 — 칸 글 → 분모(정수 · 범위 안) · 자동 = null · 틀리면 까닭 글.
|
||||
* 서버 `checked_scales`(`_Router_Support_Scale.py`)와 같은 규칙 — 범위는 서버가 내려 줌.
|
||||
* ========================================================================== */
|
||||
|
||||
/** 칸 하나 읽기 — 숫자 · null(자동) · 까닭 글(틀림). */
|
||||
export function readScaleAxis(
|
||||
raw: string,
|
||||
auto: boolean,
|
||||
[low, high]: [number, number],
|
||||
): number | null | string {
|
||||
if (auto) return null;
|
||||
const text = raw.trim().replace(/,/g, "");
|
||||
const value = Number(text);
|
||||
if (text === "" || !Number.isInteger(value) || value < low || value > high) {
|
||||
return `1/${low.toLocaleString()}~1/${high.toLocaleString()} 사이 정수`;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** 보낼 값 — 기본값과 같으면 null(파일엔 기본 그대로 남김). */
|
||||
export const scaleToSend = (value: number | null, fallback: number | null): number | null =>
|
||||
value === fallback ? null : value;
|
||||
Reference in New Issue
Block a user