From 65d6fd261f8bdb15f8778491c902ce48864e70a9 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 2 Sep 2026 18:12:28 +0900 Subject: [PATCH] =?UTF-8?q?fix(B07):=20=EB=8F=84=EA=B0=81=20=ED=8E=B8?= =?UTF-8?q?=EC=A7=91=20=EB=9D=A0=20=EC=82=AC=EC=9D=B4=EB=93=9C=EB=B0=94=20?= =?UTF-8?q?=EC=9D=B4=EA=B4=80=C2=B7=EA=B9=A8=EC=A7=84=20=EA=B7=B8=EB=A6=BC?= =?UTF-8?q?=EC=97=90=20=EB=A0=8C=EB=8D=94=20=EB=A3=A8=ED=94=84=20=EC=A0=95?= =?UTF-8?q?=EC=A7=80=20=ED=95=B4=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 편집 띠를 CAD 위 절대배치에서 도면 목록 하단 액션 칸으로 옮김 — 1행 편집 띠, 2행 [도각 편집]·[현재 도면 확정] (리본과 겹쳐 문구·버튼이 찌그러지던 문제) - `drawImage` 가 'broken' 상태 그림에서 던지던 예외로 렌더 루프가 끊겨 이후 모든 도면이 백지로 남던 결함 해소 — 못 읽은 그림은 건너뜀 - `ImageEntity` 가 자리표시자(`{{회사로고}}`)를 절대 URL 로 굳히던 저장 경로 수정 — 원본 주소를 보관해 그대로 돌려주고, 자리표시자는 요청조차 하지 않음 - 버튼 줌(ZOOMIN·ZOOMOUT)이 배율만 바꿔 도면이 화면 밖으로 밀리던 것을 화면 중심 고정으로 수정 Co-Authored-By: Claude Opus 5 (1M context) --- .../B07_DesignDetail_UI_FrameEdit.ts | 7 ++- B07_DesignDetail/B07_DesignDetail_UI_Page.ts | 8 +++- .../B07_DesignDetail_UI_Style.css | 45 ++++++++++++++----- .../openwebcad/src/commands/commands.view.ts | 20 ++++++++- .../screenCanvas.drawController.ts | 5 +++ .../openwebcad/src/entities/ImageEntity.ts | 21 +++++++-- 6 files changed, 85 insertions(+), 21 deletions(-) diff --git a/B07_DesignDetail/B07_DesignDetail_UI_FrameEdit.ts b/B07_DesignDetail/B07_DesignDetail_UI_FrameEdit.ts index f7a1fafc..7a195985 100644 --- a/B07_DesignDetail/B07_DesignDetail_UI_FrameEdit.ts +++ b/B07_DesignDetail/B07_DesignDetail_UI_FrameEdit.ts @@ -19,7 +19,7 @@ import { export interface FrameTemplateEditor { /** 도면 목록 아래에 놓는 「도각 편집」 버튼. */ button: HTMLButtonElement; - /** 편집 중임을 알리는 CAD 화면 상단 띠 (평소엔 숨김). */ + /** 편집 중임을 알리는 띠 — 도면 목록 하단 액션 칸의 1행 (평소엔 숨김). */ banner: HTMLElement; /** 편집 중인가 — 도면 변경 알림(확정 해제)을 이 동안 막는 데 쓴다. */ isEditing: () => boolean; @@ -66,7 +66,10 @@ export function createFrameTemplateEditor(options: Options): FrameTemplateEditor variant: "ghost", onClick: () => leave(), }); - banner.append(finishButton, resetButton, cancelButton); + const bannerButtons = document.createElement("div"); + bannerButtons.className = "b07-frame-edit__buttons"; + bannerButtons.append(finishButton, resetButton, cancelButton); + banner.append(bannerButtons); const button = createButton({ label: "도각 편집", diff --git a/B07_DesignDetail/B07_DesignDetail_UI_Page.ts b/B07_DesignDetail/B07_DesignDetail_UI_Page.ts index 807467e4..575a7fd1 100644 --- a/B07_DesignDetail/B07_DesignDetail_UI_Page.ts +++ b/B07_DesignDetail/B07_DesignDetail_UI_Page.ts @@ -601,7 +601,6 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise { }, onSaved: () => drawingCache.clear(), }); - cadHost.prepend(frameEditor.banner); window.addEventListener("message", (event: MessageEvent) => { if (event.origin !== window.location.origin || event.source !== frame.contentWindow) return; @@ -668,7 +667,12 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise { // 하단 고정은 공용 ui-sidebar-actions로 통일 — 사이드 본문이 [스크롤 영역][액션 줄]로 // 쪼개지고 액션 줄은 스크롤 밖에 남는다(2026-08-18 사용자 지시, B04~B07 공통). confirmActions.className = "b07-drawing-actions ui-sidebar-actions"; - confirmActions.append(frameEditor.button, confirmButton); + // 액션 칸은 두 줄이다 — 1행 도각 편집 띠(평소 숨김), 2행 [도각 편집]·[현재 도면 확정] + // (2026-09-02 사용자 지시 — CAD 리본과 겹치던 떠 있는 띠를 여기로 옮김). + const confirmButtonRow = document.createElement("div"); + confirmButtonRow.className = "b07-drawing-actions__row"; + confirmButtonRow.append(frameEditor.button, confirmButton); + confirmActions.append(frameEditor.banner, confirmButtonRow); drawingPanel.append(infoPanelHost, confirmActions); diff --git a/B07_DesignDetail/B07_DesignDetail_UI_Style.css b/B07_DesignDetail/B07_DesignDetail_UI_Style.css index f2ca6ee6..c9a95180 100644 --- a/B07_DesignDetail/B07_DesignDetail_UI_Style.css +++ b/B07_DesignDetail/B07_DesignDetail_UI_Style.css @@ -126,11 +126,25 @@ /* 하단 고정·배경·상단 구분선은 공용 ui-sidebar-actions가 맡는다(2026-08-18 통합). 여기서는 B07 고유 여백만 남긴다. */ +/* 액션 칸은 세로 두 줄 — 1행 도각 편집 띠, 2행 [도각 편집]·[현재 도면 확정]. + 공용 .ui-sidebar-actions는 가로 한 줄이라 방향과 늘어남을 여기서 되돌린다. */ .b07-drawing-actions { + flex-direction: column; padding-top: var(--spacing-12); } -.b07-drawing-actions > button { +.b07-drawing-actions > * { + flex: none; +} + +.b07-drawing-actions__row { + display: flex; + gap: var(--spacing-8); +} + +.b07-drawing-actions__row > button { + flex: 1 1 0; + min-width: 0; width: 100%; } @@ -271,29 +285,36 @@ color: var(--color-text-muted); } -/* 도각 편집 모드 띠 — CAD 위에 겹쳐 편집 중임을 알리고 [완료]·[취소]를 준다. */ +/* 도각 편집 모드 띠 — 도면 목록 하단 액션 칸의 1행. CAD 위에 떠 있던 배치는 리본과 + 겹쳐 문구가 접히고 버튼이 찌그러졌다(2026-09-02 사용자 지시로 사이드바로 옮김). */ .b07-frame-edit { - position: absolute; - z-index: 3; - top: 8px; - left: 50%; - transform: translateX(-50%); display: flex; - align-items: center; + flex-direction: column; gap: var(--spacing-8); - padding: 6px 10px; + padding: var(--spacing-8); border: 1px solid var(--color-border); border-radius: var(--radius-cards); background-color: var(--color-surface); - box-shadow: 0 6px 18px rgb(0 0 0 / 25%); +} + +.b07-frame-edit[hidden] { + display: none; } .b07-frame-edit__label { font-size: 0.78rem; + line-height: 1.4; color: var(--color-text); } -.b07-frame-edit > button { - padding: 4px 12px; +.b07-frame-edit__buttons { + display: flex; + gap: var(--spacing-8); +} + +.b07-frame-edit__buttons > button { + flex: 1 1 0; + min-width: 0; + padding: 4px 8px; font-size: 0.78rem; } diff --git a/B07_DesignDetail/openwebcad/src/commands/commands.view.ts b/B07_DesignDetail/openwebcad/src/commands/commands.view.ts index 6f1fd7ad..0c21cede 100644 --- a/B07_DesignDetail/openwebcad/src/commands/commands.view.ts +++ b/B07_DesignDetail/openwebcad/src/commands/commands.view.ts @@ -1,4 +1,5 @@ /** 뷰 탭 — 탐색·재생성 명령 (조사표 7절 중 구현분) */ +import { Point } from '@flatten-js/core'; import { toast } from 'react-toastify'; import type { CadCommand } from './command.types'; import { bumpSceneVersion } from '../helpers/scene-version'; @@ -8,8 +9,23 @@ import { selectToolStateMachine } from '../tools/select-tool'; const zoomBy = (factor: number): string => { const controller = getScreenCanvasDrawController(); - controller.setScreenScale(Math.max(0.01, controller.getScreenScale() * factor)); - return `줌 ${Math.round(controller.getScreenScale() * 100)}%`; + const oldScale = controller.getScreenScale(); + const newScale = Math.max(0.01, oldScale * factor); + // 배율만 바꾸면 화면이 월드 원점 쪽으로 늘어나 도면이 화면 밖으로 밀려난다. + // 휠 줌이 커서 밑 좌표를 붙잡아 두듯, 버튼 줌은 **화면 중심**의 월드 좌표를 + // 붙잡아 둔다 (screen = (world - offset) * scale). + const canvasSize = controller.getCanvasSize(); + const offset = controller.getScreenOffset(); + const worldCenterX = offset.x + canvasSize.x / 2 / oldScale; + const worldCenterY = offset.y + canvasSize.y / 2 / oldScale; + controller.setScreenScale(newScale); + controller.setScreenOffset( + new Point( + worldCenterX - canvasSize.x / 2 / newScale, + worldCenterY - canvasSize.y / 2 / newScale + ) + ); + return `줌 ${Math.round(newScale * 100)}%`; }; export const VIEW_COMMANDS: CadCommand[] = [ diff --git a/B07_DesignDetail/openwebcad/src/drawControllers/screenCanvas.drawController.ts b/B07_DesignDetail/openwebcad/src/drawControllers/screenCanvas.drawController.ts index 4facbdbd..2afd797f 100644 --- a/B07_DesignDetail/openwebcad/src/drawControllers/screenCanvas.drawController.ts +++ b/B07_DesignDetail/openwebcad/src/drawControllers/screenCanvas.drawController.ts @@ -596,6 +596,11 @@ export class ScreenCanvasDrawController implements DrawController { angle: number ): void { if (this.batching) this.flushBatch(); + // 못 읽은 그림은 건너뛴다. 'broken' 상태의 그림을 그리려 하면 캔버스가 예외를 + // 던지고, 그 예외가 렌더 루프를 끊어 **이후 모든 도면이 백지**로 남았다 + // (2026-09-02 실측: 도각 자리표시자가 404 로 깨진 채 줌 한 번에 화면이 멈췄다). + // 자리 하나가 비는 것과 도면 전체가 안 나오는 것은 무게가 다르다. + if (imageElement.complete === false || imageElement.naturalWidth === 0) return; // 크기는 배율만 곱한다. 예전에는 (width, height)를 좌표처럼 변환해 화면 오프셋과 // y 뒤집기가 섞여 들어갔고, 그림이 제 자리를 벗어나 비율까지 무너졌다(2026-09-02 // 도각 로고·서명에서 드러남). 자리는 SVG 컨트롤러와 같이 **세계 중심**으로 잡는다. diff --git a/B07_DesignDetail/openwebcad/src/entities/ImageEntity.ts b/B07_DesignDetail/openwebcad/src/entities/ImageEntity.ts index 3476aa8b..cca181dd 100644 --- a/B07_DesignDetail/openwebcad/src/entities/ImageEntity.ts +++ b/B07_DesignDetail/openwebcad/src/entities/ImageEntity.ts @@ -26,6 +26,13 @@ export class ImageEntity implements Entity { private imageElement: HTMLImageElement; private polygon: Polygon; private angle: number; + /** + * JSON에서 받은 그림 주소 원본. `imageElement.currentSrc`는 브라우저가 절대 URL로 + * 바꿔 놓아, 도각의 자리표시자(`{{회사로고}}`)가 저장 한 번에 + * `http://…/b07-cad/%7B%7B회사로고%7D%7D`로 굳는다(2026-09-02 실측 — 회사 도각이 + * 그렇게 손상됐다). 원본을 들고 있다가 그대로 돌려준다. + */ + private sourceData: string | null = null; constructor( layerId: string, @@ -115,7 +122,9 @@ export class ImageEntity implements Entity { public clone(): ImageEntity { const clonedImage = document.createElement('img'); clonedImage.src = this.imageElement.src; - return new ImageEntity(getActiveLayerId(), clonedImage, this.polygon.clone()); + const cloned = new ImageEntity(getActiveLayerId(), clonedImage, this.polygon.clone()); + cloned.sourceData = this.sourceData; + return cloned; } // TODO add destroy method to cleanup this.imageElement.src @@ -230,7 +239,7 @@ export class ImageEntity implements Entity { x: vertex.x, y: vertex.y, })), - imageData: this.imageElement.currentSrc, + imageData: this.sourceData ?? this.imageElement.currentSrc, }, }; } @@ -243,7 +252,12 @@ export class ImageEntity implements Entity { jsonEntity.shapeData.points.map((point) => new Point(point.x, point.y)) ); const image = new Image(); - image.src = jsonEntity.shapeData.imageData; + // 자리표시자는 주소가 아니다 — 그대로 넣으면 404 요청이 나가고 그림이 'broken' + // 상태가 된다. 그 상태의 그림을 그리려 하면 캔버스가 예외를 던져 렌더 루프가 + // 끊기고 **이후 모든 도면이 백지**로 남았다(2026-09-02 실측). 자리만 남긴다. + if (!jsonEntity.shapeData.imageData.includes('{{')) { + image.src = jsonEntity.shapeData.imageData; + } const rectangleEntity = new ImageEntity( jsonEntity.layerId || getActiveLayerId(), image, @@ -253,6 +267,7 @@ export class ImageEntity implements Entity { rectangleEntity.lineColor = jsonEntity.lineColor; rectangleEntity.lineWidth = jsonEntity.lineWidth; rectangleEntity.lineDash = jsonEntity.lineDash; + rectangleEntity.sourceData = jsonEntity.shapeData.imageData; return rectangleEntity; } }