feat(B07): 그린 것을 블록으로 저장해 다시 놓고, 사진 넣기를 되살린다

조사표 4·7절 검토 결과를 반영한다.

- 블록 라이브러리: 선택한 객체를 이름 붙여 저장(BLOCK)하고 팔레트에서 골라
  클릭 한 번으로 놓는다(INSERT). 정의-참조 링크 없는 클론 방식이라 새 엔티티
  타입 없이 기존 직렬화·groupId를 그대로 쓴다. 기준점은 선택 영역 중심.
  라이브러리는 도면과 분리해 브라우저에 두고, 유실 대비로 파일 내보내기·
  가져오기를 붙였다.
- 사진 넣기: IMAGEATTACH가 파일 선택창을 띄우지 않아 도구가 FILE_SELECTED를
  영원히 기다리고 있었다. 도구형 명령도 run()을 함께 부르게 하고 파일 선택을
  붙여 되살렸다.
- 조사표 4·7절의 반영 등급·반영 열을 실제 코드에 맞춰 정정하고, 반영하지
  않기로 한 항목의 사유를 절마다 남겼다. 7절은 신규 구현 없음.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-30 14:00:42 +09:00
co-authored by Claude Opus 5
parent eb4303253a
commit 3fccf8278b
13 changed files with 500 additions and 40 deletions
@@ -0,0 +1,73 @@
/** 블록 라이브러리 팔레트 — 저장한 블록을 고르면 삽입 도구가 켜진다 (INSERT/BLOCK) */
import type { FC } from 'react';
import {
deleteBlock,
exportBlocksToFile,
getBlocks,
importBlocksFromFile,
} from '../blocks/block-library';
import { pickFile } from '../helpers/pick-file';
import { startBlockInsert } from '../tools/insert-block-tool';
import { isBlockLibraryVisible, setBlockLibraryVisible } from './ui-state';
export const BlockLibraryPanel: FC = () => {
if (!isBlockLibraryVisible()) return null;
const blocks = getBlocks();
return (
<div className="cad-block-library controls">
<div className="cad-quick-properties__header">
<span> </span>
<button type="button" onClick={() => setBlockLibraryVisible(false)} title="닫기">
</button>
</div>
{blocks.length === 0 ? (
<p className="cad-block-library__empty"> BLOCK .</p>
) : (
<ul className="cad-block-library__list">
{blocks.map((block) => (
<li key={block.name}>
<button
type="button"
className="cad-block-library__name"
title={`객체 ${block.entities.length}개 — 클릭하면 삽입`}
onClick={() => {
setBlockLibraryVisible(false);
void startBlockInsert(block);
}}
>
{block.name}
</button>
<button
type="button"
title="삭제"
onClick={() => {
if (window.confirm(`블록 '${block.name}'을 삭제하시겠습니까?`)) {
deleteBlock(block.name);
}
}}
>
</button>
</li>
))}
</ul>
)}
<div className="cad-block-library__actions">
<button type="button" onClick={() => exportBlocksToFile()}>
</button>
<button
type="button"
onClick={() => pickFile('application/json', (file) => importBlocksFromFile(file))}
>
</button>
</div>
</div>
);
};
@@ -6,6 +6,7 @@ import { type FC, useEffect, useState } from 'react';
import { CommandLine } from './CommandLine';
import { InspectorPanel } from './InspectorPanel';
import { QuickProperties } from './QuickProperties';
import { BlockLibraryPanel } from './BlockLibraryPanel';
import { LayersWidget, PropertiesWidget, TextStyleWidget } from './RibbonWidgets';
import { QuickAccessBar } from './QuickAccessBar';
import { Ribbon } from '../ribbon/Ribbon';
@@ -51,6 +52,7 @@ export const Toolbar: FC = () => {
/>
<ViewControls />
<QuickProperties />
<BlockLibraryPanel />
{commandLineVisible && <CommandLine />}
<StatusBar
commandLineVisible={commandLineVisible}
@@ -10,6 +10,7 @@ let inspectorTab: InspectorTab = 'layers';
let inspectorCollapsed = false;
let quickPropertiesVisible = false;
let activeRibbonTab = 'home';
let blockLibraryVisible = false;
const notify = () => window.dispatchEvent(new CustomEvent(HtmlEvent.UPDATE_STATE));
@@ -17,6 +18,7 @@ export const getInspectorTab = () => inspectorTab;
export const isInspectorCollapsed = () => inspectorCollapsed;
export const isQuickPropertiesVisible = () => quickPropertiesVisible;
export const getActiveRibbonTab = () => activeRibbonTab;
export const isBlockLibraryVisible = () => blockLibraryVisible;
export function setActiveRibbonTab(tabId: string): void {
activeRibbonTab = tabId;
@@ -38,3 +40,8 @@ export function setQuickPropertiesVisible(visible: boolean): void {
quickPropertiesVisible = visible;
notify();
}
export function setBlockLibraryVisible(visible: boolean): void {
blockLibraryVisible = visible;
notify();
}