feat(M02): 47-6 면적산출 상세 오른쪽 = 공용 접는 패널 둘(일위대가 · 도면)

- 옛 접기 손잡이 · 폭 손잡이 · 사이드바 카드 틀을 공용 오른쪽 접는 패널(47-3)로 갈음 — 하나만 열림 · 폭 공유 · 모두 접힘
- 「도면」 패널 = 그 열 구조물 도면 보기만(확대 · 이동) · 처음 펼 때 읽고 그림
- DrawingCard 미리보기 한 벌(mountStructure)을 카드 · 도면 패널이 같이 씀
- 시험: test_m02_structure_panel 기대를 공용 패널로 · ui_checks/47-6 시나리오

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W4uKaV4ZBhsJN4GHG7yRj4
This commit is contained in:
2026-10-05 13:47:07 +09:00
co-authored by Claude Opus 5.5
parent 9499ff16cc
commit fff2845117
5 changed files with 178 additions and 115 deletions
@@ -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,7 +10,12 @@
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";
@@ -56,25 +61,41 @@ 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, // 구조물도는 도각이 없음
...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: Pick<ItemSource, "layer" | "projectId">,
source: Where,
column: string,
): { destroy: () => void } {
const handle = mountDrawingTemplate(host, doc.도면 as unknown as DrawingTemplateDoc, {
readOnly: true,
name: `preview:${column}`,
layer: source.layer,
projectId: source.projectId,
panelsCollapsed: true,
preview: true,
originMarker: true,
noArea: true,
});
const frame = host.querySelector("iframe");
if (frame) hideChrome(frame, HIDE_VIEW);
const handle = mountStructure(host, doc, source, column);
return { destroy: () => handle.destroy() };
}
@@ -111,23 +132,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, // 구조물도는 도각이 없음
...(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() };
}