feat(M02): 도면 카드 미리보기에서 단축키로 그리기 · 고치기 · 카드 [저장] · ? 도움말(PLAN 31-4)
- 새 M02_MasterTemplete_DrawingCard — 고칠 권한 + 저장 길이면 CAD 를 고치기로(보기만 끔) · 제목줄 · 리본 · 패널 · 상태줄 숨김(같은 출처 스타일 · CAD 앱 안 고침) · 전체 편집기 [도면 수정]과 같은 단축키 - 카드 [저장] · Ctrl+S = 전체 편집기와 같은 저장 길(saveTemplate · 읽은 문서에 도면 칸만) - ItemSource write(M02 화면 자료 자리만 · 모달 복사본은 보기만) - 저장 안 한 카드 고침 — 다른 항목 열 때 · 페이지 떠날 때 물음(ItemCards · Structure dirty · UI_Main 두 줄) - 시험 test_m02_drawing_card(새) · structure_panel · item_picker_detail 모양 맞춤 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BU9gbNE3aWFUvE61pS2WGt
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_DrawingCard.ts
|
||||
* 항목 카드 「도면 정보」 미리보기 — 고칠 수 있으면 메뉴(제목줄 · 리본 · 패널 · 상태줄) 없이
|
||||
* **단축키로 그리기 · 고치기**(전체 편집기 [도면 수정]과 같은 웹캐드 · 같은 단축키) · 카드 안 [저장]
|
||||
* (전체 편집기와 같은 저장 길 = 그 항목 구조물 도면 파일 `도면` 칸만 갈아 끼움) · ? = 단축키 도움말
|
||||
* (PLAN 31-4). 고칠 수 없으면(읽기 층 · 저장 길 없는 자료 자리) 예전처럼 보기만.
|
||||
* CAD 앱은 안 고침 — 메뉴는 같은 출처 iframe 에 스타일을 얹어 숨김.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
import { StaleError, type StructureDoc } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { mountDrawingTemplate, type DrawingTemplateDoc } from "./M02_MasterTemplete_Drawing";
|
||||
import type { ItemSource } from "./M02_MasterTemplete_ItemSource";
|
||||
|
||||
const TEXT = {
|
||||
save: "저장",
|
||||
help: "?",
|
||||
helpTitle: "단축키 — 글자를 치고 Enter",
|
||||
saved: "도면을 저장함",
|
||||
stale: "그 사이 도면이 바뀜 — 항목을 다시 골라 새로 읽음",
|
||||
failed: "도면을 저장 못 함 — {value}",
|
||||
};
|
||||
|
||||
/** 단축키 도움말 — 전체 편집기 명령 별칭(openwebcad `commands.*.ts`)과 같은 것만 */
|
||||
const KEYS: [string, string][] = [
|
||||
["L · PL", "선 · 폴리선"],
|
||||
["C · A · REC", "원 · 호 · 사각형"],
|
||||
["T · DT", "글(여러 줄 · 한 줄)"],
|
||||
["DLI", "치수"],
|
||||
["M · CO · RO", "옮기기 · 복사 · 회전"],
|
||||
["SC · MI · O", "축척 · 대칭 · 간격 띄우기"],
|
||||
["TR · EX · F", "자르기 · 연장 · 모깎기"],
|
||||
["E · Delete", "지우기"],
|
||||
["누름 · 끌기 · Ctrl+A", "고르기 · 전부"],
|
||||
["Esc", "고름 풀기 · 명령 멈춤"],
|
||||
["Ctrl+Z · Ctrl+Y", "되돌리기 · 다시 하기"],
|
||||
["Ctrl+S", "저장(카드 [저장]과 같음)"],
|
||||
["휠 · 가운데 끌기", "확대·축소 · 화면 옮기기"],
|
||||
];
|
||||
|
||||
/** 보기만 = 제목줄 · 리본만 숨김(예전 그대로) · 고치기 = 패널 · 상태줄까지(명령 안내 줄은 남김) */
|
||||
const HIDE_VIEW = ".cad-titlebar, .cad-ribbon { display: none !important; }";
|
||||
const HIDE_EDIT =
|
||||
".cad-titlebar, .cad-ribbon, .cad-inspector, .cad-statusbar, .cad-quick-access" +
|
||||
" { display: none !important; }";
|
||||
|
||||
/** 같은 출처 iframe 안쪽에 스타일 한 벌 — 다시 뜨면(load) 또 얹음 */
|
||||
function hideChrome(frame: HTMLIFrameElement, css: string): void {
|
||||
const apply = (): void => {
|
||||
const inner = frame.contentDocument;
|
||||
if (!inner?.head || inner.getElementById("m02-struct-hide")) return;
|
||||
const style = inner.createElement("style");
|
||||
style.id = "m02-struct-hide";
|
||||
style.textContent = css;
|
||||
inner.head.append(style);
|
||||
};
|
||||
frame.addEventListener("load", apply);
|
||||
apply();
|
||||
}
|
||||
|
||||
export interface DrawingCardOptions {
|
||||
/** 구조물 도면 문서 통째(`도번` · `도면` …) — 저장은 `도면` 칸만 갈아 끼움 */
|
||||
doc: StructureDoc;
|
||||
/** 읽은 판 — 저장 때 보냄(다르면 409) */
|
||||
version: string | null;
|
||||
source: ItemSource;
|
||||
column: string;
|
||||
/** 고칠 권한(층) — 자료 자리에 저장 길(`write`)도 있어야 고치기 */
|
||||
editable: boolean;
|
||||
/** 저장 안 한 고침이 생김 · 없어짐 */
|
||||
onDirty?: (dirty: boolean) => void;
|
||||
}
|
||||
|
||||
export interface DrawingCardHandle {
|
||||
/** 카드 머리에 붙일 [저장] · ? (보기만이면 빈 칸) */
|
||||
tools: HTMLElement;
|
||||
dirty: () => boolean;
|
||||
destroy: () => void;
|
||||
}
|
||||
|
||||
export function mountDrawingCard(host: HTMLElement, opt: DrawingCardOptions): DrawingCardHandle {
|
||||
const write = opt.source.write;
|
||||
const edit = opt.editable && !!write;
|
||||
let doc = opt.doc;
|
||||
let version = opt.version ?? "";
|
||||
let dirty = false;
|
||||
let saving = false;
|
||||
|
||||
const save = createButton({ label: TEXT.save, variant: "filled", size: "sm" });
|
||||
const help = createButton({ label: TEXT.help, variant: "ghost", size: "sm" });
|
||||
help.title = help.ariaLabel = TEXT.helpTitle;
|
||||
const helpBox = el("div", {
|
||||
className: "m02-struct__keys",
|
||||
attrs: { hidden: "" },
|
||||
children: [
|
||||
el("strong", { text: TEXT.helpTitle }),
|
||||
el("dl", {
|
||||
children: KEYS.flatMap(([key, what]) => [
|
||||
el("dt", { text: key }),
|
||||
el("dd", { text: what }),
|
||||
]),
|
||||
}),
|
||||
],
|
||||
});
|
||||
help.addEventListener("click", () => (helpBox.hidden = !helpBox.hidden));
|
||||
|
||||
const setDirty = (next: boolean): void => {
|
||||
dirty = next;
|
||||
save.disabled = !next || saving;
|
||||
opt.onDirty?.(next);
|
||||
};
|
||||
|
||||
/** 전체 편집기 [저장]과 같은 길 — 읽은 문서에 `도면` 만 갈아 끼워 그 항목 파일에 */
|
||||
async function doSave(drawing?: DrawingTemplateDoc): Promise<void> {
|
||||
if (!write || saving) return;
|
||||
saving = true;
|
||||
save.disabled = true;
|
||||
try {
|
||||
const next = { ...doc, 도면: (drawing ?? (await handle.getDoc())) as StructureDoc["도면"] };
|
||||
version = (await write("structure", opt.column, version, next)).판;
|
||||
doc = next;
|
||||
setDirty(false);
|
||||
showToast(TEXT.saved, "success");
|
||||
} catch (error) {
|
||||
const why = error instanceof Error ? error.message : "";
|
||||
showToast(
|
||||
error instanceof StaleError ? TEXT.stale : TEXT.failed.replace("{value}", why),
|
||||
"error",
|
||||
);
|
||||
} finally {
|
||||
saving = false;
|
||||
save.disabled = !dirty;
|
||||
}
|
||||
}
|
||||
save.addEventListener("click", () => void doSave());
|
||||
save.disabled = true;
|
||||
|
||||
const handle = mountDrawingTemplate(host, doc.도면 as unknown as DrawingTemplateDoc, {
|
||||
readOnly: !edit,
|
||||
name: `preview:${opt.column}`, // 자동백업 칸은 전체 편집 화면과 갈라 둠
|
||||
layer: opt.source.layer,
|
||||
projectId: opt.source.projectId,
|
||||
panelsCollapsed: true,
|
||||
preview: !edit, // 보기만 = 왼쪽 끌기 화면 옮기기 · 고치기 = 누름 고르기 · 단축키
|
||||
originMarker: true, // 좌표 0,0 기준점 십자
|
||||
noArea: true, // 구조물도는 도각이 없음
|
||||
...(edit ? { onChanged: setDirty, onSave: (drawing) => doSave(drawing) } : {}),
|
||||
});
|
||||
const frame = host.querySelector("iframe");
|
||||
if (frame) hideChrome(frame, edit ? HIDE_EDIT : HIDE_VIEW);
|
||||
if (edit) host.append(helpBox);
|
||||
|
||||
return {
|
||||
tools: el("span", { className: "m02-struct__tools", children: edit ? [help, save] : [] }),
|
||||
dirty: () => dirty,
|
||||
destroy: () => {
|
||||
handle.destroy();
|
||||
helpBox.remove();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -8,16 +8,12 @@
|
||||
* 덧붙는 카드는 `extraCards` 로 끼움 — 선택 입력 카드(`M02_MasterTemplete_InputsCard.ts`)가 그 자리.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements";
|
||||
import { attachCollapsible } from "@ui/ui_template_collapsible";
|
||||
import { showLoading } from "@ui/ui_template_loading";
|
||||
import type { SpreadsheetHandle, Workbook } from "../A00_Common/spreadsheet/spreadsheet";
|
||||
import type { StructureDoc, StructureNumbers } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import {
|
||||
mountDrawingTemplate,
|
||||
type DrawingTemplateDoc,
|
||||
type DrawingTemplateHandle,
|
||||
} from "./M02_MasterTemplete_Drawing";
|
||||
import { mountDrawingCard, type DrawingCardHandle } from "./M02_MasterTemplete_DrawingCard";
|
||||
import type { ItemPart, ItemSource } from "./M02_MasterTemplete_ItemSource";
|
||||
import "./M02_MasterTemplete_Structure.css";
|
||||
|
||||
@@ -45,6 +41,7 @@ const TEXT = {
|
||||
noFieldNumber: "{value} 없음",
|
||||
noSheet: "스프레드시트를 못 띄움 — {value}",
|
||||
readFailed: "구조물 도면을 못 읽음 — {value}",
|
||||
discard: "도면 카드에 저장 안 한 고침이 있음 — 버리고 다른 항목을 열까요?",
|
||||
};
|
||||
|
||||
const why = (error: unknown): string => (error instanceof Error ? error.message : "");
|
||||
@@ -75,6 +72,8 @@ export interface ItemCardsOptions {
|
||||
warnOnFail?: (column: string) => boolean;
|
||||
/** 세 카드 뒤에 덧붙는 카드(선택 입력 …) */
|
||||
extraCards?: HTMLElement[];
|
||||
/** 도면 카드에서 저장 안 한 고침이 생김 · 없어짐(PLAN 31-4) */
|
||||
onDirty?: (dirty: boolean) => void;
|
||||
}
|
||||
|
||||
export interface ItemCardsHandle {
|
||||
@@ -87,13 +86,16 @@ export interface ItemCardsHandle {
|
||||
relabel: (labels: Partial<Record<EditKind, string | null>>) => void;
|
||||
/** 지금 보이는 열 id */
|
||||
column: () => string | null;
|
||||
/** 도면 카드에 저장 안 한 고침이 있나 — 페이지가 떠날 때 물음 */
|
||||
dirty: () => boolean;
|
||||
destroy: () => void;
|
||||
}
|
||||
|
||||
export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
|
||||
let column: string | null = null;
|
||||
let doc: StructureDoc | null = null;
|
||||
let preview: DrawingTemplateHandle | null = null;
|
||||
let docVersion: string | null = null;
|
||||
let preview: DrawingCardHandle | null = null;
|
||||
let seq = 0;
|
||||
|
||||
const numberOf = (kind: EditKind, id: string): string => opts.numberOf?.(kind, id) ?? "";
|
||||
@@ -102,9 +104,13 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
|
||||
const number = el("span", { className: "m02-struct__no" });
|
||||
const edit = createButton({ label: TEXT.edit, variant: "filled", size: "sm" });
|
||||
const previewHost = el("div", { className: "m02-struct__preview" });
|
||||
const drawingTools = el("span"); // 카드 안 ? · [저장](31-4 · 고칠 수 있을 때)
|
||||
const infoCard = itemCard(
|
||||
TEXT.drawingInfo,
|
||||
el("div", { className: "m02-struct__head", children: [number, columnName, edit] }),
|
||||
el("div", {
|
||||
className: "m02-struct__head",
|
||||
children: [number, columnName, drawingTools, edit],
|
||||
}),
|
||||
previewHost,
|
||||
);
|
||||
|
||||
@@ -166,26 +172,13 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
|
||||
const drop = (): void => {
|
||||
preview?.destroy();
|
||||
preview = null;
|
||||
drawingTools.replaceChildren();
|
||||
for (const c of sheets) {
|
||||
c.handle?.destroy();
|
||||
c.handle = null;
|
||||
}
|
||||
};
|
||||
|
||||
/** 미리보기 CAD 는 iframe — 안쪽 도구 모음(제목줄 · 리본)을 스타일로 숨김(같은 출처라 접근 가능) */
|
||||
const hideToolbar = (frame: HTMLIFrameElement): void => {
|
||||
const apply = (): void => {
|
||||
const inner = frame.contentDocument;
|
||||
if (!inner?.head || inner.getElementById("m02-struct-hide")) return;
|
||||
const style = inner.createElement("style");
|
||||
style.id = "m02-struct-hide";
|
||||
style.textContent = ".cad-titlebar, .cad-ribbon { display: none !important; }";
|
||||
inner.head.append(style);
|
||||
};
|
||||
frame.addEventListener("load", apply);
|
||||
apply();
|
||||
};
|
||||
|
||||
/** 스프레드시트 미리보기 — 보기만(고침은 [수정] 이 여는 전체 화면) · 부품을 늦게 받음 ·
|
||||
* 그 사이 열이 바뀌면 버림 */
|
||||
const paintSheet = async (c: SheetCard, mine: number): Promise<void> => {
|
||||
@@ -218,25 +211,24 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
|
||||
return;
|
||||
}
|
||||
previewHost.replaceChildren();
|
||||
const source = opts.source();
|
||||
// 미리보기 — 도면 양식과 같은 부품을 읽기만으로. 자동백업 칸은 편집 화면과 갈라 둠.
|
||||
preview = mountDrawingTemplate(previewHost, doc.도면 as unknown as DrawingTemplateDoc, {
|
||||
readOnly: true,
|
||||
name: `preview:${column ?? ""}`,
|
||||
layer: source.layer,
|
||||
projectId: source.projectId,
|
||||
panelsCollapsed: true, // 특성 · 도면층 패널은 접힌 채 — › 를 누르면 펼침
|
||||
preview: true, // 왼쪽 끌기 = 화면 옮기기 · 두 번 누름 = 전체 맞춤
|
||||
originMarker: true, // 좌표 0,0 기준점 십자
|
||||
noArea: true, // 구조물도는 도각이 없음 — 작도 영역 단추 없음
|
||||
// 미리보기 — 고칠 수 있으면 메뉴 없이 단축키로 고치고 카드 [저장](31-4 · `DrawingCard`)
|
||||
preview = mountDrawingCard(previewHost, {
|
||||
doc,
|
||||
version: docVersion,
|
||||
source: opts.source(),
|
||||
column: at,
|
||||
editable: opts.editable(),
|
||||
onDirty: (d) => opts.onDirty?.(d),
|
||||
});
|
||||
const frame = previewHost.querySelector("iframe");
|
||||
if (frame) hideToolbar(frame);
|
||||
drawingTools.replaceChildren(preview.tools);
|
||||
};
|
||||
|
||||
async function load(col: { id: string; label: string } | null): Promise<void> {
|
||||
// 도면 카드 고침을 저장 안 했으면 버릴지 물음 — 취소면 보던 열 그대로
|
||||
if (preview?.dirty() && col?.id !== column && !(await showConfirmDialog(TEXT.discard))) return;
|
||||
const mine = ++seq;
|
||||
drop();
|
||||
opts.onDirty?.(false);
|
||||
if (!col) {
|
||||
root.hidden = true;
|
||||
column = null;
|
||||
@@ -248,6 +240,7 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
|
||||
columnName.textContent = col.label;
|
||||
number.textContent = numberOf("structure", col.id) || TEXT.noNumber;
|
||||
doc = null;
|
||||
docVersion = null;
|
||||
previewHost.replaceChildren();
|
||||
for (const c of sheets) {
|
||||
c.doc = null;
|
||||
@@ -263,7 +256,10 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
|
||||
]);
|
||||
stops.forEach((stop) => stop());
|
||||
if (mine !== seq) return;
|
||||
if (drawing.status === "fulfilled") doc = drawing.value.문서 as StructureDoc;
|
||||
if (drawing.status === "fulfilled") {
|
||||
doc = drawing.value.문서 as StructureDoc;
|
||||
docVersion = drawing.value.판 ?? null;
|
||||
}
|
||||
// 도번 목록에 있는 열인데 못 읽으면 알림 — 없는 열은 그냥 「도면 없음」
|
||||
else if (opts.warnOnFail?.(col.id) ?? true)
|
||||
showToast(fill(TEXT.readFailed, why(drawing.reason)), "error");
|
||||
@@ -291,6 +287,7 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
|
||||
}
|
||||
},
|
||||
column: () => column,
|
||||
dirty: () => !!preview?.dirty(),
|
||||
destroy: () => {
|
||||
seq += 1;
|
||||
drop();
|
||||
|
||||
@@ -5,7 +5,12 @@
|
||||
* 카드(`M02_MasterTemplete_ItemCards.ts`)는 어디서 읽는지 모르고 이 창구만 부름.
|
||||
* ========================================================================== */
|
||||
|
||||
import { getActiveLayer, readTemplate, type Layer } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import {
|
||||
getActiveLayer,
|
||||
readTemplate,
|
||||
saveTemplate,
|
||||
type Layer,
|
||||
} from "./M02_MasterTemplete_Api_Fetch";
|
||||
|
||||
/** 집계표 열 하나에 딸린 문서 셋 — 구조물 도면 · 면적산출 · 재료산출 */
|
||||
export type ItemPart = "structure" | "area" | "material";
|
||||
@@ -15,7 +20,9 @@ export interface ItemSource {
|
||||
layer: Layer;
|
||||
projectId: string | null;
|
||||
/** 열 id 의 문서 하나 — 못 읽으면 던짐(카드가 「없음」 · 알림으로 갈라 보임) */
|
||||
read: (part: ItemPart, column: string) => Promise<{ 문서: unknown }>;
|
||||
read: (part: ItemPart, column: string) => Promise<{ 문서: unknown; 판?: string | null }>;
|
||||
/** 카드 안 [저장](PLAN 31-4) — 전체 편집기와 같은 저장 길 · 없으면 카드는 보기만 */
|
||||
write?: (part: ItemPart, column: string, 판: string, 문서: unknown) => Promise<{ 판: string }>;
|
||||
}
|
||||
|
||||
/** M02 화면의 자료 자리 — 부를 때의 지금 층(`getActiveLayer`) 그대로 */
|
||||
@@ -25,5 +32,6 @@ export const layerSource = (): ItemSource => {
|
||||
layer,
|
||||
projectId: null,
|
||||
read: (part, column) => readTemplate(layer, part, column, null),
|
||||
write: (part, column, 판, 문서) => saveTemplate(layer, part, column, null, 판, 문서),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -99,6 +99,57 @@
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* 도면 카드 고치기(PLAN 31-4) — 머리 오른쪽 ? · [저장] · ? = 칸 오른쪽 위 단축키 도움말 */
|
||||
.m02-struct__tools {
|
||||
display: inline-flex;
|
||||
gap: var(--spacing-8);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.m02-struct__head .m02-struct__tools .ui-btn,
|
||||
.m02-struct__tools:not(:empty) + .ui-btn {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.m02-struct__preview:has(.m02-struct__keys) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.m02-struct__keys {
|
||||
position: absolute;
|
||||
top: var(--spacing-8);
|
||||
right: var(--spacing-8);
|
||||
z-index: 2;
|
||||
max-height: calc(100% - 2 * var(--spacing-8));
|
||||
overflow: auto;
|
||||
padding: var(--spacing-8) var(--spacing-12);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-buttons);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
|
||||
.m02-struct__keys[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.m02-struct__keys dl {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 2px var(--spacing-12);
|
||||
margin: var(--spacing-8) 0 0;
|
||||
}
|
||||
|
||||
.m02-struct__keys dt {
|
||||
font-family: var(--font-mono, monospace);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.m02-struct__keys dd {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 면적산출 · 재료산출 미리보기 — `.ss` 는 height:100% 로 부모 높이를 그대로 받는데 부모(이 칸)에 높이가
|
||||
없으면 0 이 되어 칸이 안 보였음(진짜 원인). 약 30행이 늘 보이게 높이를 정하고
|
||||
넘치면 격자 안쪽(.ss-grid-host)이 스스로 스크롤(spreadsheet_grid.css). */
|
||||
|
||||
@@ -278,6 +278,8 @@ export interface StructurePanelHandle {
|
||||
saveInputs: () => Promise<void>;
|
||||
/** 고른 열이 바뀜 — 같은 열이면 이름만 새로(CAD 를 다시 안 띄움) */
|
||||
show: (colId: string | null, columns: StructureColumn[]) => void;
|
||||
/** 도면 카드에 저장 안 한 고침(31-4 · 카드 [저장] 몫) — 페이지가 떠날 때 함께 물음 */
|
||||
dirty: () => boolean;
|
||||
/** 보던 열을 다시 읽음 — 집계표 [저장] 뒤(서버가 새 열 파일 · 번호를 만듦) */
|
||||
refresh: (columns: StructureColumn[]) => void;
|
||||
/** 집계표 조회 후 선택(`onPick`) — 일위대가 = 고른 키(「비움」 = 빈 글 · 닫으면 null) ·
|
||||
@@ -413,6 +415,7 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane
|
||||
inputs.invalidate(); // 새 열 · 바뀐 일위대가를 서버 항목 목록에서 다시 찾게
|
||||
},
|
||||
saveInputs: () => inputs.save(),
|
||||
dirty: () => cards.dirty(),
|
||||
show,
|
||||
refresh: (columns) => {
|
||||
const at = column;
|
||||
|
||||
@@ -182,13 +182,13 @@ export function createMain(
|
||||
|
||||
const guard = (event: BeforeUnloadEvent): void => {
|
||||
if (!root.isConnected) return window.removeEventListener("beforeunload", guard);
|
||||
if (!dirty) return;
|
||||
if (!dirty && !panel?.dirty()) return;
|
||||
event.preventDefault();
|
||||
event.returnValue = "";
|
||||
};
|
||||
window.addEventListener("beforeunload", guard);
|
||||
const confirmLeave = async (): Promise<boolean> => {
|
||||
if (!dirty) return true;
|
||||
if (!dirty && !panel?.dirty()) return true; // 도면 카드 고침(31-4)도 같은 물음
|
||||
if (!(await showConfirmDialog(L("M02_DiscardConfirm")))) {
|
||||
// 취소 — 초점을 시트(고른 칸 입력 자리) · 집계표 · 도면으로 돌려 키보드 그대로
|
||||
host
|
||||
|
||||
@@ -131,7 +131,9 @@ def test_모달은_M02_와_같은_카드_한_벌을_씀():
|
||||
for name in ("ui_template_item_picker_detail.ts", "ui_template_item_picker.ts"):
|
||||
text = (UI / name).read_text(encoding="utf-8")
|
||||
assert "mountDrawingTemplate(" not in text and "createSpreadsheet(" not in text, name
|
||||
assert cards.count("mountDrawingTemplate(") == 1
|
||||
assert cards.count("mountDrawingCard(") == 1 # 도면 미리보기 = 카드 파일에서 뗀 부품 하나(31-4)
|
||||
drawing_card = (M02 / "M02_MasterTemplete_DrawingCard.ts").read_text(encoding="utf-8")
|
||||
assert drawing_card.count("mountDrawingTemplate(") == 1
|
||||
|
||||
|
||||
def test_선택_입력_카드는_sub2_파일_한_벌(out):
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
"""도면 카드 미리보기 수정(PLAN 31-4) — 메뉴 없이 단축키로 고치기 · 카드 [저장] · 떠날 때 물음.
|
||||
|
||||
코드 글 모양으로 봄(CAD 는 iframe · 화면 검증은 ORCA).
|
||||
- 고치기 = 층 고칠 권한 + 자료 자리의 저장 길(`write`) 둘 다 · 아니면 예전처럼 보기만.
|
||||
- 고칠 때 CAD 메뉴(제목줄 · 리본 · 패널 · 상태줄)는 숨김 · CAD 앱은 안 고침(같은 출처 스타일).
|
||||
- 저장 = 전체 편집기와 같은 길(`saveTemplate`) · 읽은 문서에 `도면` 만 갈아 끼움 · Ctrl+S 도 같음.
|
||||
- 저장 안 한 카드 고침 = 다른 항목 열기 · 페이지 떠나기 때 물음.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
M02 = ROOT / "M02_MasterTemplete"
|
||||
|
||||
|
||||
def _text(name: str) -> str:
|
||||
return (M02 / name).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_고치기는_권한과_저장_길_둘_다():
|
||||
card = _text("M02_MasterTemplete_DrawingCard.ts")
|
||||
assert "const edit = opt.editable && !!write;" in card
|
||||
assert "readOnly: !edit," in card and "preview: !edit," in card
|
||||
assert "onSave: (drawing) => doSave(drawing)" in card # CAD Ctrl+S · 💾 = 카드 저장
|
||||
source = _text("M02_MasterTemplete_ItemSource.ts")
|
||||
assert (
|
||||
"write: (part, column, 판, 문서) => saveTemplate(layer, part, column, null, 판, 문서)"
|
||||
in source
|
||||
)
|
||||
|
||||
|
||||
def test_메뉴는_숨기고_저장은_도면_칸만():
|
||||
card = _text("M02_MasterTemplete_DrawingCard.ts")
|
||||
for part in (".cad-titlebar", ".cad-ribbon", ".cad-inspector", ".cad-statusbar"):
|
||||
assert part in card.split("const HIDE_EDIT")[1].split(";\n")[0]
|
||||
assert "const next = { ...doc, 도면:" in card # 도번 · 열 · 판 은 읽은 그대로
|
||||
assert 'write("structure", opt.column, version, next)' in card
|
||||
for key in ("L · PL", "E · Delete", "Ctrl+Z · Ctrl+Y", "Ctrl+S"):
|
||||
assert f'["{key}"' in card # ? 도움말 — 전체 편집기 명령 별칭과 같은 것
|
||||
|
||||
|
||||
def test_저장_안_한_카드_고침은_물음():
|
||||
cards = _text("M02_MasterTemplete_ItemCards.ts")
|
||||
assert (
|
||||
"if (preview?.dirty() && col?.id !== column && !(await showConfirmDialog(TEXT.discard)))"
|
||||
in cards
|
||||
)
|
||||
assert "dirty: () => !!preview?.dirty()," in cards
|
||||
assert "dirty: () => cards.dirty()," in _text("M02_MasterTemplete_Structure.ts")
|
||||
main = _text("M02_MasterTemplete_UI_Main.ts")
|
||||
assert main.count("!dirty && !panel?.dirty()") == 2 # 떠날 때 물음 · 창 닫기 막음
|
||||
@@ -22,6 +22,7 @@ ROOT = Path(__file__).resolve().parents[2]
|
||||
M02 = ROOT / "M02_MasterTemplete"
|
||||
PANEL = M02 / "M02_MasterTemplete_Structure.ts"
|
||||
CARDS = M02 / "M02_MasterTemplete_ItemCards.ts" # 카드 셋 그리기(25-2d 에서 PANEL 에서 옮김)
|
||||
DRAWING_CARD = M02 / "M02_MasterTemplete_DrawingCard.ts" # 도면 카드(31-4 · 카드 파일에서 뗌)
|
||||
MAIN = M02 / "M02_MasterTemplete_UI_Main.ts"
|
||||
SIDE = M02 / "M02_MasterTemplete_UI_Side.ts"
|
||||
|
||||
@@ -54,7 +55,8 @@ def test_소스가_700줄을_넘지_않음():
|
||||
# (`{열 id: {도번, 면적번, 재료번}}`)
|
||||
("M02_MasterTemplete_Api_Fetch.ts", ["fetchStructureNumbers"], SIDE),
|
||||
("M02_MasterTemplete_Structure.ts", ["structureLabel", "isStructureColumn"], SIDE),
|
||||
("M02_MasterTemplete_Drawing.ts", ["mountDrawingTemplate"], CARDS),
|
||||
# 도면 카드(31-4)는 카드 파일에서 뗀 `DrawingCard` 가 부름
|
||||
("M02_MasterTemplete_Drawing.ts", ["mountDrawingTemplate"], DRAWING_CARD),
|
||||
("../A00_Common/spreadsheet/spreadsheet.ts", ["createSpreadsheet"], CARDS),
|
||||
],
|
||||
)
|
||||
@@ -132,9 +134,11 @@ def test_열_머리_글_잇기():
|
||||
|
||||
|
||||
def test_미리보기_CAD_패널은_숨기지_않고_접힌_채로_엶():
|
||||
"""특성 · 도면층 패널을 숨기지 않음 — 곁값 panelsCollapsed 로 접어 실음(B07 은 안 보냄)."""
|
||||
panel = _text(CARDS)
|
||||
assert ".cad-inspector" not in panel and "panelsCollapsed: true" in panel
|
||||
"""보기만 카드는 특성 · 도면층 패널을 숨기지 않음 — 곁값 panelsCollapsed 로 접어 실음(B07 은 안
|
||||
보냄) · 고치기 카드(31-4)만 패널까지 숨기고 단축키로 고침."""
|
||||
panel = _text(M02 / "M02_MasterTemplete_DrawingCard.ts")
|
||||
view = panel.split("const HIDE_VIEW = ")[1].split(";\n")[0]
|
||||
assert ".cad-inspector" not in view and "panelsCollapsed: true" in panel
|
||||
drawing = _text(M02 / "M02_MasterTemplete_Drawing.ts")
|
||||
assert "panelsCollapsed: options.panelsCollapsed" in drawing
|
||||
assert "panelsCollapsed?: boolean" in _text(ROOT / "ui_template/cad_host/cad_host.ts")
|
||||
|
||||
Reference in New Issue
Block a user