Merge remote-tracking branch 'origin/sub_laptop_3' into main_laptop_1
충돌(DrawingCard.ts) = 47-6 mountStructure 한 벌을 취하고 47-2 noImport 를 그 안에 옮김
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_AreaDrawing.ts
|
||||
* 면적산출 상세 오른쪽 「도면」 패널 몸(PLAN 47-6) — 그 열 구조물 도면을 보기만(확대 · 이동).
|
||||
* 그리기는 구조물 조합 도면 카드 미리보기와 같은 길(`DrawingCard.mountDrawingView`).
|
||||
* 처음 펼 때 읽고 그림 — 접힌 채 iframe 을 띄우지 않음.
|
||||
* ========================================================================== */
|
||||
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { readTemplate, type Layer, type StructureDoc } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { mountDrawingView } from "./M02_MasterTemplete_DrawingCard";
|
||||
|
||||
const TEXT = {
|
||||
loading: "도면 불러오는 중…",
|
||||
none: "이 열은 구조물 도면이 없음",
|
||||
};
|
||||
|
||||
export interface AreaDrawing {
|
||||
root: HTMLElement;
|
||||
/** 패널이 펼쳐짐 — 처음 한 번만 읽고 그림 */
|
||||
show: () => void;
|
||||
destroy: () => void;
|
||||
}
|
||||
|
||||
export function createAreaDrawing(layer: Layer, column: string): AreaDrawing {
|
||||
const root = el("div", { className: "m02-apanel__drawing" });
|
||||
let view: { destroy: () => void } | null = null;
|
||||
let started = false;
|
||||
let dead = false;
|
||||
const note = (text: string): void =>
|
||||
root.replaceChildren(el("p", { className: "m02-apanel__note", text }));
|
||||
|
||||
const show = (): void => {
|
||||
if (started) return;
|
||||
started = true;
|
||||
note(TEXT.loading);
|
||||
readTemplate(layer, "structure", column, null)
|
||||
.then((got) => {
|
||||
if (dead) return;
|
||||
const doc = got.문서 as StructureDoc | null;
|
||||
if (!doc?.도면) return note(TEXT.none);
|
||||
root.replaceChildren();
|
||||
view = mountDrawingView(root, doc, { layer, projectId: null }, column);
|
||||
})
|
||||
.catch(() => !dead && note(TEXT.none));
|
||||
};
|
||||
|
||||
return {
|
||||
root,
|
||||
show,
|
||||
destroy: () => {
|
||||
dead = true;
|
||||
view?.destroy();
|
||||
root.remove();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,48 +1,25 @@
|
||||
/* M02 면적산출 상세 오른쪽 패널(일위대가 담은 로직 · 자재목록) · 칸 지정 모달 — 틀은 공용 사이드바 카드 ·
|
||||
접기 · 폭 손잡이 · 접기 손잡이 */
|
||||
/* M02 면적산출 상세 오른쪽 패널(틀 = 공용 오른쪽 접는 패널 · 일위대가 · 도면) · 칸 지정 모달 */
|
||||
|
||||
/* 편집 칸 = 시트 | 패널 — 패널 폭은 손잡이가 `--m02-apanel-width` 로 줌 */
|
||||
.m02-apanel {
|
||||
position: relative;
|
||||
/* 「도면」 패널 몸 — 패널 높이를 채움(CAD 는 높이를 정해야 보임) */
|
||||
.ui-side-panels__body:has(> .m02-apanel__drawing) {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
flex-direction: column;
|
||||
width: var(--m02-apanel-width, 36rem); /* 금액 칸 넷이 가로 굴림 없이 보이게 */
|
||||
min-width: 280px;
|
||||
max-width: 60%;
|
||||
min-height: 0;
|
||||
border-left: 1px solid var(--color-border);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.m02-apanel.is-resizing {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.m02-apanel.is-collapsed {
|
||||
width: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.m02-apanel.is-collapsed > .m02-apanel__body,
|
||||
.m02-apanel.is-collapsed > .ui-resizer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 접기 손잡이 — 패널 왼쪽 밖 세로 가운데(시트 위로) */
|
||||
.m02-apanel > .ui-workflow-overlay__handle--side {
|
||||
z-index: 6;
|
||||
right: auto;
|
||||
left: calc(-1 * var(--spacing-24));
|
||||
border-right: 0;
|
||||
border-radius: var(--radius-buttons) 0 0 var(--radius-buttons);
|
||||
}
|
||||
|
||||
/* 안쪽만 굴림 — 페이지 스크롤 안 생김 */
|
||||
.m02-apanel__body {
|
||||
.m02-apanel__drawing {
|
||||
display: flex;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: var(--spacing-8);
|
||||
}
|
||||
|
||||
.m02-apanel__drawing .cad-host {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.m02-apanel__note {
|
||||
margin: var(--spacing-12);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* 칸 지정 모달 — 지금 연결 한 줄 · [지금 고른 칸으로] [풀기] · 주소 칸 [잇기] */
|
||||
|
||||
@@ -1,32 +1,29 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_AreaPanel.ts
|
||||
* 면적산출 상세 오른쪽 패널(PLAN 39-4 D2 · D6) — 그 열 일위대가의 담은 로직 + 자재목록(기준 · 반영).
|
||||
* 몸 = 일위대가 보기 갈래(`Labor_View` · 고치기 없음) · 틀 = 공용 사이드바 카드 · 접기 손잡이 · 폭 손잡이
|
||||
* (폭 · 접힘 기억은 옛 조합 패널 키 그대로).
|
||||
* 면적산출 상세 오른쪽 패널(PLAN 39-4 D2 · D6 · 47-6) — 틀 = 공용 오른쪽 접는 패널(하나만 열림 · 폭 공유)
|
||||
* · 「일위대가」 = 그 열 일위대가의 담은 로직 + 자재목록(기준 · 반영) · 몸 = 일위대가 보기 갈래(`Labor_View`)
|
||||
* · 「도면」 = 그 열 구조물 도면 보기만(`AreaDrawing` · 처음 펼 때 그림)
|
||||
* · 줄 누름 = 칸 지정 모달(`AreaLink` · 그 줄 알약 키를 면적 칸에 잇기 · 풀기)
|
||||
* · 시트 칸 값 · 연결이 바뀌면 `refresh` — 받은 기준에 바로 곱함 · 칸 주소도 넘김(서버 안 부름 · `Labor_View.setQty`)
|
||||
* ========================================================================== */
|
||||
|
||||
import { el, showToast } from "@ui/ui_template_elements";
|
||||
import { attachCollapsible } from "@ui/ui_template_collapsible";
|
||||
import { createWorkflowPanelHandle } from "@ui/ui_template_overlay";
|
||||
import { createPanelResizer } from "@ui/ui_template_resizer";
|
||||
import { readStateRaw, stateKey, writeStateRaw } from "../A00_Common/b_page_state";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { createSidePanels } from "@ui/ui_template_side_panels";
|
||||
import type { LinkItem, SpreadsheetHandle } from "../A00_Common/spreadsheet/spreadsheet";
|
||||
import type { Layer } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { createAreaDrawing } from "./M02_MasterTemplete_AreaDrawing";
|
||||
import { openAreaLink } from "./M02_MasterTemplete_AreaLink";
|
||||
import { createLaborView, type LaborViewRow } from "./M02_MasterTemplete_Labor_View";
|
||||
import "./M02_MasterTemplete_AreaPanel.css";
|
||||
|
||||
const TEXT = {
|
||||
handle: "일위대가",
|
||||
title: "이 열의 일위대가",
|
||||
labor: "일위대가",
|
||||
drawing: "도면",
|
||||
noPill: "이 줄은 면적산출 알약이 없음 — 돈 아닌 로직이거나 계산 전",
|
||||
};
|
||||
|
||||
/** 최소 폭(px) · 최대 = 편집 칸 폭의 비율 */
|
||||
const MIN_WIDTH = 280;
|
||||
const MAX_RATIO = 0.6;
|
||||
/** 기억 없을 때 폭(px) — 금액 칸 넷이 가로 굴림 없이 보이게 */
|
||||
const DEFAULT_WIDTH = 576;
|
||||
|
||||
export interface AreaPanelOptions {
|
||||
layer: Layer;
|
||||
@@ -98,39 +95,18 @@ export function buildAreaPanel(opts: AreaPanelOptions): AreaPanel {
|
||||
});
|
||||
};
|
||||
const view = createLaborView(openLink);
|
||||
const body = el("div", {
|
||||
className: "m02-apanel__body ui-sidebar-panel",
|
||||
children: [
|
||||
el("section", {
|
||||
className: "ui-collapsible ui-sidebar-section ui-sidebar-card",
|
||||
children: [el("h3", { className: "ui-collapsible__title", text: TEXT.title }), view.root],
|
||||
}),
|
||||
const drawing = createAreaDrawing(opts.layer, opts.column);
|
||||
const panels = createSidePanels({
|
||||
prefKey: "m02-area-panels",
|
||||
defaultWidth: DEFAULT_WIDTH,
|
||||
panels: [
|
||||
{ id: "labor", title: TEXT.labor, body: view.root },
|
||||
{ id: "drawing", title: TEXT.drawing, body: drawing.root },
|
||||
],
|
||||
onChange: (open) => open === "drawing" && drawing.show(),
|
||||
});
|
||||
attachCollapsible(body);
|
||||
|
||||
const root = el("aside", { className: "m02-apanel" });
|
||||
const handle = createWorkflowPanelHandle("side", "right", TEXT.handle);
|
||||
// 왼쪽 경계를 끌어 폭 조절 — 폭은 브라우저에 기억 · 더블클릭 = 기본 폭
|
||||
const resizer = createPanelResizer({
|
||||
axis: "horizontal",
|
||||
target: root,
|
||||
cssVar: "--m02-apanel-width",
|
||||
direction: -1,
|
||||
min: MIN_WIDTH,
|
||||
max: () => (root.parentElement?.clientWidth ?? window.innerWidth) * MAX_RATIO,
|
||||
storageKey: stateKey("m02-combo-panel-width") ?? undefined,
|
||||
});
|
||||
root.append(handle.root, resizer.root, body);
|
||||
const setCollapsed = (collapsed: boolean): void => {
|
||||
root.classList.toggle("is-collapsed", collapsed);
|
||||
handle.setOpen(!collapsed);
|
||||
writeStateRaw("m02-combo-panel-collapsed", String(collapsed));
|
||||
};
|
||||
handle.root.addEventListener("click", () =>
|
||||
setCollapsed(!root.classList.contains("is-collapsed")),
|
||||
);
|
||||
setCollapsed(readStateRaw("m02-combo-panel-collapsed") === "true");
|
||||
if (panels.current() === "drawing") drawing.show();
|
||||
const root = panels.root;
|
||||
|
||||
// 처음 = 저장 안 한 시트 칸 값으로 셈(서버 calc 한 번)
|
||||
const refresh = (): void => view.setQty(quantities(opts.sheet()), cellsOf(opts.sheet()));
|
||||
@@ -141,7 +117,7 @@ export function buildAreaPanel(opts: AreaPanelOptions): AreaPanel {
|
||||
refresh,
|
||||
destroy: () => {
|
||||
view.destroy();
|
||||
resizer.dispose();
|
||||
drawing.destroy();
|
||||
root.remove();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -10,9 +10,16 @@
|
||||
|
||||
import type { StructureDoc } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import type { SaveControl } from "./M02_MasterTemplete_CardShell";
|
||||
import { mountDrawingTemplate, type DrawingTemplateDoc } from "./M02_MasterTemplete_Drawing";
|
||||
import {
|
||||
mountDrawingTemplate,
|
||||
type DrawingTemplateDoc,
|
||||
type DrawingTemplateHandle,
|
||||
type DrawingTemplateOptions,
|
||||
} from "./M02_MasterTemplete_Drawing";
|
||||
import type { ItemSource } from "./M02_MasterTemplete_ItemSource";
|
||||
|
||||
export const DRAWING_HELP = "단축키 — 글자를 치고 Enter";
|
||||
|
||||
/** 단축키 도움말 — 전체 편집기 명령 별칭(openwebcad `commands.*.ts`)과 같은 것만 */
|
||||
export const DRAWING_KEYS: [string, string][] = [
|
||||
["L · PL", "선 · 폴리선"],
|
||||
@@ -54,6 +61,45 @@ function hideChrome(frame: HTMLIFrameElement, css: string): void {
|
||||
export const drawingEditable = (editable: boolean, source: ItemSource): boolean =>
|
||||
editable && !!source.write;
|
||||
|
||||
type Where = Pick<ItemSource, "layer" | "projectId">;
|
||||
|
||||
/** 구조물 도면 미리보기 한 벌 — 카드 · 면적산출 도면 패널이 같이 씀 · `hooks` 있으면 고치기 */
|
||||
function mountStructure(
|
||||
host: HTMLElement,
|
||||
doc: StructureDoc,
|
||||
source: Where,
|
||||
column: string,
|
||||
hooks?: Pick<DrawingTemplateOptions, "onChanged" | "onSave">,
|
||||
): DrawingTemplateHandle {
|
||||
const edit = !!hooks;
|
||||
const handle = mountDrawingTemplate(host, doc.도면 as unknown as DrawingTemplateDoc, {
|
||||
readOnly: !edit,
|
||||
name: `preview:${column}`, // 자동백업 칸은 전체 편집 화면과 갈라 둠
|
||||
layer: source.layer,
|
||||
projectId: source.projectId,
|
||||
panelsCollapsed: true,
|
||||
preview: !edit, // 보기만 = 왼쪽 끌기 화면 옮기기 · 고치기 = 누름 고르기 · 단축키
|
||||
originMarker: true, // 좌표 0,0 기준점 십자
|
||||
noArea: true, // 구조물도는 도각이 없음
|
||||
noImport: true, // 파일 불러오기는 도면 상세에서만(47-2)
|
||||
...hooks,
|
||||
});
|
||||
const frame = host.querySelector("iframe");
|
||||
if (frame) hideChrome(frame, edit ? HIDE_EDIT : HIDE_VIEW);
|
||||
return handle;
|
||||
}
|
||||
|
||||
/** 보기만 도면 — 카드 미리보기의 보기 갈래와 같은 길(확대 · 이동만 · 면적산출 오른쪽 도면 패널 47-6) */
|
||||
export function mountDrawingView(
|
||||
host: HTMLElement,
|
||||
doc: StructureDoc,
|
||||
source: Where,
|
||||
column: string,
|
||||
): { destroy: () => void } {
|
||||
const handle = mountStructure(host, doc, source, column);
|
||||
return { destroy: () => handle.destroy() };
|
||||
}
|
||||
|
||||
export interface DrawingCardOptions {
|
||||
/** 구조물 도면 문서 통째(`도번` · `도면` …) — 저장은 `도면` 칸만 갈아 끼움 */
|
||||
doc: StructureDoc;
|
||||
@@ -87,24 +133,17 @@ export function mountDrawingCard(host: HTMLElement, opt: DrawingCardOptions): Dr
|
||||
doc = next;
|
||||
}
|
||||
|
||||
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, // 구조물도는 도각이 없음
|
||||
noImport: true, // 파일 불러오기는 도면 상세에서만(47-2)
|
||||
...(edit
|
||||
const handle = mountStructure(
|
||||
host,
|
||||
doc,
|
||||
opt.source,
|
||||
opt.column,
|
||||
edit
|
||||
? {
|
||||
onChanged: (dirty: boolean) => opt.saver.mark(dirty),
|
||||
onSave: (drawing: DrawingTemplateDoc) => void opt.saver.run(() => doSave(drawing)),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
const frame = host.querySelector("iframe");
|
||||
if (frame) hideChrome(frame, edit ? HIDE_EDIT : HIDE_VIEW);
|
||||
: undefined,
|
||||
);
|
||||
return { write: () => doSave(), destroy: () => handle.destroy() };
|
||||
}
|
||||
|
||||
@@ -157,11 +157,12 @@ def test_좌측_구조물_집계_양식_아래_하위_넷_새로_본떠_지우
|
||||
|
||||
|
||||
def test_면적_오른쪽_일위대가_패널은_공용_틀_손잡이():
|
||||
"""좌측과 같은 사이드바 카드 · 접기 · 공용 폭 손잡이 · 접기 손잡이 — 새로 그리지 않음(39-4 · 옛 조합 패널 지움)."""
|
||||
"""틀 = 공용 오른쪽 접는 패널(47-6 · 일위대가 · 도면) — 새로 그리지 않음(옛 조합 패널 지움)."""
|
||||
apanel = _text(M02 / "M02_MasterTemplete_AreaPanel.ts")
|
||||
assert "createSidePanels(" in apanel and "createAreaDrawing(" in apanel
|
||||
for name in ("createPanelResizer", "createWorkflowPanelHandle", "attachCollapsible"):
|
||||
assert name in apanel, name
|
||||
assert "ui-sidebar-card" in apanel and "setColumnCombo" not in apanel
|
||||
assert name not in apanel, name
|
||||
assert "setColumnCombo" not in apanel
|
||||
for gone in ("ComboPanel.ts", "ComboPanel.css", "Combo.ts", "Combo.css", "Combo_Api.ts"):
|
||||
assert not (M02 / f"M02_MasterTemplete_{gone}").exists(), gone
|
||||
api = _text(M02 / "M02_MasterTemplete_Api_Fetch.ts")
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
/* 47-6 — M02 면적산출 상세 오른쪽 = 공용 접는 패널 둘(일위대가 · 도면) · 하나만 열림 · 폭 공유 · 모두 접힘
|
||||
* 시작 화면: M02 마스터 템플릿 · 마스터 층(#/m02-master-template?open=구조물조합&root=master) · 시스템 관리자
|
||||
* · 나무 item1 → 면적산출 카드 [수정] → 상세 · 끝에 [돌아가기] · 저장 안 함
|
||||
* 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌)
|
||||
* 기대 수치:
|
||||
* - titles = ["일위대가", "도면"] · 일위대가 폄 → open = ["labor"] · labor.sections 에 「단가산출 로직」
|
||||
* - 도면 띠 누름 → open = ["drawing"](일위대가 접힘) · drawing.iframe = true · drawing.h > 200
|
||||
* · drawing.w = labor.w(폭 공유)
|
||||
* - 접기 ▸ → open = [] · strips = 2(모두 접힘)
|
||||
* - 끝에 처음 열림 상태로 되돌림(restored = true)
|
||||
*/
|
||||
(async () => {
|
||||
const W = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const $ = (s, root = document) => root.querySelector(s);
|
||||
const $$ = (s, root = document) => [...root.querySelectorAll(s)];
|
||||
const until = async (fn, ms = 8000) => {
|
||||
for (let t = 0; t < ms; t += 150) {
|
||||
const v = fn();
|
||||
if (v) return v;
|
||||
await W(150);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const vis = (b) => b && !b.hidden && b.offsetParent !== null;
|
||||
const edits = () =>
|
||||
$$(".m02-struct button").filter((b) => vis(b) && b.textContent.trim() === "수정");
|
||||
$('[data-node="i|item1"]')?.click();
|
||||
await until(() => edits().length === 3);
|
||||
const edit = edits().find((b) => /면적/.test(b.title)) ?? edits()[2];
|
||||
if (!edit) return JSON.stringify({ error: "면적산출 [수정] 없음" });
|
||||
edit.click();
|
||||
const sp = await until(() => $(".m02-main__host .ui-side-panels"));
|
||||
if (!sp) return JSON.stringify({ error: "오른쪽 패널 없음" });
|
||||
const panel = (id) => $(`.ui-side-panels__panel[data-panel="${id}"]`, sp);
|
||||
const strip = (title) =>
|
||||
$$(".ui-side-panels__strip", sp).find((s) => s.innerText.trim() === title);
|
||||
const openIds = () =>
|
||||
$$(".ui-side-panels__panel", sp)
|
||||
.filter((p) => !p.hidden)
|
||||
.map((p) => p.dataset.panel);
|
||||
const strips = () => $$(".ui-side-panels__strip", sp).filter((s) => !s.hidden).length;
|
||||
const first = openIds()[0] ?? null;
|
||||
const out = { titles: $$(".ui-side-panels__strip", sp).map((s) => s.innerText.trim()) };
|
||||
|
||||
if (panel("labor").hidden) strip("일위대가").click();
|
||||
await W(1500);
|
||||
out.labor = {
|
||||
open: openIds(),
|
||||
sections: $$(
|
||||
"h3, h4, .ui-sidebar-section__title, strong",
|
||||
panel("labor").querySelector(".ui-side-panels__body"),
|
||||
)
|
||||
.map((h) => h.innerText.trim())
|
||||
.filter(Boolean)
|
||||
.slice(0, 4),
|
||||
w: Math.round(panel("labor").getBoundingClientRect().width),
|
||||
};
|
||||
|
||||
strip("도면").click();
|
||||
await until(() => panel("drawing").querySelector("iframe"), 8000);
|
||||
await W(1500);
|
||||
const f = panel("drawing").querySelector("iframe");
|
||||
out.drawing = {
|
||||
open: openIds(),
|
||||
iframe: !!f,
|
||||
h: f ? Math.round(f.getBoundingClientRect().height) : 0,
|
||||
w: Math.round(panel("drawing").getBoundingClientRect().width),
|
||||
note: panel("drawing").querySelector(".m02-apanel__note")?.innerText ?? null,
|
||||
};
|
||||
|
||||
panel("drawing").querySelector(".ui-side-panels__fold").click();
|
||||
await W(300);
|
||||
out.folded = { open: openIds(), strips: strips() };
|
||||
|
||||
// 되돌림 — 처음 열려 있던 패널
|
||||
if (first) strip(first === "labor" ? "일위대가" : "도면").click();
|
||||
await W(300);
|
||||
out.restored = (openIds()[0] ?? null) === first;
|
||||
out.title = $(".m02-main__title")?.textContent.trim() ?? null;
|
||||
$$(".m02-main__head button")
|
||||
.find((x) => vis(x) && x.textContent.trim() === "돌아가기")
|
||||
?.click();
|
||||
out.pass =
|
||||
out.titles.join() === "일위대가,도면" &&
|
||||
out.labor.open.join() === "labor" &&
|
||||
out.drawing.open.join() === "drawing" &&
|
||||
out.drawing.iframe &&
|
||||
out.drawing.h > 200 &&
|
||||
out.drawing.w === out.labor.w &&
|
||||
out.folded.open.length === 0 &&
|
||||
out.folded.strips === 2 &&
|
||||
out.restored;
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
Reference in New Issue
Block a user