fix: resolve workspace integration blockers (#5)

This commit is contained in:
2026-07-29 10:01:17 +09:00
parent 3279ac099e
commit cadbd5fb60
37 changed files with 1817 additions and 73 deletions
+2 -1
View File
@@ -3,6 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'/%3E" />
<title>DWG/DXF · 모듈 조합 샘플</title>
<style>
:root {
@@ -90,7 +91,7 @@
</ol>
<p style="margin:10px 0 0">
코드: <code>src/main.ts</code> ·
지도: <a href="/docs/modules-dwg-dxf.html" target="_blank">docs/modules-dwg-dxf.html</a>
지도: <a href="./docs/modules-dwg-dxf.html" target="_blank">docs/modules-dwg-dxf.html</a>
</p>
</aside>
+1 -1
View File
@@ -4,7 +4,7 @@
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite --port 5173 --strictPort",
"dev": "vite --host 127.0.0.1 --port 5173 --strictPort",
"build": "tsc --noEmit && vite build",
"preview": "vite preview",
"typecheck": "tsc --noEmit"
@@ -0,0 +1,72 @@
export const MAX_CAD_BYTES = 50 * 1024 * 1024;
export const CAD_FETCH_TIMEOUT_MS = 15_000;
export function assertCadByteLength(byteLength: number): void {
if (!Number.isFinite(byteLength) || byteLength < 0) {
throw new Error('CAD 파일 크기를 확인할 수 없습니다.');
}
if (byteLength > MAX_CAD_BYTES) {
throw new Error('CAD 파일은 50 MiB 이하여야 합니다.');
}
}
export function resolveCadUrl(input: string, base: URL): URL {
const url = new URL(input, base);
if (!['http:', 'https:'].includes(url.protocol)) {
throw new Error('CAD URL은 HTTP(S)만 허용합니다.');
}
if (url.origin !== base.origin) {
throw new Error('CAD URL은 viewer와 같은 origin만 허용합니다.');
}
return url;
}
export async function fetchCadBuffer(url: URL): Promise<ArrayBuffer> {
const controller = new AbortController();
const timeout = window.setTimeout(() => controller.abort(), CAD_FETCH_TIMEOUT_MS);
try {
const response = await fetch(url, {
credentials: 'same-origin',
signal: controller.signal,
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const declaredLength = Number(response.headers.get('content-length'));
if (Number.isFinite(declaredLength) && declaredLength > 0) {
assertCadByteLength(declaredLength);
}
if (!response.body) {
const buffer = await response.arrayBuffer();
assertCadByteLength(buffer.byteLength);
return buffer;
}
const reader = response.body.getReader();
const chunks: Uint8Array[] = [];
let total = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
total += value.byteLength;
assertCadByteLength(total);
chunks.push(value);
}
const data = new Uint8Array(total);
let offset = 0;
for (const chunk of chunks) {
data.set(chunk, offset);
offset += chunk.byteLength;
}
return data.buffer;
} catch (error) {
if (controller.signal.aborted) {
throw new Error('CAD 파일 요청 시간이 초과되었습니다.');
}
throw error;
} finally {
window.clearTimeout(timeout);
}
}
+33 -7
View File
@@ -26,6 +26,11 @@ import {
parseCad,
type CadParseResult,
} from '@hmwebviewer/viewer2d';
import {
assertCadByteLength,
fetchCadBuffer,
resolveCadUrl,
} from './cadInputPolicy';
// parseDxfBuffer 내부:
// dxf-parser → HatchHandler → dxfAdapter
@@ -50,6 +55,12 @@ function setStatus(msg: string, err = false) {
const viewer = createViewer2D(app, {
fontUrl: publicUrl('fonts/NanumGothic-Regular.ttf'),
});
declare global {
interface Window {
__viewer2d?: typeof viewer;
}
}
window.__viewer2d = viewer;
viewer.onSelect((entity: unknown) => {
if (!entity) {
setStatus('deselected');
@@ -68,14 +79,14 @@ async function loadAndRender(buf: ArrayBuffer, name: string): Promise<CadParseRe
}
async function loadUrl(url: string, nameHint?: string) {
const name = nameHint ?? url.split('?')[0]!.split('/').pop() ?? url;
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return loadAndRender(await res.arrayBuffer(), name);
const resolved = resolveCadUrl(url, new URL(window.location.href));
const name = nameHint ?? resolved.pathname.split('/').pop() ?? resolved.href;
return loadAndRender(await fetchCadBuffer(resolved), name);
}
async function loadFile(file: File) {
if (!isCad2DFile(file.name)) throw new Error(`not .dwg/.dxf: ${file.name}`);
assertCadByteLength(file.size);
return loadAndRender(await file.arrayBuffer(), file.name);
}
@@ -154,7 +165,22 @@ window.addEventListener('drop', (e) => {
});
const model = new URLSearchParams(location.search).get('model');
if (model) void run(model, () => loadUrl(model));
else setStatus('ready · Sample DWG / DXF 또는 파일 드롭');
if (model) {
try {
const resolved = resolveCadUrl(model, new URL(window.location.href));
void run(model, () => loadUrl(resolved.href));
} catch (error) {
setStatus((error as Error).message, true);
}
} else {
setStatus('ready · Sample DWG / DXF 또는 파일 드롭');
}
window.addEventListener('resize', () => viewer.resize());
window.addEventListener(
'pagehide',
() => {
viewer.dispose();
delete window.__viewer2d;
},
{ once: true },
);