fix: resolve workspace integration blockers (#5)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 },
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user