fix(B07): 도각 편집 띠 사이드바 이관·깨진 그림에 렌더 루프 정지 해소

- 편집 띠를 CAD 위 절대배치에서 도면 목록 하단 액션 칸으로 옮김 —
  1행 편집 띠, 2행 [도각 편집]·[현재 도면 확정] (리본과 겹쳐 문구·버튼이 찌그러지던 문제)
- `drawImage` 가 'broken' 상태 그림에서 던지던 예외로 렌더 루프가 끊겨
  이후 모든 도면이 백지로 남던 결함 해소 — 못 읽은 그림은 건너뜀
- `ImageEntity` 가 자리표시자(`{{회사로고}}`)를 절대 URL 로 굳히던 저장 경로 수정 —
  원본 주소를 보관해 그대로 돌려주고, 자리표시자는 요청조차 하지 않음
- 버튼 줌(ZOOMIN·ZOOMOUT)이 배율만 바꿔 도면이 화면 밖으로 밀리던 것을
  화면 중심 고정으로 수정

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-02 18:12:28 +09:00
co-authored by Claude Opus 5
parent 2bdad0947b
commit 65d6fd261f
6 changed files with 85 additions and 21 deletions
@@ -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[] = [
@@ -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 컨트롤러와 같이 **세계 중심**으로 잡는다.
@@ -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;
}
}