fix(B07): 편집이 사라지지 않게 하고 확정한 도면을 잠근다

사용자 편의성 점검 15건 중 반영 확정분 12건.

편집 손실 방지
- CAD에 미저장 표시(drawingDirty)를 두고, 도면을 바꾸기 전에 묻는다.
  전에는 그은 선이 경고 없이 사라졌다(실측: 캔버스 서명이 원본과 동일).
- 도면 전환을 겹쳐 눌러도 늦게 온 응답이 화면을 덮지 않게 가드를 뒀다.
- 도면층 삭제·객체 이동을 되돌리기에 실어 Ctrl+Z로 돌아오게 했다.

확정한 도면은 읽기 전용
- runCommand 한 곳에서 보는 명령만 통과시킨다(허용 명시 방식).
- 도면을 실을 때 앞 도면의 그리기 도구를 내린다 — 켜 둔 도구가 확정본에도
  계속 그렸다.
- 확정 버튼 자리를 [현재 도면 확정] / [수정]으로 가른다. 확정 해제는 [수정]
  한 곳뿐 — 되돌리기·색 고르기로 확정이 풀리던 경로를 없앴다.

그 밖
- 잠금 도면층(도각·등고선·계류)은 마우스가 스쳐도 하이라이트하지 않는다.
- 준비 중 도면 7종도 빈 도각으로 열린다(눌리지 않는 회색 버튼 제거).
- 도면층 행이 잘리지 않게 패널을 300px로 넓히고 이름을 줄여 담는다.
- 기본 그리기 색을 검정으로 — 흰 종이에 흰 선이라 안 보였다.
- 색 고르는 동안 변경 통지가 연발하지 않는다.
- 수량표는 앞 단계 산출물이라 도면에서 고치지 못하게 막고 안내한다.
- 자동백업 칸을 도면별로 나눈다 — 되살리면 다른 도면에 붙었다.
- 횡단 장 id를 시작 측점 기준으로 바꿔 구간이 달라져도 옛 확정이 안 붙는다.
- 회사 도각 저장 전에 좌표를 검사하고, 기본 도각으로 되돌리기를 연다.
- 횡단 파일이 없어도 나머지 도면 목록은 남는다.
- 불러오는 중·실패를 화면에 표시한다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-01 19:05:22 +09:00
co-authored by Claude Fable 5
parent f82e60492e
commit 17e14a25ed
22 changed files with 666 additions and 105 deletions
@@ -44,13 +44,15 @@ export const LayerManager: FC<LayerManagerProps> = ({
for (const entity of selectedEntities) {
entity.layerId = layerId;
}
console.info(`Assigned ${selectedEntities.length} entities to layer`);
// 객체를 옮기는 것도 도면 편집이다 — 되돌리기·미저장 표시에 실어야 한다.
setEntities([...getEntities()], true);
};
const handleDeleteLayer = (evt: MouseEvent, layerId: string): void => {
evt.stopPropagation();
const entitiesNotOnLayer = getEntities().filter((entity) => entity.layerId !== layerId);
setEntities(entitiesNotOnLayer);
// 도면층을 지우면 그 위의 객체가 통째로 사라진다 — 되돌리기에 실어야 Ctrl+Z로 돌아온다.
setEntities(entitiesNotOnLayer, true);
setLayers(getLayers().filter((layer) => layer.id !== layerId));
if (getActiveLayerId() === layerId) {
setActiveLayerId(getLayers()[0].id);
@@ -36,16 +36,31 @@ export const dashToLineType = (dash: number[] | undefined): string =>
LINE_TYPES.find((type) => JSON.stringify(type.dash) === JSON.stringify(dash))?.value ?? 'solid';
/** 선택 객체가 있으면 즉시 적용하고, 없으면 이후 그리기 기본값만 바꾼다. */
function applyToSelection(mutate: (entity: Entity) => void): boolean {
function applyToSelection(mutate: (entity: Entity) => void, trackInUndoStack = true): boolean {
const selected = getSelectedEntities();
if (!selected.length) return false;
for (const entity of selected) {
mutate(entity);
}
setEntities([...getEntities()], true);
setEntities([...getEntities()], trackInUndoStack);
return true;
}
/**
* 색 선택기는 드래그하는 내내 값 변경을 쏜다. 그 하나하나를 되돌리기·변경통지에 실으면
* 색 한 번 고르는 동안 수십 건이 나간다(2026-09-01 지적). 끄는 동안에는 화면만 칠하고,
* 멎은 뒤 한 번만 되돌리기·통지에 싣는다.
*/
const COLOR_COMMIT_DELAY_MS = 350;
let colorCommitTimer: ReturnType<typeof setTimeout> | undefined;
function paintNowCommitLater(mutate: (entity: Entity) => void): void {
applyToSelection(mutate, false); // 화면만 — 되돌리기·미저장 표시는 아직
clearTimeout(colorCommitTimer);
colorCommitTimer = setTimeout(() => {
applyToSelection(() => {}, true); // 멎었다 — 이번 한 번만 실어 보낸다
}, COLOR_COMMIT_DELAY_MS);
}
export const PropertiesWidget: FC = () => {
const lineColor = getActiveLineColor();
const lineWidth = getActiveLineWidth();
@@ -59,9 +74,10 @@ export const PropertiesWidget: FC = () => {
type="color"
value={lineColor}
onChange={(event) => {
setActiveLineColor(event.target.value);
applyToSelection((entity) => {
entity.lineColor = event.target.value;
const color = event.target.value;
setActiveLineColor(color);
paintNowCommitLater((entity) => {
entity.lineColor = color;
});
}}
/>
@@ -145,13 +161,16 @@ export const LayersWidget: FC = () => {
export const TextStyleWidget: FC = () => {
const textStyle = getActiveTextStyle();
const handle = (patch: Partial<typeof textStyle>) => {
const handle = (patch: Partial<typeof textStyle>, live = false) => {
setActiveTextStyle(patch);
applyToSelection((entity) => {
const mutate = (entity: Entity) => {
if (entity.getType() === EntityName.Text) {
(entity as TextEntity).setTextOptions(patch);
}
});
};
// 색만 드래그 중 연발한다 — 나머지 항목은 한 번 고르면 끝이라 바로 싣는다.
if (live) paintNowCommitLater(mutate);
else applyToSelection(mutate);
};
return (
@@ -203,7 +222,7 @@ export const TextStyleWidget: FC = () => {
<input
type="color"
value={textStyle.textColor}
onChange={(event) => handle({ textColor: event.target.value })}
onChange={(event) => handle({ textColor: event.target.value }, true)}
/>
</label>
</div>
@@ -24,10 +24,10 @@ export const Toolbar: FC = () => {
const activeTool = (getActiveToolActor()?.getSnapshot()?.context?.type ?? null) as Tool | null;
useEffect(() => {
document.documentElement.style.setProperty(
'--cad-panel-width',
panelCollapsed ? '0px' : '248px'
);
// 편 상태의 폭은 App.css의 `--cad-panel-width` 기본값을 쓴다. 여기 숫자를 박아
// 두면 스타일에서 넓혀도 이 인라인 값이 덮는다(2026-09-01 실측: 300px 무시됨).
if (panelCollapsed) document.documentElement.style.setProperty('--cad-panel-width', '0px');
else document.documentElement.style.removeProperty('--cad-panel-width');
document.documentElement.style.setProperty(
'--cad-command-height',
commandLineVisible ? '86px' : '0px'