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;
|
||||
|
||||
Reference in New Issue
Block a user