Files
Aislo/B07_DesignDetail/B07_DesignDetail_UI_Settings_Scale.ts
T
eomsangdonandClaude Sonnet 5.5 835f9d9290 refactor(B07): 상세 설계 화면 페이지 자체 변종을 부품 상자 부품으로 — 도면 단추 · 장 번호 · 도구 단추 · 카드 · 띠 알림 · 값 목록 (PLAN 553-1)
- 도면 단추(날 ui-panel-btn) · 장 번호 · 도구 단추 = 단추 부품 · ⚙ = 아이콘 단추 꼴 · 평면도 줄 = 목록 줄 꼴
- 머리 = 패널 머리 + 배지 · 확정 건너뜀 = 띠 알림 · 빈 글 = 빈 상태 · 측점 정보 = 값 목록 · ⚙ 모달 그룹 상자 = 카드 · 종류 이름 칸 = 입력
- window.confirm 둘 → 확인창 부품 · 죽은 CSS 걷음 · 시험 헬퍼 부품 가짜 한 벌(helper_b07_part_stubs) · 153-1 옛 오류(removeAttribute) 고침 · 가이드 선택자 · 지킴 시험

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 22:29:22 +09:00

181 lines
6.9 KiB
TypeScript

/* =============================================================================
* B07_DesignDetail_UI_Settings_Scale.ts
* ⚙ 모달 X · Y 축척 칸(485-2 · 요구 280-5) — 종단 · 횡단 · 토적 · 전개도만.
* 종단 = 셋째 칸 「평면도」(521-2 · 우측 상단 평면 위치도) — 칸은 서버 기본값 열쇠 그대로.
*
* 칸 = 분모 정수(늘 지정값 · 자동 없음 523-1) · 범위 밖 = 칸 붉게 + 모달 [확인] 꺼짐
* 저장 = 모달 [확인](`_UI_Settings`)이 `save` 를 부름 — 그 종류 저장본이 있으면 n 장 물음(취소 = 그대로) → PUT
* [기본값] = 칸만 기본으로 채움 — 저장은 [확인]
* `settingsGroup` = 모달 안 그룹 상자(523-1) — 횡단 배치 · 표도 같은 상자
* ========================================================================== */
import {
createCard,
createInputField,
hideLoadingOverlay,
showConfirmDialog,
showLoadingOverlay,
showToast,
} from "@ui/ui_template_elements";
import {
type DrawingScaleAxes,
type DrawingScaleKind,
type DrawingScales,
type DrawingScaleSend,
fetchDrawingScales,
saveDrawingScales,
} from "./B07_DesignDetail_Api_Fetch";
import { createText } from "@ui/ui_template_text";
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: "전개도",
};
type Axis = keyof DrawingScaleAxes;
const AXIS_LABEL: Record<Axis, string> = { x: "X(가로) 1/", y: "Y(세로) 1/", plan: "평면도 1/" };
const isScaleKind = (kind: string): kind is DrawingScaleKind => kind in LABEL;
/** 모달 안 그룹 상자 — 카드 부품(제목 + 칸). 나중에 넣을 칸은 `groupBody(box)` 에. */
export function settingsGroup(legend: string, ...children: HTMLElement[]): HTMLElement {
const box = createCard({ title: legend, body: children, pad: 12 });
box.classList.add("b07-settings__group");
return box;
}
/** 그룹 상자의 칸 자리 — 카드 부품의 마지막 자식 */
export function groupBody(box: HTMLElement): HTMLElement {
return (box.children[box.children.length - 1] as HTMLElement | undefined) ?? box;
}
/** 축척 칸 한 벌 — 모달 [확인] · [기본값] 이 씀. */
export interface ScaleFields {
/** null = 그대로(못 받음 포함) · "invalid" = 붉은 칸 있음 · 그 밖 = 보낼 값 */
read: () => DrawingScaleSend | null | "invalid";
reset: () => void;
/** 저장본 물음 → PUT — 취소 · 실패면 거짓(모달 머묾) */
save: (next: DrawingScaleSend) => Promise<boolean>;
}
/** ⚙ 모달 몸에 축척 그룹을 붙임 — 축척 없는 종류는 undefined · `onCheck` = 칸 바뀜(모달 [확인] 켜고 끔). */
export function mountDrawingScale(
body: HTMLElement,
kind: string,
deps: DrawingSettingsDeps,
onCheck: () => void,
): ScaleFields | undefined {
if (!isScaleKind(kind) || !deps.plan) return undefined;
const { projectId } = deps.plan;
const status = createText("축척 받는 중…", "muted", 14);
status.classList.add("b07-settings__note");
const section = settingsGroup(
kind === "cross" ? "축척 — 횡단도 장(표준 횡단면도 뺌)" : `축척 — ${LABEL[kind]}`,
status,
);
section.classList.add("b07-scale");
body.append(section);
let built: Omit<ScaleFields, "save"> | undefined;
let data: DrawingScales | undefined;
void fetchDrawingScales(projectId)
.then((got) => {
data = got;
status.remove();
built = buildScaleFields(section, kind, got, onCheck);
onCheck();
})
.catch(() => {
status.textContent = "축척을 받지 못했습니다.";
});
return {
read: () => built?.read() ?? null,
reset: () => built?.reset(),
save: async (next) => {
const saved = data?.saved[kind] ?? 0;
const ask = `${LABEL[kind]} 축척을 바꾸면 저장본 ${saved}장이 풀립니다(원본에서 다시 그림). 바꿀까요?`;
if (saved > 0 && !(await showConfirmDialog(ask, "바꿈"))) return false;
showLoadingOverlay();
try {
await saveDrawingScales(projectId, { [kind]: next });
showToast(`${LABEL[kind]} 축척을 바꿨습니다.`, "success");
return true;
} catch (error) {
showToast(error instanceof Error ? error.message : "축척을 저장하지 못했습니다.", "error");
return false;
} finally {
hideLoadingOverlay();
}
},
};
}
function buildScaleFields(
section: HTMLElement,
kind: DrawingScaleKind,
data: DrawingScales,
onCheck: () => void,
): Omit<ScaleFields, "save"> {
const axes = Object.keys(data.defaults[kind]) as Axis[];
const fields = axes.map((axis) => {
const fallback = data.defaults[kind][axis] as number;
const range = data.ranges[kind][axis] ?? [1, 1];
const [min, max] = range;
const field = createInputField({
label: AXIS_LABEL[axis],
type: "number",
compact: true,
min,
max,
});
field.root.classList.add("b07-scale__field");
const set = (value: number): void => {
field.input.value = String(value);
};
set(data.scales[kind][axis] ?? fallback);
const read = () => readScaleAxis(field.input.value, range);
return { axis, field, fallback, set, read };
});
// 칸 밑 오류 글은 숨기고(틀린 칸만 길어짐) 줄 아래 한 줄로 알림
const alertLine = createText("", "error");
alertLine.classList.add("b07-scale__error");
alertLine.hidden = true;
const check = (): void => {
const errors = new Map<string, string[]>();
for (const f of fields) {
const result = f.read();
const message = typeof result === "string" ? result : undefined;
f.field.setError(message);
const name = f.axis === "plan" ? "평면도" : f.axis.toUpperCase();
if (message) errors.set(message, [...(errors.get(message) ?? []), name]);
}
alertLine.textContent = [...errors].map(([m, axes]) => `${axes.join(" · ")}: ${m}`).join(" / ");
alertLine.hidden = errors.size === 0;
onCheck();
};
for (const f of fields) f.field.input.addEventListener("input", check);
const row = document.createElement("div");
row.className = "b07-scale__row";
row.append(...fields.map((f) => f.field.root));
groupBody(section).append(row, alertLine);
return {
read: () => {
const read = fields.map((f) => [f, f.read()] as const);
if (read.some(([, value]) => typeof value === "string")) return "invalid";
const now = data.scales[kind];
if (read.every(([f, value]) => value === (now[f.axis] ?? f.fallback))) return null;
return Object.fromEntries(
read.map(([f, v]) => [f.axis, scaleToSend(v as number, f.fallback)]),
) as DrawingScaleSend;
},
reset: () => {
fields.forEach((f) => f.set(f.fallback));
check();
},
};
}