Merge remote-tracking branches 'origin/sub_laptop_3' and 'origin/sub_laptop_6' into main_laptop_1
This commit is contained in:
@@ -243,8 +243,10 @@ export function saveCrossLayout(projectId: string, layout: CrossLayout): Promise
|
||||
|
||||
/** 도면 축척 종류(485) — 표준 횡단 · 평면도 · 구조물은 뺌. */
|
||||
export type DrawingScaleKind = "longitudinal" | "cross" | "mass_haul" | "development";
|
||||
/** 축척 분모 — null = 자동(토적 가로 · 전개도 · 종단 평면 위치도) · 범위 = [최소, 최대]. */
|
||||
export type DrawingScaleAxes = { x: number | null; y: number | null; plan?: number | null };
|
||||
/** 축척 분모 — 늘 지정값(자동 없음 · 523-1) · 범위 = [최소, 최대]. */
|
||||
export type DrawingScaleAxes = { x: number; y: number; plan?: number };
|
||||
/** 보낼 축척 — null = 기본. */
|
||||
export type DrawingScaleSend = { [K in keyof DrawingScaleAxes]: number | null };
|
||||
export interface DrawingScales {
|
||||
scales: Record<DrawingScaleKind, DrawingScaleAxes>;
|
||||
defaults: Record<DrawingScaleKind, DrawingScaleAxes>;
|
||||
@@ -265,7 +267,7 @@ export function fetchDrawingScales(projectId: string): Promise<DrawingScales> {
|
||||
/** 축척 저장 — 보낸 종류만 바꿈 · 값이 바뀐 종류의 저장본은 서버가 풂. */
|
||||
export function saveDrawingScales(
|
||||
projectId: string,
|
||||
scales: Partial<Record<DrawingScaleKind, DrawingScaleAxes>>,
|
||||
scales: Partial<Record<DrawingScaleKind, DrawingScaleSend>>,
|
||||
): Promise<DrawingScales> {
|
||||
return requestJson(`/projects/${projectId}/drawing-scales`, {
|
||||
method: "PUT",
|
||||
|
||||
@@ -32,6 +32,7 @@ from config.config_system import (
|
||||
DRAWING_SCALE_CROSS,
|
||||
DRAWING_SCALE_LONG_H,
|
||||
DRAWING_SCALE_LONG_V,
|
||||
DRAWING_SCALE_MASSHAUL_H,
|
||||
DRAWING_SCALE_MASSHAUL_V_M3_MM,
|
||||
)
|
||||
from M02_MasterTemplete.M02_Template_Layers import (
|
||||
@@ -93,16 +94,17 @@ def add_title_fields(extra: dict[str, str]) -> None:
|
||||
|
||||
|
||||
# 도면 축척(485) — 프로젝트마다 `{B07 폴더}/drawing_scales.json` `{종류: {x, y}}`(분모).
|
||||
# 칸이 없거나 어긋나면 기본(`config_system`) · None = 자동(토적 가로 · 전개도 가로 · 세로).
|
||||
# 칸이 없거나 어긋나면(옛 null 포함) 기본(`config_system`) — 기본은 모두 지정값(자동 없음 · 523-1).
|
||||
# 읽는 프로젝트 = 위 문맥 변수(라우터가 세움) — 엔진 서명을 고치지 않는다.
|
||||
DRAWING_SCALES_FILE = "drawing_scales.json"
|
||||
DRAWING_SCALE_DEFAULTS: dict[str, dict[str, int | None]] = {
|
||||
# plan = 종단 장 우측 상단 평면 위치도(521-2) · None = 자동(노선을 칸에 맞춤)
|
||||
"longitudinal": {"x": DRAWING_SCALE_LONG_H, "y": DRAWING_SCALE_LONG_V, "plan": None},
|
||||
DRAWING_SCALE_DEFAULTS: dict[str, dict[str, int]] = {
|
||||
# plan = 종단 장 우측 상단 평면 위치도(521-2)
|
||||
"longitudinal": {"x": DRAWING_SCALE_LONG_H, "y": DRAWING_SCALE_LONG_V, "plan": 5000},
|
||||
"cross": {"x": DRAWING_SCALE_CROSS, "y": DRAWING_SCALE_CROSS},
|
||||
# 토적 세로 = 1 mm 당 50 ㎥ → 표기 1:50,000 (분모 / 1000 = 종이 1 mm 의 ㎥)
|
||||
"mass_haul": {"x": None, "y": int(DRAWING_SCALE_MASSHAUL_V_M3_MM * 1000)},
|
||||
"development": {"x": None, "y": None},
|
||||
"mass_haul": {"x": DRAWING_SCALE_MASSHAUL_H, "y": int(DRAWING_SCALE_MASSHAUL_V_M3_MM * 1000)},
|
||||
# 전개도 = 옛 자동이 흔히 고르던 1/50(가로 · 세로 같음)
|
||||
"development": {"x": 50, "y": 50},
|
||||
}
|
||||
|
||||
|
||||
@@ -110,7 +112,7 @@ def drawing_scales_path(project_root: Path) -> Path:
|
||||
return _design_root(project_root) / DRAWING_SCALES_FILE
|
||||
|
||||
|
||||
def read_drawing_scales(project_root: Path | None) -> dict[str, dict[str, int | None]]:
|
||||
def read_drawing_scales(project_root: Path | None) -> dict[str, dict[str, int]]:
|
||||
"""종류마다 {x, y} — 파일에 양의 정수가 있으면 그것, 아니면 기본."""
|
||||
# ponytail: 부를 때마다 작은 파일을 읽음(수정 시각 캐시는 같은 시각 두 번 쓰기에 낡음)
|
||||
stored: Any = {}
|
||||
@@ -121,7 +123,7 @@ def read_drawing_scales(project_root: Path | None) -> dict[str, dict[str, int |
|
||||
stored = {}
|
||||
if not isinstance(stored, dict):
|
||||
stored = {}
|
||||
scales: dict[str, dict[str, int | None]] = {}
|
||||
scales: dict[str, dict[str, int]] = {}
|
||||
for kind, defaults in DRAWING_SCALE_DEFAULTS.items():
|
||||
row = stored.get(kind) if isinstance(stored.get(kind), dict) else {}
|
||||
scales[kind] = {
|
||||
@@ -133,8 +135,8 @@ def read_drawing_scales(project_root: Path | None) -> dict[str, dict[str, int |
|
||||
return scales
|
||||
|
||||
|
||||
def drawing_scale(kind: str) -> dict[str, int | None]:
|
||||
"""이 요청 프로젝트의 그 종류 축척 {x, y}(분모 · None = 자동)."""
|
||||
def drawing_scale(kind: str) -> dict[str, int]:
|
||||
"""이 요청 프로젝트의 그 종류 축척 {x, y}(분모)."""
|
||||
return read_drawing_scales(_project_root.get())[kind]
|
||||
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ from config.config_db import get_db_pool
|
||||
|
||||
router = APIRouter(prefix="/api/projects", tags=["B07 Design Detail"])
|
||||
|
||||
# 분모 범위(정수) — 기본 · 자동 후보(토적 500~6,000 · 전개도 50~5,000)를 모두 담음.
|
||||
# 분모 범위(정수) — 기본(`DRAWING_SCALE_DEFAULTS`) · 도면 관행 축척(토적 500~6,000 · 전개도 50~5,000)을 모두 담음.
|
||||
SCALE_RANGES: dict[str, dict[str, tuple[int, int]]] = {
|
||||
"longitudinal": {"x": (100, 10000), "y": (10, 2000), "plan": (500, 50000)},
|
||||
"cross": {"x": (10, 1000), "y": (10, 1000)},
|
||||
@@ -50,7 +50,7 @@ _KIND_LABEL = {
|
||||
|
||||
|
||||
def checked_scales(scales: Any) -> dict[str, dict[str, int | None]]:
|
||||
"""{종류: {x, y}} — 분모 정수 · 범위 안 · null = 기본(자동 칸은 자동)."""
|
||||
"""{종류: {x, y}} — 분모 정수 · 범위 안 · null = 기본."""
|
||||
if not isinstance(scales, dict):
|
||||
raise ValueError("축척은 {종류: {x, y}} 꼴로 보내 주세요.")
|
||||
checked: dict[str, dict[str, int | None]] = {}
|
||||
@@ -138,7 +138,7 @@ def write_drawing_scales(
|
||||
|
||||
|
||||
class DrawingScalesRequest(BaseModel):
|
||||
"""바꿀 종류만 — {종류: {x, y}} · 분모 정수 · null = 기본(자동)."""
|
||||
"""바꿀 종류만 — {종류: {x, y}} · 분모 정수 · null = 기본."""
|
||||
|
||||
scales: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { attachCollapsible, createPanelCard } from "@ui/ui_template_collapsible";
|
||||
import { createButton, createInputField, showToast } from "@ui/ui_template_elements";
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
import type { CrossDesignInfo, DesignDrawingItem } from "./B07_DesignDetail_Api_Fetch";
|
||||
import { buildDevelopmentSection } from "./B07_DesignDetail_UI_Development";
|
||||
@@ -236,99 +235,6 @@ export function isCrossSheet(drawing: DesignDrawingItem): boolean {
|
||||
return drawing.kind === "cross" && drawing.id.startsWith("cross_s");
|
||||
}
|
||||
|
||||
/** 표 · 글 배율 범위(180-2) — 서버 `CROSS_SCALE_RANGE` 와 같음. */
|
||||
const CROSS_SCALE_MIN = 0.5;
|
||||
const CROSS_SCALE_MAX = 2;
|
||||
|
||||
/** 배치 칸 줄 뒤 덧칸 — 표 배율(180-2). */
|
||||
export interface CrossLayoutExtras {
|
||||
scale?: { value: number; onApply: (scale: number) => void };
|
||||
}
|
||||
|
||||
/**
|
||||
* 횡단 장 배치 칸(140-3 · 153-1) — 줄마다 한 일(212-3):
|
||||
* 「배치 열 [4] 행 [4]」 · 「[이 장] [모든 장]」 · 「표 배율 [1] [배율 적용]」 · (부르는 쪽이 붙이는 [안내선 숨기기]).
|
||||
* 단추 · 숫자 칸 = 줄 위 작은 단추 한 규격(`--btn-bar-*`) · `onApply` 가 없으면(장이 아닌 도면) [이 장]을 끔.
|
||||
*/
|
||||
export function buildCrossLayoutControls(
|
||||
grid: [number, number],
|
||||
onApply: ((grid: [number, number]) => void) | null,
|
||||
onAllDefault: (grid: [number, number]) => void,
|
||||
extras: CrossLayoutExtras = {},
|
||||
): HTMLElement {
|
||||
const box = document.createElement("div");
|
||||
box.className = "b07-cross-layout";
|
||||
const row = (...items: HTMLElement[]): HTMLElement => {
|
||||
const line = document.createElement("div");
|
||||
line.className = "b07-cross-layout__row";
|
||||
line.append(...items);
|
||||
return line;
|
||||
};
|
||||
const number = (value: number, label: string) =>
|
||||
createInputField({
|
||||
label,
|
||||
type: "number",
|
||||
value: String(value),
|
||||
min: 1,
|
||||
max: 10,
|
||||
compact: true,
|
||||
});
|
||||
const cols = number(grid[0], "배치 열");
|
||||
const rows = number(grid[1], "행");
|
||||
const read = (): [number, number] | null => {
|
||||
const value: [number, number] = [Number(cols.input.value), Number(rows.input.value)];
|
||||
return value.every((n) => Number.isInteger(n) && n >= 1 && n <= 10) ? value : null;
|
||||
};
|
||||
const action = (label: string, title: string, run: ((grid: [number, number]) => void) | null) => {
|
||||
const button = createButton({
|
||||
label,
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
disabled: !run,
|
||||
onClick: () => {
|
||||
const value = read();
|
||||
if (value) run?.(value);
|
||||
else showToast("열 · 행은 1~10 사이 정수로 적어 주세요.", "error");
|
||||
},
|
||||
});
|
||||
button.title = title;
|
||||
return button;
|
||||
};
|
||||
box.append(
|
||||
row(cols.root, rows.root),
|
||||
row(
|
||||
action("이 장", "이 장만 이 배치로", onApply),
|
||||
action("모든 장", "모든 장을 이 배치로(장마다 값 지움)", onAllDefault),
|
||||
),
|
||||
);
|
||||
if (extras.scale) {
|
||||
const { onApply: applyScale } = extras.scale;
|
||||
const scale = createInputField({
|
||||
label: "표 배율",
|
||||
type: "number",
|
||||
value: String(extras.scale.value),
|
||||
min: CROSS_SCALE_MIN,
|
||||
max: CROSS_SCALE_MAX,
|
||||
compact: true,
|
||||
});
|
||||
scale.input.setAttribute("step", "0.1");
|
||||
const button = createButton({
|
||||
label: "배율 적용",
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => {
|
||||
const value = Number(scale.input.value);
|
||||
if (value >= CROSS_SCALE_MIN && value <= CROSS_SCALE_MAX) applyScale(value);
|
||||
else
|
||||
showToast(`표 배율은 ${CROSS_SCALE_MIN}~${CROSS_SCALE_MAX} 사이로 적어 주세요.`, "error");
|
||||
},
|
||||
});
|
||||
button.title = "모든 장의 표 · 표 위 글 크기(기본 1)";
|
||||
box.append(row(scale.root, button));
|
||||
}
|
||||
return box;
|
||||
}
|
||||
|
||||
/** 측구 규격 표시 문자열 (design 신구조: 형식별 ditch spec, F-2 호환). */
|
||||
function ditchLabel(design: CrossDesignInfo): string {
|
||||
const ditch = design.ditch;
|
||||
|
||||
@@ -3,21 +3,19 @@
|
||||
* 좌측 도면 묶음 ⚙ 설정 모달(요구 280-5) — 공용 `openModal` · 머리 [원본에서 다시 그리기](281).
|
||||
*
|
||||
* 평면도 = 평면도 관리 모달 그대로(`openPlanManager`)
|
||||
* 횡단도 = 장 배치(열 · 행 · [이 장] · [모든 장]) · 표 배율 · [안내선 숨기기] — 좌측 카드 밑에서 옮김
|
||||
* 그 밖 = 그룹 상자(523-1) + 맨 아래 단추 줄 하나 — [안내선 숨기기](횡단) · [기본값](축척) · [취소] · [확인]
|
||||
* 횡단도 = 「장 배치」(열 · 행 · 적용 이 장 / 모든 장) · 「표」(표 배율) · 축척
|
||||
* 구조물 도면 = 「M02 그림 — 축척 없음」 한 줄 · 종단 · 횡단 · 토적 · 전개도 = X · Y 축척 칸(`_UI_Settings_Scale`)
|
||||
* [확인] = 바뀐 것만 저장(축척 → 배치) 뒤 닫고 다시 그림 · 바뀐 것 없으면 닫기만 · 붉은 칸 있으면 꺼짐
|
||||
* [원본에서 다시 그리기] = 지금 연 도면이 그 묶음일 때만 켬
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import { createButton, createInputField, createSelectField } from "@ui/ui_template_elements";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import type { CrossLayout, DesignDrawingItem } from "./B07_DesignDetail_Api_Fetch";
|
||||
import {
|
||||
buildCrossLayoutControls,
|
||||
DRAWING_GROUPS,
|
||||
isCrossSheet,
|
||||
} from "./B07_DesignDetail_UI_Panels";
|
||||
import { DRAWING_GROUPS, isCrossSheet } from "./B07_DesignDetail_UI_Panels";
|
||||
import { openPlanManager, type PlanManageControls } from "./B07_DesignDetail_UI_PlanManage";
|
||||
import { mountDrawingScale } from "./B07_DesignDetail_UI_Settings_Scale";
|
||||
import { mountDrawingScale, settingsGroup } from "./B07_DesignDetail_UI_Settings_Scale";
|
||||
|
||||
type Kind = DesignDrawingItem["kind"];
|
||||
|
||||
@@ -40,6 +38,10 @@ export interface DrawingSettingsDeps {
|
||||
/** ⚙ 묶음에 드는 도면 종류 — 횡단도 카드엔 표준 횡단면도 줄이 같이 있음. */
|
||||
const memberOf = (kind: Kind): Kind[] => (kind === "cross" ? ["cross", "cross_standard"] : [kind]);
|
||||
|
||||
/** 표 · 글 배율 범위(180-2) — 서버 `CROSS_SCALE_RANGE` 와 같음. */
|
||||
const CROSS_SCALE_MIN = 0.5;
|
||||
const CROSS_SCALE_MAX = 2;
|
||||
|
||||
const note = (text: string): HTMLElement => {
|
||||
const line = document.createElement("p");
|
||||
line.className = "b07-settings__note";
|
||||
@@ -81,47 +83,127 @@ export function openDrawingSettings(kind: Kind, deps: DrawingSettingsDeps): void
|
||||
mount: (body, close) => {
|
||||
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);
|
||||
if (kind === "structure") body.append(settingsGroup("축척", note("M02 그림 — 축척 없음")));
|
||||
const confirm = createButton({ label: "확인", onClick: () => void apply() });
|
||||
const refresh = (): void => {
|
||||
confirm.disabled = scale?.read() === "invalid" || cross?.read() === "invalid";
|
||||
};
|
||||
const cross = kind === "cross" ? mountCross(body, deps, refresh) : undefined;
|
||||
const scale = mountDrawingScale(body, kind, deps, refresh);
|
||||
|
||||
const apply = async (): Promise<void> => {
|
||||
const nextScale = scale?.read() ?? null;
|
||||
const nextLayout = cross?.read() ?? null;
|
||||
if (nextScale === "invalid" || nextLayout === "invalid") return;
|
||||
if (nextScale && !(await scale!.save(nextScale))) return;
|
||||
close();
|
||||
if (nextLayout) deps.cross.save(nextLayout, cross!.sheet);
|
||||
else if (nextScale) deps.plan?.onClosed(true, deps.current()?.id); // 목록 · 도면 다시 받음
|
||||
};
|
||||
const cancel = createButton({ label: "취소", variant: "ghost", onClick: close });
|
||||
cancel.classList.add("b07-settings__cancel");
|
||||
const footer = document.createElement("div");
|
||||
footer.className = "b07-settings__actions";
|
||||
if (cross) footer.append(deps.cross.guideToggle);
|
||||
if (scale) {
|
||||
const reset = createButton({ label: "기본값", variant: "ghost", onClick: scale.reset });
|
||||
reset.title = "축척 칸을 기본으로 채움 — [확인]으로 씀";
|
||||
footer.append(reset);
|
||||
}
|
||||
footer.append(cancel, confirm);
|
||||
body.append(footer);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 횡단 모달 몸 — 배치 칸은 받은 뒤 · 안내선 단추는 먼저(배치를 못 받아도 남음). */
|
||||
function mountCross(body: HTMLElement, deps: DrawingSettingsDeps, close: () => void): void {
|
||||
interface CrossFields {
|
||||
sheet: number;
|
||||
/** null = 그대로(못 받음 포함) · "invalid" = 붉은 칸 · 그 밖 = 저장할 배치 */
|
||||
read: () => CrossLayout | null | "invalid";
|
||||
}
|
||||
|
||||
/** 횡단 「장 배치」 · 「표」 그룹 — 칸은 배치를 받은 뒤 채움. */
|
||||
function mountCross(
|
||||
body: HTMLElement,
|
||||
deps: DrawingSettingsDeps,
|
||||
onCheck: () => void,
|
||||
): CrossFields {
|
||||
const current = deps.current();
|
||||
const sheet =
|
||||
current && isCrossSheet(current)
|
||||
? deps.drawings.filter(isCrossSheet).findIndex((item) => item.id === current.id) + 1
|
||||
: 0;
|
||||
body.classList.add("ui-panel-form"); // 칸 · 단추 꼴 = 좌측 패널 때와 같음(269)
|
||||
const holder = document.createElement("div");
|
||||
holder.className = "b07-cross-layout";
|
||||
holder.append(deps.cross.guideToggle);
|
||||
body.append(holder);
|
||||
const number = (label: string, min: number, max: number) => {
|
||||
const field = createInputField({ label, type: "number", min, max, compact: true });
|
||||
field.input.disabled = true; // 받기 전
|
||||
field.input.addEventListener("input", onCheck);
|
||||
return field;
|
||||
};
|
||||
const cols = number("배치 열", 1, 10);
|
||||
const rows = number("행", 1, 10);
|
||||
const scope = createSelectField({
|
||||
label: "적용",
|
||||
compact: true,
|
||||
options: [
|
||||
...(sheet ? [{ value: "sheet", text: "이 장만" }] : []),
|
||||
{ value: "all", text: "모든 장" },
|
||||
],
|
||||
value: sheet ? "sheet" : "all",
|
||||
});
|
||||
scope.select.title = "이 장만 = 지금 장 · 모든 장 = 모든 장을 이 배치로(장마다 값 지움)";
|
||||
const table = number("표 배율", CROSS_SCALE_MIN, CROSS_SCALE_MAX);
|
||||
table.input.setAttribute("step", "0.1");
|
||||
table.root.title = "모든 장의 표 · 표 위 글 크기(기본 1)";
|
||||
const line = (...items: HTMLElement[]): HTMLElement => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "b07-cross-layout__row";
|
||||
row.append(...items);
|
||||
return row;
|
||||
};
|
||||
const layoutBox = settingsGroup("장 배치", line(cols.root, rows.root), line(scope.root));
|
||||
const tableBox = settingsGroup("표", line(table.root));
|
||||
layoutBox.classList.add("b07-cross-layout");
|
||||
tableBox.classList.add("b07-cross-layout");
|
||||
body.append(layoutBox, tableBox);
|
||||
|
||||
let layout: CrossLayout | undefined;
|
||||
let shown: [number, number] = [0, 0];
|
||||
void deps.cross
|
||||
.layout()
|
||||
.then((layout) => {
|
||||
const apply = (next: CrossLayout): void => {
|
||||
close();
|
||||
deps.cross.save(next, sheet);
|
||||
};
|
||||
const controls = buildCrossLayoutControls(
|
||||
(sheet && layout.sheets[String(sheet)]) || layout.default,
|
||||
sheet
|
||||
? (grid) => apply({ ...layout, sheets: { ...layout.sheets, [String(sheet)]: grid } })
|
||||
: null,
|
||||
(grid) => apply({ ...layout, default: grid, sheets: {} }),
|
||||
{
|
||||
scale: {
|
||||
value: layout.table_scale ?? 1,
|
||||
onApply: (scale) => apply({ ...layout, table_scale: scale }),
|
||||
},
|
||||
},
|
||||
);
|
||||
controls.append(deps.cross.guideToggle);
|
||||
holder.replaceWith(controls);
|
||||
.then((got) => {
|
||||
layout = got;
|
||||
shown = (sheet && got.sheets[String(sheet)]) || got.default;
|
||||
cols.input.value = String(shown[0]);
|
||||
rows.input.value = String(shown[1]);
|
||||
table.input.value = String(got.table_scale ?? 1);
|
||||
for (const field of [cols, rows, table]) field.input.disabled = false;
|
||||
})
|
||||
.catch(() => holder.prepend(note("배치를 받지 못했습니다.")));
|
||||
.catch(() => layoutBox.append(note("배치를 받지 못했습니다.")));
|
||||
|
||||
return {
|
||||
sheet,
|
||||
read: () => {
|
||||
if (!layout) return null;
|
||||
const grid: [number, number] = [Number(cols.input.value), Number(rows.input.value)];
|
||||
const tableScale = Number(table.input.value);
|
||||
const badGrid = !grid.every((n) => Number.isInteger(n) && n >= 1 && n <= 10);
|
||||
const badTable = !(tableScale >= CROSS_SCALE_MIN && tableScale <= CROSS_SCALE_MAX);
|
||||
cols.setError(badGrid ? "1~10 정수" : undefined);
|
||||
rows.setError(badGrid ? "1~10 정수" : undefined);
|
||||
table.setError(badTable ? `${CROSS_SCALE_MIN}~${CROSS_SCALE_MAX}` : undefined);
|
||||
if (badGrid || badTable) return "invalid";
|
||||
let next = layout;
|
||||
const touched =
|
||||
grid[0] !== shown[0] ||
|
||||
grid[1] !== shown[1] ||
|
||||
scope.select.value !== (sheet ? "sheet" : "all");
|
||||
if (touched && scope.select.value === "sheet") {
|
||||
next = { ...next, sheets: { ...next.sheets, [String(sheet)]: grid } };
|
||||
} else if (touched) {
|
||||
next = { ...next, default: grid, sheets: {} };
|
||||
}
|
||||
if (tableScale !== (layout.table_scale ?? 1)) next = { ...next, table_scale: tableScale };
|
||||
return next === layout ? null : next;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,14 +3,13 @@
|
||||
* ⚙ 모달 X · Y 축척 칸(485-2 · 요구 280-5) — 종단 · 횡단 · 토적 · 전개도만.
|
||||
* 종단 = 셋째 칸 「평면도」(521-2 · 우측 상단 평면 위치도) — 칸은 서버 기본값 열쇠 그대로.
|
||||
*
|
||||
* 칸 = 분모 정수 · 범위 밖 = 칸 붉게 + [축척 저장] 꺼짐 · 기본이 자동인 칸(토적 X · 전개도) = 「자동」
|
||||
* [축척 저장] = 그 종류 저장본이 있으면 n 장 물음(취소 = 그대로) → PUT → 화면 다시 세움(도면 다시 그림)
|
||||
* [기본값] = 칸만 기본으로 채움 — 저장은 [축척 저장]
|
||||
* 칸 = 분모 정수(늘 지정값 · 자동 없음 523-1) · 범위 밖 = 칸 붉게 + 모달 [확인] 꺼짐
|
||||
* 저장 = 모달 [확인](`_UI_Settings`)이 `save` 를 부름 — 그 종류 저장본이 있으면 n 장 물음(취소 = 그대로) → PUT
|
||||
* [기본값] = 칸만 기본으로 채움 — 저장은 [확인]
|
||||
* `settingsGroup` = 모달 안 그룹 상자(523-1) — 횡단 배치 · 표도 같은 상자
|
||||
* ========================================================================== */
|
||||
|
||||
import { createCheckbox } from "@ui/ui_template_checkbox";
|
||||
import {
|
||||
createButton,
|
||||
createInputField,
|
||||
hideLoadingOverlay,
|
||||
showConfirmDialog,
|
||||
@@ -21,6 +20,7 @@ import {
|
||||
type DrawingScaleAxes,
|
||||
type DrawingScaleKind,
|
||||
type DrawingScales,
|
||||
type DrawingScaleSend,
|
||||
fetchDrawingScales,
|
||||
saveDrawingScales,
|
||||
} from "./B07_DesignDetail_Api_Fetch";
|
||||
@@ -38,86 +38,104 @@ const AXIS_LABEL: Record<Axis, string> = { x: "X(가로) 1/", y: "Y(세로) 1/",
|
||||
|
||||
const isScaleKind = (kind: string): kind is DrawingScaleKind => kind in LABEL;
|
||||
|
||||
/** ⚙ 모달 몸에 축척 칸을 붙임 — 축척 없는 종류는 아무것도 안 함. */
|
||||
/** 모달 안 그룹 상자 — 제목(legend) + 칸. */
|
||||
export function settingsGroup(legend: string, ...children: HTMLElement[]): HTMLFieldSetElement {
|
||||
const box = document.createElement("fieldset");
|
||||
box.className = "b07-settings__group";
|
||||
const title = document.createElement("legend");
|
||||
title.textContent = legend;
|
||||
box.append(title, ...children);
|
||||
return 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,
|
||||
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 = "축척 받는 중…";
|
||||
onCheck: () => void,
|
||||
): ScaleFields | undefined {
|
||||
if (!isScaleKind(kind) || !deps.plan) return undefined;
|
||||
const { projectId } = deps.plan;
|
||||
const status = document.createElement("p");
|
||||
status.className = "b07-settings__note";
|
||||
status.textContent = "축척 받는 중…";
|
||||
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((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();
|
||||
}
|
||||
});
|
||||
.then((got) => {
|
||||
data = got;
|
||||
status.remove();
|
||||
built = buildScaleFields(section, kind, got, onCheck);
|
||||
onCheck();
|
||||
})
|
||||
.catch(() => {
|
||||
section.textContent = "축척을 받지 못했습니다.";
|
||||
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,
|
||||
save: (next: DrawingScaleAxes) => Promise<void>,
|
||||
): void {
|
||||
onCheck: () => void,
|
||||
): Omit<ScaleFields, "save"> {
|
||||
const axes = Object.keys(data.defaults[kind]) as Axis[];
|
||||
const fields = axes.map((axis) => {
|
||||
const label = AXIS_LABEL[axis];
|
||||
const fallback = data.defaults[kind][axis] ?? null;
|
||||
const fallback = data.defaults[kind][axis] as number;
|
||||
const range = data.ranges[kind][axis] ?? [1, 1];
|
||||
const [min, max] = range;
|
||||
const field = createInputField({ label, type: "number", compact: true, min, max });
|
||||
const field = createInputField({
|
||||
label: AXIS_LABEL[axis],
|
||||
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;
|
||||
const set = (value: number): void => {
|
||||
field.input.value = String(value);
|
||||
};
|
||||
set(data.scales[kind][axis] ?? null);
|
||||
const read = () => readScaleAxis(field.input.value, !!auto?.input.checked, range);
|
||||
return { axis, field, auto, fallback, set, read };
|
||||
set(data.scales[kind][axis] ?? fallback);
|
||||
const read = () => readScaleAxis(field.input.value, range);
|
||||
return { axis, field, fallback, set, read };
|
||||
});
|
||||
|
||||
const saveButton = createButton({
|
||||
label: "축척 저장",
|
||||
onClick: () => {
|
||||
const read = fields.map((f) => [f, f.read() as number | null] as const);
|
||||
const now = data.scales[kind];
|
||||
if (read.every(([f, value]) => value === (now[f.axis] ?? null))) {
|
||||
showToast("바뀐 축척이 없습니다.", "info");
|
||||
return;
|
||||
}
|
||||
const next = Object.fromEntries(read.map(([f, v]) => [f.axis, scaleToSend(v, f.fallback)]));
|
||||
void save(next as DrawingScaleAxes);
|
||||
},
|
||||
});
|
||||
// 칸 밑 오류 글은 숨기고(틀린 칸만 길어짐) 줄 아래 한 줄로 알림
|
||||
const alertLine = document.createElement("p");
|
||||
alertLine.className = "b07-scale__error";
|
||||
@@ -134,35 +152,27 @@ function buildScaleFields(
|
||||
}
|
||||
alertLine.textContent = [...errors].map(([m, axes]) => `${axes.join(" · ")}: ${m}`).join(" / ");
|
||||
alertLine.hidden = errors.size === 0;
|
||||
saveButton.disabled = errors.size > 0;
|
||||
onCheck();
|
||||
};
|
||||
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 = "칸을 기본 축척으로 채움 — [축척 저장]으로 씀";
|
||||
for (const f of fields) f.field.input.addEventListener("input", check);
|
||||
|
||||
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, alertLine, buttons);
|
||||
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();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -214,8 +214,8 @@
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* 횡단 장 배치(153-1 · 212-3 · 280-5 ⚙ 횡단 모달) — 한 줄에 한 일:
|
||||
배치 열 · 행 / [이 장] [모든 장] / 표 배율 [배율 적용] / [안내선 숨기기] · 한 줄 안 같은 너비. */
|
||||
/* 횡단 장 배치(153-1 · 280-5 ⚙ 횡단 모달 · 523-1 그룹 상자) — 한 줄에 한 일:
|
||||
「장 배치」 배치 열 · 행 / 적용 · 「표」 표 배율 · 한 줄 안 같은 너비. */
|
||||
.b07-cross-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -453,27 +453,71 @@ html.is-bm-route .b07-cad-stack > .cad-tabs {
|
||||
padding-block: 2px;
|
||||
}
|
||||
|
||||
/* ⚙ 설정 모달(280-5) — 안내 한 줄 · 횡단 배치 칸 */
|
||||
/* ⚙ 설정 모달(280-5 · 523-1) — 공용 모달 폭 절반 · 그룹 상자 · 맨 아래 단추 줄 하나 */
|
||||
.ui-modal.b07-settings {
|
||||
width: min(360px, 92vw);
|
||||
}
|
||||
|
||||
/* 좁은 폭 — 제목 · 머리 단추 한 줄(줄바꿈 없음) */
|
||||
.ui-modal.b07-settings .ui-modal__top > h3 {
|
||||
font-size: 16px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ui-modal.b07-settings .ui-modal__top .ui-btn {
|
||||
padding-inline: var(--spacing-8);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.b07-settings__note {
|
||||
margin: 0 0 var(--spacing-8);
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.b07-settings .b07-cross-layout {
|
||||
.b07-settings__group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
min-width: 280px;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
padding: var(--spacing-8) var(--spacing-12) var(--spacing-12);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
/* ⚙ 축척 칸(485-2) — X · Y 두 칸 한 줄 · 단추 오른쪽 */
|
||||
.b07-settings__group > legend {
|
||||
padding: 0 var(--spacing-4);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.b07-settings__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
padding-top: var(--spacing-8);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.b07-settings__cancel {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* 단추 줄 한 규격 — 안내선 단추(좌측 패널 작은 꼴)도 같은 키 · 좁은 폭에 맞춘 옆 여백 */
|
||||
.b07-settings__actions .ui-btn {
|
||||
padding: var(--spacing-8) var(--spacing-12);
|
||||
font-size: var(--text-body-sm);
|
||||
border-radius: var(--radius-buttons);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ⚙ 축척 칸(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 {
|
||||
@@ -495,12 +539,6 @@ html.is-bm-route .b07-cad-stack > .cad-tabs {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.b07-scale__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
/* 받지 못한 도면(508-3b) — 미리받기 · 열기가 두 번 다 실패 · 누르면 다시 받음 */
|
||||
.b07-drawing-button[data-fetch="error"] {
|
||||
border-left-color: var(--color-danger);
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
/* =============================================================================
|
||||
* B07_DesignDetail_Scale_Check.ts
|
||||
* ⚙ 축척 칸(485-2) 판정 — 칸 글 → 분모(정수 · 범위 안) · 자동 = null · 틀리면 까닭 글.
|
||||
* ⚙ 축척 칸(485-2) 판정 — 칸 글 → 분모(정수 · 범위 안) · 틀리면 까닭 글.
|
||||
* 서버 `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;
|
||||
/** 칸 하나 읽기 — 숫자 · 까닭 글(틀림). */
|
||||
export function readScaleAxis(raw: string, [low, high]: [number, number]): number | string {
|
||||
const text = raw.trim().replace(/,/g, "");
|
||||
const value = Number(text);
|
||||
if (text === "" || !Number.isInteger(value) || value < low || value > high) {
|
||||
@@ -20,5 +15,5 @@ export function readScaleAxis(
|
||||
}
|
||||
|
||||
/** 보낼 값 — 기본값과 같으면 null(파일엔 기본 그대로 남김). */
|
||||
export const scaleToSend = (value: number | null, fallback: number | null): number | null =>
|
||||
export const scaleToSend = (value: number, fallback: number): number | null =>
|
||||
value === fallback ? null : value;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* 153-1 B07 횡단 장 배치 칸 헬퍼 — 좌측 「횡단도」 묶음 맨 아래 자리 · 공용 입력칸 · 작은 단추를
|
||||
* Node 로 돌려 표준출력 JSON 한 벌로. TS 는 그 자리에서 트랜스파일 · 화면 그릇(DOM)은 쓰는 만큼만 흉내 ·
|
||||
* 공용 단추 · 입력칸은 진짜(`ui_template_elements`) · 알림만 기록용 가짜.
|
||||
/* 153-1 · 523-1 B07 횡단 장 배치 헬퍼 — 좌측 「횡단도」 카드 ⚙ 하나 · ⚙ 횡단 모달(그룹 상자
|
||||
* 「장 배치」 · 「표」 · 맨 아래 단추 줄 하나 · [확인] = 바뀐 것만 저장)을 Node 로 돌려 표준출력 JSON 한 벌로.
|
||||
* TS 는 그 자리에서 트랜스파일 · 화면 그릇(DOM)은 쓰는 만큼만 흉내 · 공용 단추 · 입력칸 · 고르기칸은 진짜
|
||||
* (`ui_template_elements`) · 모달 겉틀 · 알림만 기록용 가짜.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
@@ -65,6 +66,14 @@ const byClass = (node, name) => all(node).filter((n) => n.classList.contains(nam
|
||||
globalThis.document = { createElement: (tag) => new FakeNode(tag) };
|
||||
|
||||
const toasts = [];
|
||||
const modals = [];
|
||||
const openModal = (opts) => {
|
||||
const body = new FakeNode("div");
|
||||
const modal = { opts, body, open: true };
|
||||
modals.push(modal);
|
||||
opts.mount(body, () => (modal.open = false), new FakeNode("div"));
|
||||
return { close: () => (modal.open = false) };
|
||||
};
|
||||
const { loadTs } = makeLoader({
|
||||
root: ROOT,
|
||||
requireFor: (full) => (request) => {
|
||||
@@ -74,6 +83,9 @@ const { loadTs } = makeLoader({
|
||||
...loadTs(path.join(UI, "ui_template_elements.ts")),
|
||||
showToast: (message, kind) => toasts.push([kind, message]),
|
||||
};
|
||||
if (request === "@ui/ui_template_modal") return { openModal };
|
||||
if (request === "./B07_DesignDetail_UI_PlanManage") return {};
|
||||
if (request === "./B07_DesignDetail_Api_Fetch") return {};
|
||||
// 공용 카드(269) = 진짜 모듈
|
||||
if (request === "@ui/ui_template_collapsible")
|
||||
return loadTs(path.join(UI, "ui_template_collapsible.ts"));
|
||||
@@ -90,20 +102,18 @@ const { loadTs } = makeLoader({
|
||||
},
|
||||
});
|
||||
|
||||
const { buildDrawingSidePanel, buildCrossLayoutControls } = loadTs(
|
||||
path.join(B07, "B07_DesignDetail_UI_Panels.ts"),
|
||||
);
|
||||
const { buildDrawingSidePanel } = loadTs(path.join(B07, "B07_DesignDetail_UI_Panels.ts"));
|
||||
const { openDrawingSettings } = loadTs(path.join(B07, "B07_DesignDetail_UI_Settings.ts"));
|
||||
|
||||
const out = {};
|
||||
const settings = [];
|
||||
const slot = new FakeNode("div");
|
||||
slot.className = "b07-cross-layout-slot";
|
||||
const drawings = [
|
||||
{ id: "cross_standard", kind: "cross_standard", label: "표준 횡단면도", confirmed: false },
|
||||
{ id: "cross_s1", kind: "cross", label: "1장", confirmed: false },
|
||||
{ id: "cross_s2", kind: "cross", label: "2장", confirmed: true },
|
||||
];
|
||||
const panel = buildDrawingSidePanel(
|
||||
[
|
||||
{ id: "cross_standard", kind: "cross_standard", label: "표준 횡단면도", confirmed: false },
|
||||
{ id: "cross_s1", kind: "cross", label: "1장", confirmed: false },
|
||||
{ id: "cross_s2", kind: "cross", label: "2장", confirmed: true },
|
||||
],
|
||||
drawings,
|
||||
() => {},
|
||||
undefined,
|
||||
false,
|
||||
@@ -119,74 +129,113 @@ out.card = {
|
||||
gears[0].click();
|
||||
out.card.settings = settings;
|
||||
|
||||
// 장 — [이 장] 켜짐 · 값 바꿔 누름
|
||||
const applied = [];
|
||||
const defaults = [];
|
||||
slot.replaceChildren(
|
||||
buildCrossLayoutControls(
|
||||
[3, 4],
|
||||
(grid) => applied.push(grid),
|
||||
(grid) => defaults.push(grid),
|
||||
),
|
||||
);
|
||||
const box = slot.children[0];
|
||||
const buttons = () => all(slot).filter((n) => n.tagName === "BUTTON");
|
||||
const inputs = () => all(slot).filter((n) => n.tagName === "INPUT");
|
||||
out.sheet = {
|
||||
boxClass: box.className,
|
||||
labels: byClass(slot, "ui-field__label").map((n) => n.textContent),
|
||||
inputs: inputs().map((n) => ({
|
||||
value: n.value,
|
||||
type: n.attrs.type,
|
||||
min: n.attrs.min,
|
||||
max: n.attrs.max,
|
||||
cls: n.className,
|
||||
})),
|
||||
buttons: buttons().map((n) => ({ text: n.textContent, cls: n.className, disabled: n.disabled })),
|
||||
};
|
||||
inputs()[0].value = "2";
|
||||
inputs()[1].value = "5";
|
||||
buttons()[0].click();
|
||||
buttons()[1].click();
|
||||
inputs()[0].value = "11";
|
||||
buttons()[0].click();
|
||||
out.sheetClicks = { applied, defaults, toasts: [...toasts] };
|
||||
|
||||
// 장 아님 — [이 장] 꺼짐 · [모든 장]은 됨
|
||||
const defaults2 = [];
|
||||
slot.replaceChildren(buildCrossLayoutControls([4, 4], null, (grid) => defaults2.push(grid)));
|
||||
buttons()[0].click();
|
||||
buttons()[1].click();
|
||||
out.notSheet = {
|
||||
disabled: buttons().map((n) => n.disabled),
|
||||
defaults: defaults2,
|
||||
values: inputs().map((n) => n.value),
|
||||
};
|
||||
|
||||
// 180-2 표 배율 칸 — 배치 칸 줄 뒤 · 0.5~2.0 · step 0.1 · [배율 적용]
|
||||
const scales = [];
|
||||
const toastStart = toasts.length;
|
||||
slot.replaceChildren(
|
||||
buildCrossLayoutControls([4, 4], null, () => {}, {
|
||||
scale: { value: 1, onApply: (value) => scales.push(value) },
|
||||
}),
|
||||
);
|
||||
const scaleInput = inputs()[2];
|
||||
out.scale = {
|
||||
labels: byClass(slot, "ui-field__label").map((n) => n.textContent),
|
||||
input: {
|
||||
value: scaleInput.value,
|
||||
min: scaleInput.attrs.min,
|
||||
max: scaleInput.attrs.max,
|
||||
step: scaleInput.attrs.step,
|
||||
// ── ⚙ 횡단 모달(523-1) ─────────────────────────────────────────────────────
|
||||
const tick = () => new Promise((r) => setTimeout(r, 0));
|
||||
const saved = [];
|
||||
let current = drawings[1]; // cross_s1 = 1장
|
||||
const guideToggle = new FakeNode("button");
|
||||
guideToggle.textContent = "안내선 숨기기";
|
||||
const deps = {
|
||||
drawings,
|
||||
current: () => current,
|
||||
canRedraw: () => false,
|
||||
redraw: async () => false,
|
||||
cross: {
|
||||
layout: async () => ({ default: [4, 4], sheets: { 1: [3, 4] }, table_scale: 1 }),
|
||||
save: (next, sheet) => saved.push([next, sheet]),
|
||||
guideToggle,
|
||||
},
|
||||
buttons: buttons().map((n) => n.textContent),
|
||||
};
|
||||
scaleInput.value = "1.5";
|
||||
buttons()[2].click();
|
||||
scaleInput.value = "2.5";
|
||||
buttons()[2].click();
|
||||
out.scale.applied = scales;
|
||||
out.scale.toasts = toasts.slice(toastStart).map((t) => t[0]);
|
||||
const open = async () => {
|
||||
openDrawingSettings("cross", deps);
|
||||
await tick();
|
||||
const modal = modals.at(-1);
|
||||
const inputs = all(modal.body).filter((n) => n.tagName === "INPUT");
|
||||
const select = all(modal.body).find((n) => n.tagName === "SELECT");
|
||||
const footer = byClass(modal.body, "b07-settings__actions")[0];
|
||||
const btn = (label) => all(footer).find((n) => n.textContent === label);
|
||||
const type = (input, value) => {
|
||||
input.value = value;
|
||||
(input.listeners.input || []).forEach((h) => h());
|
||||
};
|
||||
return { modal, inputs, select, footer, btn, type };
|
||||
};
|
||||
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
(async () => {
|
||||
// 장 — 그룹 상자 둘 · 공용 칸 · 단추 줄 하나
|
||||
let m = await open();
|
||||
const legends = byClass(m.modal.body, "b07-settings__group").map(
|
||||
(g) => g.children[0].textContent,
|
||||
);
|
||||
const bodyButtons = all(m.modal.body).filter((n) => n.tagName === "BUTTON");
|
||||
out.sheet = {
|
||||
legends,
|
||||
labels: byClass(m.modal.body, "ui-field__label").map((n) => n.textContent),
|
||||
inputs: m.inputs.map((n) => ({
|
||||
value: n.value,
|
||||
type: n.attrs.type,
|
||||
min: n.attrs.min,
|
||||
max: n.attrs.max,
|
||||
step: n.attrs.step ?? null,
|
||||
cls: n.className,
|
||||
})),
|
||||
scope: { options: m.select.children.map((o) => o.textContent), value: m.select.value },
|
||||
footers: byClass(m.modal.body, "b07-settings__actions").length,
|
||||
footerButtons: m.footer.children.map((n) => n.textContent),
|
||||
buttonsOutsideFooter: bodyButtons.filter((n) => !all(m.footer).includes(n)).length,
|
||||
};
|
||||
// 이 장만 — 2 × 5
|
||||
m.type(m.inputs[0], "2");
|
||||
m.type(m.inputs[1], "5");
|
||||
m.btn("확인").click();
|
||||
await tick();
|
||||
out.applySheet = { saved: [...saved], closed: !m.modal.open };
|
||||
// 모든 장 — 고르기만 바꿔도 씀(장마다 값 지움)
|
||||
saved.length = 0;
|
||||
m = await open();
|
||||
m.select.value = "all";
|
||||
m.btn("확인").click();
|
||||
await tick();
|
||||
out.applyAll = [...saved];
|
||||
// 범위 밖 = 칸 붉음 · [확인] 꺼짐 · 저장 없음
|
||||
saved.length = 0;
|
||||
m = await open();
|
||||
m.type(m.inputs[0], "11");
|
||||
const confirm = m.btn("확인");
|
||||
out.badGrid = {
|
||||
disabled: confirm.disabled,
|
||||
error: m.inputs[0].className.includes("ui-input--error"),
|
||||
};
|
||||
confirm.click();
|
||||
await tick();
|
||||
out.badGrid.saved = saved.length;
|
||||
out.badGrid.open = m.modal.open;
|
||||
// 표 배율 — 1.5 = 배율만 · 2.5 = 꺼짐
|
||||
m = await open();
|
||||
m.type(m.inputs[2], "2.5");
|
||||
out.badTable = m.btn("확인").disabled;
|
||||
m.type(m.inputs[2], "1.5");
|
||||
m.btn("확인").click();
|
||||
await tick();
|
||||
out.applyTable = [...saved];
|
||||
// 장 아님(표준 횡단면도) — 고르기 = 모든 장 하나 · 손대지 않고 [확인] = 저장 없이 닫음
|
||||
saved.length = 0;
|
||||
current = drawings[0];
|
||||
m = await open();
|
||||
out.notSheet = {
|
||||
options: m.select.children.map((o) => o.textContent),
|
||||
values: m.inputs.map((n) => n.value),
|
||||
};
|
||||
m.btn("확인").click();
|
||||
await tick();
|
||||
out.notSheet.saved = saved.length;
|
||||
out.notSheet.closed = !m.modal.open;
|
||||
// [취소] = 저장 없이 닫음
|
||||
current = drawings[1];
|
||||
m = await open();
|
||||
m.type(m.inputs[0], "2");
|
||||
m.btn("취소").click();
|
||||
out.cancel = { saved: saved.length, closed: !m.modal.open };
|
||||
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
})();
|
||||
|
||||
@@ -60,6 +60,10 @@ class FakeNode {
|
||||
kids.forEach((k) => (k.parent = this));
|
||||
this.children.unshift(...kids);
|
||||
}
|
||||
remove() {
|
||||
if (this.parent) this.parent.children = this.parent.children.filter((c) => c !== this);
|
||||
this.parent = null;
|
||||
}
|
||||
replaceWith(node) {
|
||||
const at = this.parent.children.indexOf(this);
|
||||
node.parent = this.parent;
|
||||
@@ -97,16 +101,16 @@ const calls = [];
|
||||
let failConfirm = false;
|
||||
const SCALES = {
|
||||
scales: {
|
||||
longitudinal: { x: 1000, y: 200, plan: null },
|
||||
longitudinal: { x: 1000, y: 200, plan: 5000 },
|
||||
cross: { x: 100, y: 100 },
|
||||
mass_haul: { x: null, y: 50000 },
|
||||
development: { x: null, y: null },
|
||||
mass_haul: { x: 2000, y: 50000 },
|
||||
development: { x: 50, y: 50 },
|
||||
},
|
||||
defaults: {
|
||||
longitudinal: { x: 1000, y: 200, plan: null },
|
||||
longitudinal: { x: 1000, y: 200, plan: 5000 },
|
||||
cross: { x: 100, y: 100 },
|
||||
mass_haul: { x: null, y: 50000 },
|
||||
development: { x: null, y: null },
|
||||
mass_haul: { x: 2000, y: 50000 },
|
||||
development: { x: 50, y: 50 },
|
||||
},
|
||||
ranges: {
|
||||
longitudinal: { x: [100, 10000], y: [10, 2000], plan: [500, 50000] },
|
||||
@@ -141,21 +145,27 @@ const STUBS = {
|
||||
const setError = (msg) => (input.error = msg ?? null);
|
||||
return { root, input, setError };
|
||||
},
|
||||
createSelectField: (opts) => {
|
||||
const root = new FakeNode("div");
|
||||
const label = new FakeNode("span");
|
||||
label.className = "ui-field__label";
|
||||
label.textContent = opts.label;
|
||||
const select = new FakeNode("select");
|
||||
opts.options.forEach((o) => {
|
||||
const option = new FakeNode("option");
|
||||
option.textContent = o.text;
|
||||
option.value = o.value;
|
||||
select.append(option);
|
||||
});
|
||||
select.value = opts.value ?? opts.options[0]?.value;
|
||||
root.append(label, select);
|
||||
return { root, select, setOptions() {} };
|
||||
},
|
||||
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");
|
||||
@@ -280,55 +290,85 @@ const drawings = [
|
||||
let redraws = 0;
|
||||
const saved = [];
|
||||
const guideToggle = button({ label: "안내선 숨기기" });
|
||||
const closedCalls = [];
|
||||
const deps = {
|
||||
drawings,
|
||||
current: () => current,
|
||||
canRedraw: () => true,
|
||||
redraw: async () => (redraws++, true),
|
||||
plan: { projectId: "p1", onClosed: () => {} },
|
||||
plan: { projectId: "p1", onClosed: (...a) => closedCalls.push(a) },
|
||||
cross: {
|
||||
layout: async () => ({ default: [4, 4], sheets: { 1: [3, 2] }, table_scale: 1.2 }),
|
||||
save: (next, sheet) => saved.push([next, sheet]),
|
||||
guideToggle,
|
||||
},
|
||||
};
|
||||
const tick = () => new Promise((r) => setTimeout(r, 0));
|
||||
const texts = (node) =>
|
||||
all(node)
|
||||
.map((n) => n._text)
|
||||
.filter(Boolean);
|
||||
const legends = (node) =>
|
||||
byClass(node, "b07-settings__group").map((g) => g.children[0].textContent);
|
||||
const footerOf = (modal) => byClass(modal.body, "b07-settings__actions");
|
||||
const footerBtn = (modal, label) =>
|
||||
all(footerOf(modal)[0]).find((n) => n.tagName === "BUTTON" && n.textContent === label);
|
||||
const type = (input, value) => {
|
||||
input.value = value;
|
||||
(input.listeners.input || []).forEach((h) => h());
|
||||
};
|
||||
let autoBoxes = 0; // 「자동」 체크(523-1 걷음) — 모든 모달에서 셈
|
||||
const countAuto = (modal) =>
|
||||
(autoBoxes += all(modal.body).filter((n) => n.attrs.type === "checkbox").length);
|
||||
|
||||
openDrawingSettings("cross", deps);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
await tick();
|
||||
await tick();
|
||||
const cross = modals.at(-1);
|
||||
const layoutNode = byClass(cross.body, "b07-cross-layout")[0];
|
||||
const crossInputs = all(layoutNode).filter((n) => n.tagName === "INPUT");
|
||||
countAuto(cross);
|
||||
const layoutBoxes = byClass(cross.body, "b07-cross-layout");
|
||||
const crossInputs = layoutBoxes.flatMap((n) => all(n).filter((k) => k.tagName === "INPUT"));
|
||||
out.crossModal = {
|
||||
title: cross.opts.title,
|
||||
labels: byClass(layoutNode, "ui-field__label").map((n) => n.textContent),
|
||||
legends: legends(cross.body),
|
||||
labels: layoutBoxes.flatMap((n) => byClass(n, "ui-field__label")).map((n) => n.textContent),
|
||||
values: crossInputs.map((n) => n.value),
|
||||
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,
|
||||
footers: footerOf(cross).length,
|
||||
footerButtons: footerOf(cross)[0].children.map((n) => n.textContent),
|
||||
buttonsInGroups: byClass(cross.body, "b07-settings__group").flatMap((g) =>
|
||||
all(g).filter((n) => n.tagName === "BUTTON"),
|
||||
).length,
|
||||
head: cross.opts.actions.map((n) => [n.textContent, n.disabled]),
|
||||
notes: texts(cross.body).filter((t) => t.startsWith("지금 도면")),
|
||||
};
|
||||
crossInputs[0].value = "2";
|
||||
all(layoutNode)
|
||||
.find((n) => n.textContent === "이 장")
|
||||
.click();
|
||||
out.crossSave = { saved, closed: !cross.open };
|
||||
type(crossInputs[0], "2");
|
||||
footerBtn(cross, "확인").click();
|
||||
await tick();
|
||||
out.crossSave = {
|
||||
saved,
|
||||
closed: !cross.open,
|
||||
scaleCalls: calls.filter((c) => c[0] === "scales").length,
|
||||
};
|
||||
|
||||
openDrawingSettings("structure", deps);
|
||||
const structure = modals.at(-1);
|
||||
countAuto(structure);
|
||||
out.structureModal = {
|
||||
title: structure.opts.title,
|
||||
notes: texts(structure.body),
|
||||
legends: legends(structure.body),
|
||||
footerButtons: footerOf(structure)[0].children.map((n) => n.textContent),
|
||||
head: structure.opts.actions.map((n) => [n.textContent, n.disabled, n.title]),
|
||||
};
|
||||
footerBtn(structure, "확인").click();
|
||||
out.structureModal.closed = !structure.open;
|
||||
|
||||
openDrawingSettings("longitudinal", deps);
|
||||
out.longModal = { title: modals.at(-1).opts.title, bodyKids: modals.at(-1).body.children.length };
|
||||
out.longModal = {
|
||||
title: modals.at(-1).opts.title,
|
||||
legends: legends(modals.at(-1).body),
|
||||
footers: footerOf(modals.at(-1)).length,
|
||||
};
|
||||
|
||||
current = drawings[1]; // plan_k
|
||||
openDrawingSettings("plan", deps);
|
||||
@@ -338,64 +378,52 @@ const drawings = [
|
||||
head: plan.controls.actions.map((n) => [n.textContent, n.disabled]),
|
||||
};
|
||||
plan.controls.actions[0].click();
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
await tick();
|
||||
out.planRedraw = { redraws, closed: !plan.open };
|
||||
|
||||
current = drawings[3]; // 표준 횡단면도도 횡단 묶음
|
||||
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));
|
||||
// ── 축척 칸(485-2 · 523-1 자동 없음 · 저장 = 모달 [확인]) ─────────────────
|
||||
closedCalls.length = 0;
|
||||
const scaleOf = async (kind) => {
|
||||
openDrawingSettings(kind, scaleDeps);
|
||||
openDrawingSettings(kind, deps);
|
||||
await tick();
|
||||
const modal = modals.at(-1);
|
||||
countAuto(modal);
|
||||
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 inputs = all(section).filter((n) => n.tagName === "INPUT");
|
||||
const btn = (label) => footerBtn(modal, label);
|
||||
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,
|
||||
save: !btn("확인").disabled,
|
||||
});
|
||||
return { modal, section, inputs, autos, btn, type, toggle, state };
|
||||
return { modal, section, inputs, btn, 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.longLegend = legends(s.modal.body);
|
||||
scale.longFooter = footerOf(s.modal)[0].children.map((n) => n.textContent);
|
||||
scale.longStart = s.state();
|
||||
s.type(s.inputs[0], "50");
|
||||
type(s.inputs[0], "50");
|
||||
scale.longLow = s.state();
|
||||
s.type(s.inputs[0], "500.5");
|
||||
type(s.inputs[0], "500.5");
|
||||
scale.longFrac = s.state();
|
||||
s.type(s.inputs[0], "500");
|
||||
type(s.inputs[0], "500");
|
||||
scale.longOk = s.state();
|
||||
calls.length = 0;
|
||||
answer = false;
|
||||
s.btn("축척 저장").click();
|
||||
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();
|
||||
s.btn("확인").click();
|
||||
await tick();
|
||||
await tick();
|
||||
scale.save = {
|
||||
@@ -405,66 +433,53 @@ const drawings = [
|
||||
closed: [...closedCalls],
|
||||
};
|
||||
s = await scaleOf("longitudinal");
|
||||
s.type(s.inputs[1], "300");
|
||||
type(s.inputs[1], "300");
|
||||
s.btn("기본값").click();
|
||||
scale.reset = s.state();
|
||||
calls.length = 0;
|
||||
const toastAt = toasts.length;
|
||||
s.btn("축척 저장").click();
|
||||
s.btn("확인").click();
|
||||
await tick();
|
||||
scale.same = { calls: calls.length, toasts: toasts.slice(toastAt).map((t) => t[1]) };
|
||||
// 평면 위치도 칸(521-2) — 셋째 칸 · 자동 끔 → 범위 밖 붉음 → 1/3,000 저장
|
||||
scale.same = { calls: calls.length, open: s.modal.open };
|
||||
// 평면 위치도 칸(521-2) — 처음 1/5,000 · 1/500 밑 = 붉음 · 1/3,000 저장
|
||||
s = await scaleOf("longitudinal");
|
||||
scale.planStart = s.state();
|
||||
s.toggle(s.autos[0]);
|
||||
s.type(s.inputs[2], "400");
|
||||
type(s.inputs[2], "400");
|
||||
scale.planLow = s.state();
|
||||
s.type(s.inputs[2], "3000");
|
||||
type(s.inputs[2], "3000");
|
||||
calls.length = 0;
|
||||
s.btn("축척 저장").click();
|
||||
s.btn("확인").click();
|
||||
await tick();
|
||||
await tick();
|
||||
scale.planSent = calls.find((c) => c[0] === "scales")?.[1];
|
||||
// [취소] = 저장 없이 닫음
|
||||
s = await scaleOf("longitudinal");
|
||||
type(s.inputs[0], "500");
|
||||
calls.length = 0;
|
||||
s.btn("취소").click();
|
||||
scale.cancelButton = { calls: calls.length, open: s.modal.open };
|
||||
|
||||
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();
|
||||
type(s.inputs[0], "3000");
|
||||
calls.length = 0;
|
||||
s.btn("축척 저장").click();
|
||||
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);
|
||||
openDrawingSettings("structure", deps);
|
||||
scale.structure = byClass(modals.at(-1).body, "b07-scale").length;
|
||||
scale.autoBoxes = autoBoxes;
|
||||
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),
|
||||
];
|
||||
out.readAxis = ["1,000", " 99 ", "10001", "12.5", "", "abc"].map((raw) =>
|
||||
Check.readScaleAxis(raw, [100, 10000]),
|
||||
);
|
||||
out.toSend = [Check.scaleToSend(200, 200), Check.scaleToSend(500, 200)];
|
||||
|
||||
// ── 저장 묶음 ────────────────────────────────────────────────────────────
|
||||
const tabsOf = (list, activeId) => ({
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""153-1 B07 횡단 장 배치 칸.
|
||||
"""153-1 · 523-1 B07 횡단 장 배치 — ⚙ 횡단 모달.
|
||||
|
||||
못박는 것
|
||||
- 배치 칸 자리 = 「횡단도」 카드 ⚙ 모달(280-5) — 카드엔 ⚙ 하나 · 배치 칸 없음.
|
||||
- 공용 입력칸(ui-field__label · ui-input--sm · 1~10) 둘 + 작은 ghost 단추 [이 장] [모든 장].
|
||||
- 장이 아니면 [이 장] 꺼짐 · [모든 장]은 됨 · 범위 밖 값 = 알림만.
|
||||
- 그룹 상자 「장 배치」(배치 열 · 행 · 적용) · 「표」(표 배율) · 공용 작은 칸(ui-input--sm).
|
||||
- 단추 줄 = 맨 아래 하나([안내선 숨기기] [취소] [확인]) · 그 밖 단추 없음.
|
||||
- [확인] = 바뀐 것만 저장 — 이 장만 · 모든 장(장마다 값 지움) · 표 배율만 · 손 안 대면 저장 없이 닫음.
|
||||
- 범위 밖 = 칸 붉음 + [확인] 꺼짐 · 장이 아니면 적용 = 모든 장 하나 · [취소] = 그대로 닫음.
|
||||
|
||||
Node 헬퍼 `helper_153_b07_cross_layout.cjs` 가 화면 그릇 흉내로 돌림. 화면은 사용자 확인.
|
||||
Node 헬퍼 `helper_153_b07_cross_layout.cjs` 가 화면 그릇 흉내로 돌림.
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -38,37 +40,39 @@ def test_횡단도_카드는_설정_단추만(out):
|
||||
assert card["settings"] == ["cross"]
|
||||
|
||||
|
||||
def test_공용_입력칸과_작은_단추(out):
|
||||
def test_그룹_상자와_단추_줄_하나(out):
|
||||
sheet = out["sheet"]
|
||||
assert sheet["boxClass"] == "b07-cross-layout"
|
||||
assert sheet["labels"] == ["배치 열", "행"]
|
||||
assert [i["value"] for i in sheet["inputs"]] == ["3", "4"]
|
||||
for field in sheet["inputs"]:
|
||||
assert sheet["legends"] == ["장 배치", "표"]
|
||||
assert sheet["labels"] == ["배치 열", "행", "적용", "표 배율"]
|
||||
assert [i["value"] for i in sheet["inputs"]] == ["3", "4", "1"] # 지금 장(1장) 값
|
||||
for field in sheet["inputs"][:2]:
|
||||
assert field["type"] == "number" and field["min"] == "1" and field["max"] == "10"
|
||||
assert "ui-input--sm" in field["cls"]
|
||||
assert [b["text"] for b in sheet["buttons"]] == ["이 장", "모든 장"]
|
||||
for button in sheet["buttons"]:
|
||||
assert button["cls"] == "ui-btn ui-btn--ghost ui-btn--sm" and not button["disabled"]
|
||||
table = sheet["inputs"][2]
|
||||
assert (table["min"], table["max"], table["step"]) == ("0.5", "2", "0.1")
|
||||
assert all("ui-input--sm" in field["cls"] for field in sheet["inputs"])
|
||||
assert sheet["scope"] == {"options": ["이 장만", "모든 장"], "value": "sheet"}
|
||||
assert sheet["footers"] == 1 and sheet["buttonsOutsideFooter"] == 0
|
||||
assert sheet["footerButtons"] == ["안내선 숨기기", "취소", "확인"]
|
||||
|
||||
|
||||
def test_이_장_모든_장_누름(out):
|
||||
clicks = out["sheetClicks"]
|
||||
assert clicks["applied"] == [[2, 5]]
|
||||
assert clicks["defaults"] == [[2, 5]]
|
||||
assert [t[0] for t in clicks["toasts"]] == ["error"] # 11 = 범위 밖
|
||||
def test_확인은_바뀐_것만_저장(out):
|
||||
assert out["applySheet"] == {
|
||||
"saved": [[{"default": [4, 4], "sheets": {"1": [2, 5]}, "table_scale": 1}, 1]],
|
||||
"closed": True,
|
||||
}
|
||||
assert out["applyAll"] == [[{"default": [3, 4], "sheets": {}, "table_scale": 1}, 1]]
|
||||
assert out["applyTable"] == [
|
||||
[{"default": [4, 4], "sheets": {"1": [3, 4]}, "table_scale": 1.5}, 1]
|
||||
]
|
||||
|
||||
|
||||
def test_장_아니면_이_장_꺼짐(out):
|
||||
not_sheet = out["notSheet"]
|
||||
assert not_sheet["disabled"] == [True, False]
|
||||
assert not_sheet["defaults"] == [[4, 4]]
|
||||
assert not_sheet["values"] == ["4", "4"]
|
||||
|
||||
|
||||
def test_표_배율_칸(out):
|
||||
scale = out["scale"]
|
||||
assert scale["labels"] == ["배치 열", "행", "표 배율"]
|
||||
assert scale["input"] == {"value": "1", "min": "0.5", "max": "2", "step": "0.1"}
|
||||
assert scale["buttons"] == ["이 장", "모든 장", "배율 적용"]
|
||||
assert scale["applied"] == [1.5]
|
||||
assert scale["toasts"] == ["error"] # 2.5 = 범위 밖
|
||||
def test_범위_밖_장_아님_취소(out):
|
||||
assert out["badGrid"] == {"disabled": True, "error": True, "saved": 0, "open": True}
|
||||
assert out["badTable"] is True
|
||||
assert out["notSheet"] == {
|
||||
"options": ["모든 장"],
|
||||
"values": ["4", "4", "1"],
|
||||
"saved": 0,
|
||||
"closed": True,
|
||||
}
|
||||
assert out["cancel"] == {"saved": 0, "closed": True}
|
||||
|
||||
@@ -68,8 +68,8 @@ def test_페이지_잇기_토스트_거름_장만():
|
||||
assert "if (guideShown() && guide?.marks?.length) {" in page # 숨김 = 넘침 토스트 없음
|
||||
assert "cad.setGuideVisible(visible)" in page
|
||||
assert "if (!guideShown()) cad.setGuideVisible(false);" in page # 다시 세운 화면에 이어 줌
|
||||
# 배치 칸 줄 끝 = ⚙ 횡단 모달(280-5)
|
||||
assert "controls.append(deps.cross.guideToggle);" in _read(
|
||||
# ⚙ 횡단 모달(280-5) 맨 아래 단추 줄(523-1)
|
||||
assert "if (cross) footer.append(deps.cross.guideToggle);" in _read(
|
||||
B07 / "B07_DesignDetail_UI_Settings.ts"
|
||||
)
|
||||
assert "guideToggle.disabled = !isCrossSheet(drawing);" in page # 장만
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"""212-3 B07 좌측 「횡단도」 배치 칸 — 단추 · 숫자 칸 한 규격 · 한 줄에 한 일 · 남는 검은 선 판정.
|
||||
|
||||
못박는 것
|
||||
- 줄 = 「배치 열 · 행」 · 「[이 장] [모든 장]」 · 「표 배율 [배율 적용]」
|
||||
· [안내선 숨기기](부르는 쪽).
|
||||
- 줄 = 「배치 열 · 행」 · 「적용」 · 「표 배율」(⚙ 횡단 모달 그룹 상자 · 523-1)
|
||||
· 단추([안내선 숨기기] [취소] [확인]) = 맨 아래 단추 줄 하나.
|
||||
- 단추 · 숫자 칸 · 이름 = 좌측 패널 공용 양식(269 `.ui-panel-form`) · 한 줄 안 같은 너비.
|
||||
- 옛 한 줄 감기(flex-wrap) · 숫자 칸 48px 고정 없음.
|
||||
- 인쇄 칸선 없음(526-1 · 옛 「남는 검은 선」) — 칸은 안내선 겹판(단면마다 점선)이라
|
||||
@@ -53,12 +53,11 @@ def test_한_줄에_한_일_같은_너비():
|
||||
assert "flex-wrap" not in box
|
||||
assert _decl(_block(".b07-cross-layout__row > *"), "flex") == "1 1 0"
|
||||
assert "width: 48px" not in CSS
|
||||
assert 'line.className = "b07-cross-layout__row"' in PANELS
|
||||
assert "row(cols.root, rows.root)" in PANELS
|
||||
assert re.search(r"row\(\s*action\(\"이 장\"[^;]*action\(\"모든 장\"", PANELS, re.S)
|
||||
assert "box.append(row(scale.root, button));" in PANELS
|
||||
# 안내선 단추 = 배치 칸 맨 아래 한 줄(전체 폭) · ⚙ 횡단 모달(280-5)
|
||||
assert "controls.append(deps.cross.guideToggle);" in SETTINGS
|
||||
assert 'row.className = "b07-cross-layout__row"' in SETTINGS
|
||||
assert "line(cols.root, rows.root), line(scope.root)" in SETTINGS
|
||||
assert "line(table.root)" in SETTINGS
|
||||
# 안내선 단추 = 맨 아래 단추 줄(523-1) · ⚙ 횡단 모달(280-5)
|
||||
assert "if (cross) footer.append(deps.cross.guideToggle);" in SETTINGS
|
||||
|
||||
|
||||
def test_인쇄_칸선_없음_칸은_안내선_겹판():
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""485-1 B07 도면 X · Y 축척(요구 280-5) — 프로젝트마다 `drawing_scales.json`.
|
||||
|
||||
값 없음 = 지금 결과 그대로 · 종단 X 바꿈 = 장 수 · 도각 축척 글 · 횡단 X ≠ Y 덧그림 자리 ·
|
||||
값 없음 = 기본 지정값(자동 없음 · 523-1) · 종단 X 바꿈 = 장 수 · 도각 축척 글 · 횡단 X ≠ Y 덧그림 자리 ·
|
||||
토적 · 전개도 · 범위 밖 거절 · 바꾼 종류만 저장본 풂.
|
||||
"""
|
||||
|
||||
@@ -218,8 +218,8 @@ MASS_HAUL = {
|
||||
|
||||
|
||||
def test_토적_가로_세로(project: Path) -> None:
|
||||
auto = build_mass_haul_drawing(LONGITUDINAL, MASS_HAUL, "mass_haul")
|
||||
assert "SCALE H=1:500 V=1:50,000" in _texts(auto) # 40 m = 자동 1/500
|
||||
base = build_mass_haul_drawing(LONGITUDINAL, MASS_HAUL, "mass_haul")
|
||||
assert "SCALE H=1:2,000 V=1:50,000" in _texts(base) # 기본 = 지정값(자동 없음 · 523-1)
|
||||
_set(project, {"mass_haul": {"x": 1000, "y": 100000}})
|
||||
fixed = build_mass_haul_drawing(LONGITUDINAL, MASS_HAUL, "mass_haul")
|
||||
assert "SCALE H=1:1,000 V=1:100,000" in _texts(fixed)
|
||||
@@ -231,8 +231,8 @@ def test_토적_가로_세로(project: Path) -> None:
|
||||
def test_전개도_가로_세로(project: Path) -> None:
|
||||
rows = [dev._row("soil", "돌망태", i * 20.0, i * 20.0 + 6, 2.0, str(i)) for i in range(3)]
|
||||
sheet = dev.development_sheets(rows)[0]
|
||||
auto = build_development_drawing(sheet["id"], sheet, 20.0)
|
||||
assert any(text.startswith("S=1:") for text in _texts(auto))
|
||||
base = build_development_drawing(sheet["id"], sheet, 20.0)
|
||||
assert "S=1:50" in _texts(base) # 기본 = 가로 · 세로 1/50 지정값(523-1)
|
||||
_set(project, {"development": {"x": 200, "y": 100}})
|
||||
fixed = build_development_drawing(sheet["id"], sheet, 20.0)
|
||||
assert "H=1:200 V=1:100" in _texts(fixed)
|
||||
@@ -244,7 +244,7 @@ def test_전개도_가로_세로(project: Path) -> None:
|
||||
|
||||
def test_범위_검사() -> None:
|
||||
assert checked_scales({"cross": {"x": 100, "y": None}}) == {"cross": {"x": 100, "y": None}}
|
||||
# 종단 평면 위치도(521-2) = 1/500~1/50,000 · null = 자동
|
||||
# 종단 평면 위치도(521-2) = 1/500~1/50,000 · null = 기본
|
||||
assert checked_scales({"longitudinal": {"plan": 3000}}) == {
|
||||
"longitudinal": {"x": None, "y": None, "plan": 3000}
|
||||
}
|
||||
@@ -294,7 +294,7 @@ def test_바꾼_종류만_저장본_풂(project: Path) -> None:
|
||||
drawings = _read_manifest(project)["drawings"]
|
||||
assert not drawings["longitudinal_1"]["confirmed"]
|
||||
assert drawings["cross_00020m"]["confirmed"] and drawings["cover"]["confirmed"]
|
||||
assert read_drawing_scales(project)["longitudinal"] == {"x": 500, "y": 200, "plan": None}
|
||||
assert read_drawing_scales(project)["longitudinal"] == {"x": 500, "y": 200, "plan": 5000}
|
||||
# 보낸 종류만 바뀜 — 앞서 쓴 종단은 남음 · 횡단 = 측점 설계 잠정 몫
|
||||
invalidated, stale = write_drawing_scales(
|
||||
project, project / "none.json", {"cross": {"x": 100, "y": 50}}
|
||||
@@ -321,5 +321,5 @@ def test_GET_PUT_범위_밖_400(project: Path, monkeypatch: pytest.MonkeyPatch)
|
||||
assert not drawing_scales_path(project).exists() # 거절 = 안 씀
|
||||
put = client.put(url, json={"scales": {"longitudinal": {"x": 500, "y": 200}}})
|
||||
assert put.status_code == 200
|
||||
assert put.json()["scales"]["longitudinal"] == {"x": 500, "y": 200, "plan": None}
|
||||
assert put.json()["scales"]["longitudinal"] == {"x": 500, "y": 200, "plan": 5000}
|
||||
assert put.json()["invalidated"] == {}
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
- ⚙ 누름 = 카드 접힘 그대로 · 도면 안 엶(Enter 도) · 제목 누름 · 단추 누름은 그대로.
|
||||
- 모달 — 횡단 = 배치 칸(지금 장 값) + 표 배율 + 안내선 · 평면도 = 평면도 관리 모달 · 구조물 = 축척 없음 줄 ·
|
||||
머리 [원본에서 다시 그리기] = 그 묶음 도면을 연 때만 켬(표준 횡단면도 = 횡단 묶음).
|
||||
- 523-1 — 그룹 상자(legend) · 맨 아래 단추 줄 하나([기본값] [취소] [확인]) · 「자동」 체크 없음 ·
|
||||
[확인] = 바뀐 것만 저장 · 축척 기본 = 지정값(종단 평면도 1/5,000 · 토적 X 1/2,000 · 전개도 1/50).
|
||||
- 저장 묶음 — 고친 탭(●) + 지금 탭만 confirm · 고친 것 없으면 요청 0 · 실패 / 도각 편집 = 거짓(머묾) ·
|
||||
원본 되돌림 = 물음 → invalidate → 다시 실음 · [취소] = 그대로.
|
||||
- 페이지 — [현재 도면 확정] · [수정] · 자동 B08 이동 · 「모든 횡단 확정」 잠금 없음 · 읽기 전용 안 묶음 ·
|
||||
@@ -77,24 +79,25 @@ def test_설정_누름은_접힘_도면_열기_안_일으킴(out):
|
||||
def test_횡단_모달은_배치_칸_표_배율_안내선(out):
|
||||
cross = out["crossModal"]
|
||||
assert cross["title"] == "횡단도 설정"
|
||||
assert cross["labels"] == ["배치 열", "행", "표 배율"]
|
||||
assert cross["legends"] == ["장 배치", "표", "축척 — 횡단도 장(표준 횡단면도 뺌)"]
|
||||
assert cross["labels"] == ["배치 열", "행", "적용", "표 배율"]
|
||||
assert cross["values"] == ["3", "2", "1.2"] # 지금 장(1장) 값 · 표 배율
|
||||
assert [b[0] for b in cross["buttons"]] == ["이 장", "모든 장", "배율 적용", "안내선 숨기기"]
|
||||
assert cross["guideLast"] is True
|
||||
assert cross["footers"] == 1 and cross["buttonsInGroups"] == 0 # 단추 줄 하나(523-1)
|
||||
assert cross["footerButtons"] == ["안내선 숨기기", "기본값", "취소", "확인"]
|
||||
assert cross["head"] == [["원본에서 다시 그리기", False]]
|
||||
assert cross["notes"] == ["지금 도면: cross_s1"]
|
||||
assert cross["scaleTitle"] == "축척 — 횡단도 장(표준 횡단면도 뺌)"
|
||||
saved = out["crossSave"]
|
||||
assert saved["closed"] is True
|
||||
assert saved["closed"] is True and saved["scaleCalls"] == 0 # 축척 그대로 = 안 보냄
|
||||
assert saved["saved"] == [[{"default": [4, 4], "sheets": {"1": [2, 2]}, "table_scale": 1.2}, 1]]
|
||||
|
||||
|
||||
def test_구조물_종단_평면도_모달(out):
|
||||
structure = out["structureModal"]
|
||||
assert structure["title"] == "구조물 도면 설정"
|
||||
assert structure["notes"] == ["M02 그림 — 축척 없음"]
|
||||
assert structure["legends"] == ["축척"] and "M02 그림 — 축척 없음" in structure["notes"]
|
||||
assert structure["footerButtons"] == ["취소", "확인"] and structure["closed"] is True
|
||||
assert structure["head"][0][1] is True # 다른 묶음 도면을 연 중 = 꺼짐
|
||||
assert out["longModal"] == {"title": "종단도 설정", "bodyKids": 1} # 축척 칸(485-2)
|
||||
assert out["longModal"] == {"title": "종단도 설정", "legends": ["축척 — 종단도"], "footers": 1}
|
||||
assert out["planModal"] == {"viaManager": True, "head": [["원본에서 다시 그리기", False]]}
|
||||
assert out["planRedraw"] == {"redraws": 1, "closed": True}
|
||||
assert out["standardInCross"] is True
|
||||
@@ -223,16 +226,18 @@ 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
|
||||
assert out["readAxis"] == [1000, RANGE, RANGE, RANGE, RANGE, RANGE]
|
||||
assert out["toSend"] == [None, 500] # 기본과 같으면 null
|
||||
|
||||
|
||||
def test_485_2_종단_축척_저장(out):
|
||||
scale = out["scale"]
|
||||
assert scale["longLabels"] == ["X(가로) 1/", "Y(세로) 1/", "평면도 1/"]
|
||||
assert scale["longStart"]["values"] == ["1000", "200", ""]
|
||||
assert scale["longStart"]["autos"] == [True] # 종단 = 평면도 칸만 자동(521-2)
|
||||
for bad in ("longLow", "longFrac"): # 범위 밖 · 소수 = 붉음 + 저장 꺼짐
|
||||
assert scale["longLegend"] == ["축척 — 종단도"]
|
||||
assert scale["longFooter"] == ["기본값", "취소", "확인"]
|
||||
assert scale["longStart"]["values"] == ["1000", "200", "5000"] # 평면도 = 지정값(523-1)
|
||||
assert scale["longStart"]["disabled"] == [False, False, False]
|
||||
for bad in ("longLow", "longFrac"): # 범위 밖 · 소수 = 붉음 + [확인] 꺼짐
|
||||
assert scale[bad]["errors"] == [RANGE, None, None]
|
||||
assert scale[bad]["save"] is False
|
||||
assert scale["longOk"]["errors"] == [None, None, None] and scale["longOk"]["save"] is True
|
||||
@@ -243,27 +248,23 @@ def test_485_2_종단_축척_저장(out):
|
||||
assert save["sent"] == {"longitudinal": {"x": 500, "y": None, "plan": None}} # 기본 → null
|
||||
assert save["open"] is False
|
||||
assert save["closed"] == [[True, "longitudinal"]] # 화면 다시 세움 · 같은 도면
|
||||
assert scale["reset"]["values"] == ["1000", "200", ""] # [기본값] = 칸만
|
||||
assert scale["same"] == {"calls": 0, "toasts": ["바뀐 축척이 없습니다."]}
|
||||
# 평면 위치도 칸(521-2) — 처음 자동(칸 꺼짐) · 1/500 밑 = 붉음 · 1/3,000 저장
|
||||
assert scale["planStart"]["disabled"] == [False, False, True]
|
||||
assert scale["reset"]["values"] == ["1000", "200", "5000"] # [기본값] = 칸만
|
||||
assert scale["same"] == {"calls": 0, "open": False} # 바뀐 것 없음 = 닫기만
|
||||
assert scale["planLow"]["errors"] == [None, None, "1/500~1/50,000 사이 정수"]
|
||||
assert scale["planLow"]["save"] is False
|
||||
assert scale["planSent"] == {"longitudinal": {"x": None, "y": None, "plan": 3000}}
|
||||
assert scale["cancelButton"] == {"calls": 0, "open": False}
|
||||
|
||||
|
||||
def test_485_2_토적_전개도_자동(out):
|
||||
def test_523_1_토적_전개도_지정값_자동_없음(out):
|
||||
scale = out["scale"]
|
||||
assert scale["massStart"] == {
|
||||
"values": ["", "50000"],
|
||||
"values": ["2000", "50000"],
|
||||
"errors": [None, None],
|
||||
"disabled": [True, False],
|
||||
"autos": [True],
|
||||
"disabled": [False, False],
|
||||
"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["massSent"] == {"ask": 0, "sent": {"mass_haul": {"x": 3000, "y": None}}}
|
||||
assert scale["dev"]["values"] == ["50", "50"] and scale["dev"]["disabled"] == [False, False]
|
||||
assert scale["structure"] == 0 # 구조물 = 축척 칸 없음
|
||||
assert scale["autoBoxes"] == 0 # 「자동」 체크 = 모든 모달에서 0
|
||||
|
||||
@@ -135,11 +135,11 @@ def test_평면_칸_작도영역_우상단_안_겹침_장마다_강조() -> None
|
||||
)
|
||||
assert not plan_box_overlaps(longitudinal, sheet["graph"], (x0, y0, *sheet["bbox"][2:]))
|
||||
assert f"{number}/{len(sheets)} 장" in sheet["texts"]
|
||||
assert any(text.startswith("평면도 S=1/") for text in sheet["texts"])
|
||||
assert "평면도 S=1/5,000" in sheet["texts"] # 기본 지정값(자동 없음 · 523-1)
|
||||
assert sheet["bold"] # 이 장 구간 굵게
|
||||
lows.append(min(x for x, _y in sheet["bold"]) - box[0])
|
||||
# 자동 = 모든 장이 같은 축척 · 같은 자리 — 굵은 구간만 장마다 오른쪽으로 옮겨 감
|
||||
assert lows == sorted(lows) and len(set(lows)) == len(lows)
|
||||
# 지정 축척 = 장마다 이 장 구간을 칸 가운데로 — 굵은 구간 왼끝이 칸 안
|
||||
assert all(0.0 <= low <= 150.0 for low in lows)
|
||||
|
||||
|
||||
def test_칸을_덮으면_측점_줄여_장_더_나눔() -> None:
|
||||
|
||||
@@ -123,8 +123,8 @@ def test_mass_haul_scale_follows_route_length():
|
||||
|
||||
|
||||
def test_mass_haul_curve_uses_paper_scale():
|
||||
"""유토곡선 점은 x=거리xmm_h, y=토량/50mm로 옮겨진다. 40m 노선이면 H 1/500 -> 2.0mm/m."""
|
||||
mm_h = 1000.0 / auto_scale_h(40.0)
|
||||
"""유토곡선 점은 x=거리xmm_h, y=토량/50mm로 옮겨진다. 가로 = 기본 지정값 1/2,000(523-1) -> 0.5mm/m."""
|
||||
mm_h = 1000.0 / 2000
|
||||
drawing = build_mass_haul_drawing(LONGITUDINAL, MASS_HAUL, "mass_haul")
|
||||
curve = _lines(drawing, "b08-masshaul-curve")
|
||||
assert curve[0][0] == (0.0, 0.0)
|
||||
@@ -143,7 +143,7 @@ def test_mass_haul_balloon_texts():
|
||||
assert "사토 1" in labels
|
||||
assert "M.N= 1+10.00" in labels # 측점 간격 20m 기준 30m 지점
|
||||
assert "유 토 곡 선" in labels
|
||||
assert "SCALE H=1:500 V=1:50,000" in labels # 40m 노선의 자동 축척
|
||||
assert "SCALE H=1:2,000 V=1:50,000" in labels # 기본 지정값(자동 없음 · 523-1)
|
||||
|
||||
|
||||
def test_mass_haul_table_marks_match_delivery_form():
|
||||
@@ -189,7 +189,7 @@ def test_mass_haul_band_chords_sit_at_stored_levels():
|
||||
"""경계현은 level_base, 운반현은 띠 중간 높이에 저장값 그대로 놓인다."""
|
||||
drawing = build_mass_haul_drawing(LONGITUDINAL, MASS_HAUL, "mass_haul")
|
||||
chords = _lines(drawing, "b08-masshaul-band")
|
||||
mm_h = 1000.0 / auto_scale_h(40.0)
|
||||
mm_h = 1000.0 / 2000 # 기본 지정값(523-1)
|
||||
assert ((0.0, 0.0), (40.0 * mm_h, 0.0)) in chords # 경계현 level_base=0
|
||||
assert ((6.0 * mm_h, 300.0 * 0.02), (34.0 * mm_h, 300.0 * 0.02)) in chords # 중간 300㎥
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* =============================================================================
|
||||
* ui_template_modal.ts
|
||||
* 공용 모달 겉틀 — 배경 · 제목줄 · 닫기 · Esc · 포커스. 몸은 `mount` 가 채움.
|
||||
* 바깥 누르기 = 누름 · 뗌 둘 다 배경일 때만 닫음(칸 끌기로 안 닫힘).
|
||||
* M01 · M02 가 함께 씀 — `dialogClass` · `backdropClass` 로 화면별 겉모양을 덧붙임.
|
||||
* 곁값 — `actions`(머리 [닫기] 바로 왼쪽 단추 · mount 셋째 인자 자리에 넣어도 됨) · `wide`(큰 창 96vw × 94vh).
|
||||
* 안 넘긴 창은 지금 꼴 그대로.
|
||||
@@ -49,7 +50,11 @@ export function openModal(opts: ModalOptions): { close: () => void } {
|
||||
className: `ui-modal__backdrop ${opts.backdropClass ?? ""}`.trim(),
|
||||
children: [dialog],
|
||||
});
|
||||
backdrop.addEventListener("click", (ev) => ev.target === backdrop && void ask());
|
||||
// 바깥 누르기 = 누름 · 뗌 둘 다 바깥일 때만 — 칸 안에서 끌어 바깥에서 떼면(그 반대도) 안 닫음
|
||||
let outside = false;
|
||||
backdrop.addEventListener("pointerdown", (ev) => (outside = ev.target === backdrop));
|
||||
backdrop.addEventListener("pointerup", (ev) => (outside &&= ev.target === backdrop));
|
||||
backdrop.addEventListener("click", (ev) => ev.target === backdrop && outside && void ask());
|
||||
backdrop.addEventListener("keydown", (ev) => ev.key === "Escape" && void ask());
|
||||
opts.mount(body, close, actions);
|
||||
// 단추가 있을 때만 머리에 — [닫기] 는 머리 직속 그대로(B05 추천의 `.ui-modal__top > button` 갈아 끼우기)
|
||||
|
||||
Reference in New Issue
Block a user