fix(B07 CAD·M02): 가운데 단추 끌기 때 페이지가 같이 움직이던 것 · 집계표 미리보기는 보기 전용 조작

- 캔버스 가운데 단추 mousedown · auxclick 의 기본 동작(자동 스크롤 · 붙여넣기)을 막음 — 휠은 이미 passive:false 로 막음.
  pointerdown 은 막지 않음(막으면 mousedown 이 안 와 팬이 끊김).
- 미리보기(viewOnly — M02 구조물집계표 「도면 정보」 카드): 왼쪽 끌기 = 화면 옮기기 · 휠 확대 ·
  두 번 누름 = 전체 맞춤 · 선택 · 가까운 개체 강조 · 스냅 · 십자 커서 · 명령 안내 · 좌표 풍선 없음(손 모양 커서).
  [도면 수정] 편집 화면 · 도면 양식 · B07 은 그대로.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K7JS47SacjPxZ718QsZKyr
This commit is contained in:
2026-09-28 16:27:55 +09:00
co-authored by Claude Opus 5.5
parent 14c4a507d6
commit 32c3081b68
8 changed files with 50 additions and 2 deletions
@@ -13,6 +13,7 @@ import {
getShouldDrawCursor,
getSnapPoint,
getSnapPointOnAngleGuide,
isViewOnly,
} from '../state';
import {
drawCursor,
@@ -65,7 +66,8 @@ export function draw(drawController: ScreenCanvasDrawController) {
);
drawSnapPoint(drawController, closestSnapPoint, isMarked);
if (getShouldDrawCursor()) {
// 미리보기는 십자 커서 · 명령 안내 · 좌표 풍선을 그리지 않는다(손 모양 커서).
if (getShouldDrawCursor() && !isViewOnly()) {
drawCursor(drawController);
getInputController().draw(drawController);
}
@@ -42,6 +42,7 @@ import {
getSnapPoint,
getSnapPointOnAngleGuide,
isHostSave,
isViewOnly,
redo,
setActiveToolActor,
setAngleStep,
@@ -111,6 +112,11 @@ export class InputController {
canvas?.addEventListener('contextmenu', (evt) => {
evt.preventDefault();
});
// 가운데 단추 — 브라우저 자동 스크롤(페이지가 같이 움직임) · 붙여넣기를 막는다.
// 막는 자리는 mousedown(아래 handleMouseDown) — pointerdown 을 막으면 mousedown 이 안 와 팬이 끊긴다.
canvas?.addEventListener('auxclick', (evt: MouseEvent) => {
if (evt.button === MouseButton.Middle) evt.preventDefault();
});
// 캔버스 밖(리본·패널) 위에서의 트랙패드 핀치도 브라우저를 확대시키지 않는다.
// ctrl이 없는 휠은 그대로 둬서 패널 스크롤은 살린다.
document.addEventListener(
@@ -191,6 +197,10 @@ export class InputController {
if (evt.button === MouseButton.Middle) {
setPanStartLocation(null);
}
if (evt.button === MouseButton.Left && isViewOnly()) {
setPanStartLocation(null);
return;
}
if (evt.button === MouseButton.Left) {
if (this.isSelectToolActive()) {
// 선택 도구는 누를 때 첫 점을 이미 보냈다. 끌었으면 놓는 자리로 사각형을 닫는다.
@@ -264,6 +274,9 @@ export class InputController {
setPanStartLocation(newScreenMouseLocation);
}
// 미리보기는 보기만 — 스냅 · 가까운 개체 강조를 하지 않는다.
if (isViewOnly()) return;
// Calculate angle guides and snap points
if (getSnapEnabled()) {
calculateAngleGuidesAndSnapPoints();
@@ -322,11 +335,18 @@ export class InputController {
public handleMouseDown(evt: MouseEvent) {
if (evt.button === MouseButton.Middle) {
evt.preventDefault(); // 자동 스크롤로 부모 페이지가 같이 굴러가던 것
setPanStartLocation(this.getCanvasPoint(evt));
return;
}
if (evt.button !== MouseButton.Left) return;
if ((evt.target as HTMLElement | null)?.closest('.controls')) return;
if (isViewOnly()) {
// 미리보기 — 왼쪽 끌기는 선택 상자 대신 화면 옮기기.
evt.preventDefault();
setPanStartLocation(this.getCanvasPoint(evt));
return;
}
// AutoCAD처럼 누른 자리에서 사각형이 시작되도록 선택 도구에만 첫 점을 미리 보낸다.
// 다른 도구는 예전처럼 놓을 때 한 점을 받는다 (끌다가 점이 두 개 찍히지 않게).
if (!this.isSelectToolActive()) return;
@@ -15,6 +15,7 @@ import {
getScreenCanvasDrawController,
isDrawingDirty,
isDrawingReadOnly,
isViewOnly,
resetUndoBaseline,
setActiveLayerId,
setDesignMeta,
@@ -24,6 +25,7 @@ import {
setHostSave,
setHostTitle,
setLayers,
setViewOnly,
} from '../state.ts';
import { setInspectorCollapsed, setRibbonCollapsed } from '../components/ui-state.ts';
import { toast } from 'react-toastify';
@@ -62,6 +64,8 @@ interface DrawingLoadMessage {
ribbonCollapsed?: boolean;
/** 좌측 패널(특성 · 도면층)을 접힌 채로 여는가 — 없으면(B07) 지금 상태 그대로. */
panelsCollapsed?: boolean;
/** 미리보기 — 왼쪽 끌기 = 화면 옮기기 · 두 번 누름 = 전체 맞춤 · 선택 · 명령 안내 없음. */
viewOnly?: boolean;
}
interface DrawingSaveRequestMessage {
@@ -112,6 +116,10 @@ function registerTextDoubleClickEdit() {
const canvas = getCanvas();
if (!canvas) return;
canvas.addEventListener('dblclick', (event: MouseEvent) => {
if (isViewOnly()) {
getScreenCanvasDrawController().zoomToFitScreen();
return;
}
if (isDrawingReadOnly()) {
toast.info('확정한 도면입니다. 고치려면 [수정]을 먼저 누르세요.');
return;
@@ -214,6 +222,7 @@ export function registerAisloDrawingBridge() {
setHostSave(event.data.hostSave === true);
setRibbonCollapsed(event.data.ribbonCollapsed === true);
if (event.data.panelsCollapsed) setInspectorCollapsed(true);
setViewOnly(event.data.viewOnly === true);
if (event.data.frameEdit) applyFramePreview();
// 앞 도면에서 켜 둔 그리기 도구를 내린다. 안 내리면 **확정한 도면 위에도**
// 그 도구가 계속 그린다 — 읽기 전용은 새 명령만 막기 때문이다(2026-09-01 실측:
+2 -1
View File
@@ -26,6 +26,7 @@ import {
getLastDrawTimestamp,
getScreenCanvasDrawController,
getSnapPoint,
isViewOnly,
setActiveLayerId,
setActiveToolActor,
setCanvas,
@@ -83,7 +84,7 @@ function startDrawLoop(
/**
* Highlight the entity closest to the mouse when the select tool is active
*/
if (getActiveToolActor()?.getSnapshot()?.context?.type === Tool.SELECT) {
if (getActiveToolActor()?.getSnapshot()?.context?.type === Tool.SELECT && !isViewOnly()) {
const screenCanvasDrawController = getScreenCanvasDrawController();
if (!screenCanvasDrawController) {
throw new Error('getScreenCanvasDrawController() returned null');
+10
View File
@@ -192,6 +192,8 @@ let frameEditMode = false;
let frameFields: Record<string, string> = {};
/** 부모가 보기 전용으로 실었는가 — M02 에서 고칠 권한이 없는 양식을 볼 때. */
let hostReadOnly = false;
/** 미리보기(M02 집계표 카드) — 왼쪽 끌기 = 화면 옮기기 · 두 번 누름 = 전체 맞춤 · 선택 · 명령 안내 없음. */
let viewOnly = false;
let hostTitle = 'B07 상세 설계';
let hostSave = false;
@@ -272,6 +274,7 @@ export const isDrawingDirty = () => drawingDirty;
* 확정). 보기(확대·이동·도면층 켜기끄기)는 그대로 두고, 푸는 길은 부모의 [수정]뿐이다.
*/
export const isDrawingReadOnly = (): boolean => designMeta?.confirmed === true || hostReadOnly;
export const isViewOnly = (): boolean => viewOnly;
// setters
export const setCanvas = (newCanvas: HTMLCanvasElement) => {
@@ -515,6 +518,13 @@ export const setHostReadOnly = (readOnly: boolean) => {
hostReadOnly = readOnly;
notifyWindow(HtmlEvent.UPDATE_STATE);
};
export const setViewOnly = (enabled: boolean) => {
viewOnly = enabled;
// 그리는 십자 커서 대신 손 모양 — 캔버스는 `cursor-none` 이라 안 두면 커서가 안 보임.
const canvas = getCanvas();
if (canvas) canvas.style.cursor = enabled ? 'grab' : '';
notifyWindow(HtmlEvent.UPDATE_STATE);
};
export const setFrameEditMode = (enabled: boolean, fields: Record<string, string> = {}) => {
frameEditMode = enabled;
frameFields = enabled ? fields : {};
@@ -41,6 +41,8 @@ export interface DrawingTemplateOptions {
headerSlot?: HTMLElement;
/** CAD 좌측 패널(특성 · 도면층)을 접힌 채로 연다 — 누르면 펼침(구조물 도면 미리보기). */
panelsCollapsed?: boolean;
/** 미리보기(집계표 카드) — 왼쪽 끌기 = 화면 옮기기 · 두 번 누름 = 전체 맞춤 · 선택 · 명령 안내 없음. */
preview?: boolean;
}
const RECOVERY_PREFIX = "OPEN_WEB_CAD__RECOVERY__";
@@ -198,6 +200,7 @@ export function mountDrawingTemplate(
hostSave: true,
ribbonCollapsed: true,
panelsCollapsed: options.panelsCollapsed,
viewOnly: options.preview === true,
},
);
};
@@ -377,6 +377,7 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane
name: `preview:${column ?? ""}`,
layer: "system",
panelsCollapsed: true, // 특성 · 도면층 패널은 접힌 채 — › 를 누르면 펼침
preview: true, // 왼쪽 끌기 = 화면 옮기기 · 두 번 누름 = 전체 맞춤
});
const frame = previewHost.querySelector("iframe");
if (frame) hideToolbar(frame);
+2
View File
@@ -67,6 +67,8 @@ export interface CadLoadExtra {
ribbonCollapsed?: boolean;
/** 좌측 패널(특성 · 도면층)을 접힌 채로 연다 — 안 보내면(B07) 지금 상태 그대로. */
panelsCollapsed?: boolean;
/** 미리보기 — 왼쪽 끌기 = 화면 옮기기 · 두 번 누름 = 전체 맞춤 · 선택 · 명령 안내 없음. */
viewOnly?: boolean;
}
export interface CadHost<D extends CadHostDrawing, M, Q> {