fix(M02): 도면 칸 CAD 💾 · Ctrl+S 가 페이지 [저장] 을 부름 · 엉터리 도각 파일은 400 · 자리표 패널 접기 단추 (PLAN 10-3)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PxvYb5ufV1kWdBvZbpDfu6
This commit is contained in:
2026-09-25 16:56:05 +09:00
co-authored by Claude Sonnet 5
parent 2aa4c80577
commit 1cd52cfdfb
10 changed files with 104 additions and 6 deletions
+13
View File
@@ -966,11 +966,24 @@ body > canvas[data-id="canvas"] {
}
.cad-frame-tokens__header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 6px;
color: var(--cad-text-dim);
font-size: 11px;
line-height: 1.4;
}
.cad-frame-tokens__fold {
flex: none;
border: 1px solid var(--cad-line);
border-radius: 4px;
background: transparent;
color: var(--cad-text);
cursor: pointer;
}
.cad-frame-tokens__group {
display: flex;
flex-wrap: wrap;
@@ -3,10 +3,10 @@ import { toast } from 'react-toastify';
import { clearRecovery, restoreRecovery } from '../helpers/autosave';
import { exportEntitiesToJsonFile } from '../helpers/import-export-handlers/export-entities-to-json';
import { exportEntitiesToLocalStorage } from '../helpers/import-export-handlers/export-entities-to-local-storage';
import { requestDrawingExport } from '../integration/aislo-drawing-bridge';
import { requestDrawingExport, requestHostSave } from '../integration/aislo-drawing-bridge';
import { exportEntitiesToPngFile } from '../helpers/import-export-handlers/export-entities-to-png';
import { exportEntitiesToSvgFile } from '../helpers/import-export-handlers/export-entities-to-svg';
import { redo, undo } from '../state';
import { isHostSave, redo, undo } from '../state';
import type { CadCommand } from './command.types';
export const FILE_COMMANDS: CadCommand[] = [
@@ -17,6 +17,10 @@ export const FILE_COMMANDS: CadCommand[] = [
glyph: '💾',
hint: '현재 도면을 브라우저에 저장한다',
run: () => {
if (isHostSave()) {
requestHostSave();
return '양식 저장 요청';
}
void exportEntitiesToLocalStorage().then(() => {
clearRecovery();
toast.success('도면을 저장했습니다.');
@@ -113,6 +113,8 @@ function boxSizeOf(entity: TextEntity | ImageEntity): { width: number; height: n
export const FramePlaceholderPanel: FC = () => {
const [visible, setVisible] = useState(isFrameEditMode());
// 접으면 그림 오른쪽 아래를 덮지 않는다 — 머리 줄만 남는다.
const [folded, setFolded] = useState(false);
const [picked, setPicked] = useState<TextEntity | ImageEntity | null>(null);
const [size, setSize] = useState({ width: 0, height: 0 });
@@ -139,8 +141,21 @@ export const FramePlaceholderPanel: FC = () => {
return (
<section className="cad-frame-tokens controls">
<header className="cad-frame-tokens__header">
자리표 놓기 — 누르면 화면 가운데에 서고, 끌어서 자리를 잡습니다
<span>
{folded ? '자리표 놓기' : '자리표 놓기 — 누르면 화면 가운데에 서고, 끌어서 자리를 잡습니다'}
</span>
<button
type="button"
className="cad-frame-tokens__fold"
aria-expanded={!folded}
title={folded ? '자리표 패널 펴기' : '자리표 패널 접기'}
onClick={() => setFolded(!folded)}
>
{folded ? '▴' : '▾'}
</button>
</header>
{!folded && (
<>
<div className="cad-frame-tokens__group">
<span className="cad-frame-tokens__title">글자</span>
{TEXT_TOKENS.map((token) => (
@@ -190,6 +205,8 @@ export const FramePlaceholderPanel: FC = () => {
</label>
</div>
)}
</>
)}
</section>
);
};
@@ -22,6 +22,7 @@ import {
resolveCommandInput,
} from '../commands/registry.ts';
import { runCommand } from '../commands/run-command.ts';
import { requestHostSave } from '../integration/aislo-drawing-bridge.ts';
import type { ScreenCanvasDrawController } from '../drawControllers/screenCanvas.drawController.ts';
import { calculateAngleGuidesAndSnapPoints } from '../helpers/calculate-angle-guides-and-snap-points.ts';
import { findClosestEntity } from '../helpers/find-closest-entity.ts';
@@ -40,6 +41,7 @@ import {
getSnapEnabled,
getSnapPoint,
getSnapPointOnAngleGuide,
isHostSave,
redo,
setActiveToolActor,
setAngleStep,
@@ -426,6 +428,9 @@ export class InputController {
} else if (evt.ctrlKey && evt.key === 'y') {
// User wants to redo the last action
this.handleRedo(evt);
} else if (evt.ctrlKey && evt.key === 's') {
// 부모 페이지가 저장을 맡은 화면(M02 양식)만 — B07 은 예전처럼 아무 일 없음
if (isHostSave()) requestHostSave();
} else if (evt.ctrlKey && evt.key === 'a') {
// User wants to select everything
setSelectedEntityIds(getPickableEntities().map((entity) => entity.id));
@@ -21,6 +21,7 @@ import {
setEntities,
setFrameEditMode,
setHostReadOnly,
setHostSave,
setHostTitle,
setLayers,
} from '../state.ts';
@@ -38,6 +39,7 @@ export const AISLO_DRAWING_SAVE_REQUEST_MESSAGE = 'aislo:b08:save-request';
export const AISLO_DRAWING_SAVE_RESPONSE_MESSAGE = 'aislo:b08:save-response';
export const AISLO_DRAWING_NAVIGATE_MESSAGE = 'aislo:b08:navigate';
export const AISLO_DRAWING_EXPORT_MESSAGE = 'aislo:b08:export-file';
export const AISLO_DRAWING_HOST_SAVE_MESSAGE = 'aislo:b08:host-save';
interface DrawingLoadMessage {
type: typeof AISLO_DRAWING_LOAD_MESSAGE;
@@ -53,6 +55,8 @@ interface DrawingLoadMessage {
readOnly?: boolean;
/** 제목표시줄 부제 — 없으면 「B07 상세 설계」. */
hostTitle?: string;
/** 저장(💾 · Ctrl+S)을 부모 [저장]에 맡기는가. */
hostSave?: boolean;
}
interface DrawingSaveRequestMessage {
@@ -75,6 +79,11 @@ export function requestDrawingNavigation(direction: 'prev' | 'next') {
notifyParent(AISLO_DRAWING_NAVIGATE_MESSAGE, { direction });
}
/** 저장 요청을 부모 페이지 [저장]에 넘긴다 — 캐드는 양식 파일을 모른다. */
export function requestHostSave() {
notifyParent(AISLO_DRAWING_HOST_SAVE_MESSAGE);
}
/**
* 지금 도면을 DXF·DWG 파일로 내려받도록 부모에게 요청한다 (2026-09-06 사용자 지시).
* 캐드는 프로젝트를 모르므로 파일 만들기는 부모가 서버에 맡긴다.
@@ -197,6 +206,7 @@ export function registerAisloDrawingBridge() {
setFrameEditMode(event.data.frameEdit === true, event.data.frameFields ?? {});
setHostReadOnly(event.data.readOnly === true);
setHostTitle(event.data.hostTitle ?? 'B07 상세 설계');
setHostSave(event.data.hostSave === true);
if (event.data.frameEdit) applyFramePreview();
// 앞 도면에서 켜 둔 그리기 도구를 내린다. 안 내리면 **확정한 도면 위에도**
// 그 도구가 계속 그린다 — 읽기 전용은 새 명령만 막기 때문이다(2026-09-01 실측:
+6
View File
@@ -193,6 +193,7 @@ let frameFields: Record<string, string> = {};
/** 부모가 보기 전용으로 실었는가 — M02 에서 고칠 권한이 없는 양식을 볼 때. */
let hostReadOnly = false;
let hostTitle = 'B07 상세 설계';
let hostSave = false;
/**
* 실은 뒤로 실제 편집이 있었는가. 도면을 바꾸기 전에 부모가 물어보는 근거다 —
@@ -498,6 +499,11 @@ export const setDesignMeta = (newMeta: DesignMeta | null) => {
/** 도각 편집 모드 켜고 끄기 — 자리표 패널의 표시 여부를 가른다 (2026-09-06 사용자 지시). */
/** 부모가 실은 도면을 보기 전용으로 둔다 — 확정본과 같은 막힘(도면을 실을 때마다 새로 정함). */
/** 제목표시줄 부제 — 부모가 도면을 실을 때 정함(없으면 B07 기본 글). */
/** 저장(💾 · Ctrl+S)을 부모 페이지 [저장]에 맡기는가 — M02 양식만 켠다. */
export const isHostSave = (): boolean => hostSave;
export const setHostSave = (enabled: boolean) => {
hostSave = enabled;
};
export const getHostTitle = (): string => hostTitle;
export const setHostTitle = (title: string) => {
hostTitle = title;