Files
Pier_v1/랜딩페이지/drawing-popup.ts
T

137 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import './drawing-popup.css';
export const drawingPopupMarkup = `
<dialog class="drawing-popup" id="drawing-popup" aria-labelledby="drawing-popup-title">
<header class="drawing-popup-head"><div><span>DRAWING IMPORT</span><h2 id="drawing-popup-title">도면에서 교각 정보 추출</h2></div><button type="button" class="drawing-popup-close" aria-label="도면추출 닫기">×</button></header>
<p class="drawing-popup-intro">종평면도·받침배치도·ELEVATION DWG가 들어 있는 폴더를 선택하세요. 기존 추출 엔진으로 Excel을 만들고 바로 불러옵니다.</p>
<label class="drawing-folder-label" for="drawing-folder">도면 폴더</label>
<div class="drawing-folder-row"><input id="drawing-folder" type="text" placeholder="도면 폴더 경로를 입력하거나 선택하세요" autocomplete="off" /><button id="drawing-choose" type="button">폴더 선택</button></div>
<div class="drawing-run-row"><button id="drawing-start" type="button" class="drawing-primary" disabled>자동 추출 시작</button><span id="drawing-status" role="status" aria-live="polite">도면 폴더를 선택하세요.</span></div>
<progress id="drawing-progress" aria-label="도면 추출 진행 중" hidden></progress>
<section class="drawing-log-section" aria-labelledby="drawing-log-title"><h3 id="drawing-log-title">진행 내용</h3><pre id="drawing-logs" tabindex="0" aria-label="도면추출 진행 로그">아직 추출을 시작하지 않았습니다.</pre></section>
<section id="drawing-results" hidden><h3>추출 결과</h3><p id="drawing-result-note"></p><div id="drawing-books"></div><p id="drawing-output-path"></p></section>
<footer class="drawing-popup-foot"><span>닫아도 진행 중인 추출은 계속됩니다. 미확정 값은 Excel에서 확인하세요.</span><button type="button" class="drawing-popup-done">닫기</button></footer>
</dialog>`;
type Book = { index: number; name: string; fileName: string; reviewCount: number };
type Job = { id: string; folder: string; status: 'running' | 'completed' | 'needs_review' | 'error'; logs: string[]; error?: string;
result?: { outputDir: string; reviewCount: number; failedFileCount: number; workbooks: Book[] } };
export function createDrawingPopup(root: HTMLElement, beforeOpen: () => void): { open: () => void } {
const popup = root.querySelector<HTMLDialogElement>('#drawing-popup')!;
const folder = popup.querySelector<HTMLInputElement>('#drawing-folder')!;
const choose = popup.querySelector<HTMLButtonElement>('#drawing-choose')!;
const start = popup.querySelector<HTMLButtonElement>('#drawing-start')!;
const status = popup.querySelector<HTMLElement>('#drawing-status')!;
const progress = popup.querySelector<HTMLProgressElement>('#drawing-progress')!;
const logs = popup.querySelector<HTMLElement>('#drawing-logs')!;
const results = popup.querySelector<HTMLElement>('#drawing-results')!;
const books = popup.querySelector<HTMLElement>('#drawing-books')!;
const cardNote = root.querySelector<HTMLElement>('#landing-drawing-status')!;
const cardButton = root.querySelector<HTMLButtonElement>('[data-card="drawing"] button')!;
let job: Job | undefined, timer: number | undefined, busy = false, rendered = '';
const request = async (action: string, body: object = {}): Promise<Response> => {
const response = await fetch(`${import.meta.env.BASE_URL}__pierzainer/drawing-extractor/${action}`, {
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Pierzainer-Action': 'drawing-extractor' }, body: JSON.stringify(body),
});
if (!response.ok) {
const data = await response.json().catch(() => null) as { error?: string } | null;
throw new Error(data?.error || '로컬 도면추출 연결을 확인하세요. 이 PC에서 Pierzainer를 실행해야 합니다.');
}
return response;
};
const json = async <T>(action: string, body: object = {}): Promise<T> => {
const response = await request(action, body);
try { return await response.json() as T; }
catch { throw new Error('도면추출 서버에 연결할 수 없습니다. Pierzainer를 다시 실행해 주세요.'); }
};
const controls = (): void => {
const running = job?.status === 'running';
folder.disabled = choose.disabled = busy || running;
start.disabled = busy || running || !folder.value.trim();
progress.hidden = !running;
start.textContent = running ? '추출 중…' : '자동 추출 시작';
};
const showError = (error: unknown): void => { status.textContent = (error as Error).message; status.dataset.state = 'error'; };
const loadBook = async (book: Book, download: boolean): Promise<void> => {
if (!job) return;
books.querySelectorAll('button').forEach(button => { button.disabled = true; });
try {
const response = await request('workbook', { jobId: job.id, index: book.index });
const bytes = new Uint8Array(await response.arrayBuffer());
if (download) {
const url = URL.createObjectURL(new Blob([bytes], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }));
const link = document.createElement('a'); link.href = url; link.download = book.fileName; link.click();
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
} else {
if (!window.loadBytesToApp) throw new Error('교각 Excel 가져오기를 찾을 수 없습니다.');
window.dispatchEvent(new CustomEvent('pierzainer:ground-source',{detail:{name:book.fileName,bytes,folder:job.folder}}));
window.loadBytesToApp(bytes, book.fileName);
const name = document.querySelector<HTMLInputElement>('#project-name')!;
const bridge = document.querySelector<HTMLInputElement>('#bridge-name')!;
name.value = book.name; bridge.value = book.name;
name.dispatchEvent(new Event('input', { bubbles: true }));
popup.close();
root.classList.add('modal-open');
const importer = document.querySelector<HTMLDialogElement>('#import-dialog')!;
if (!importer.open) importer.showModal();
name.focus();
}
} catch (error) { showError(error); }
finally { books.querySelectorAll('button').forEach(button => { button.disabled = false; }); }
};
const render = (value: Job): void => {
job = value; folder.value = value.folder;
logs.textContent = value.logs.join('\n'); logs.scrollTop = logs.scrollHeight;
const running = value.status === 'running';
status.dataset.state = value.status;
status.textContent = running ? '도면을 읽고 표·좌표를 분석하고 있습니다…' : value.status === 'error' ? value.error || '추출에 실패했습니다.' : '추출이 완료됐습니다. 결과 Excel을 확인하세요.';
cardNote.textContent = running ? '도면 추출 진행 중' : value.status === 'error' ? '추출 로그를 확인하세요.' : '추출 완료 · 결과 확인';
results.hidden = !value.result || running || value.status === 'error';
if (value.result && rendered !== value.id) {
rendered = value.id; books.replaceChildren();
popup.querySelector<HTMLElement>('#drawing-result-note')!.textContent = `Excel ${value.result.workbooks.length}개 · 검토 항목 ${value.result.reviewCount}개 · 파일 읽기 실패 ${value.result.failedFileCount}개`;
popup.querySelector<HTMLElement>('#drawing-output-path')!.textContent = `저장 위치: ${value.result.outputDir}`;
for (const book of value.result.workbooks) {
const row = document.createElement('div'); row.className = 'drawing-book';
const title = document.createElement('strong'); title.textContent = book.name;
const note = document.createElement('span'); note.textContent = `검토 항목 ${book.reviewCount}개`;
const actions = document.createElement('div'); actions.className = 'drawing-book-actions';
const download = document.createElement('button'); download.type = 'button'; download.textContent = 'Excel 저장';
const load = document.createElement('button'); load.type = 'button'; load.className = 'drawing-primary'; load.textContent = '교각 불러오기';
download.addEventListener('click', () => { void loadBook(book, true); });
load.addEventListener('click', () => { void loadBook(book, false); });
actions.append(download, load); row.append(title, note, actions); books.append(row);
}
}
controls();
};
const poll = async (): Promise<void> => {
clearTimeout(timer);
try {
const data = await json<{ job: Job | null }>('status', job ? { jobId: job.id } : {});
if (data.job) render(data.job);
else if (job?.status === 'running') { job = undefined; controls(); showError(new Error('실행 서버가 다시 시작됐습니다. 폴더를 확인하고 다시 추출해 주세요.')); }
if (popup.open && job?.status === 'running') timer = window.setTimeout(() => { void poll(); }, 900);
} catch (error) { showError(error); if (popup.open && job?.status === 'running') timer = window.setTimeout(() => { void poll(); }, 2000); }
};
folder.addEventListener('input', controls);
choose.addEventListener('click', async () => {
busy = true; controls(); status.textContent = '폴더 선택 창에서 도면 폴더를 선택하세요.';
try { const data = await json<{ folder: string }>('choose-folder'); if (data.folder) folder.value = data.folder; status.textContent = '도면 폴더를 확인하고 자동 추출을 시작하세요.'; status.dataset.state = ''; }
catch (error) { showError(error); }
finally { busy = false; controls(); }
});
start.addEventListener('click', async () => {
busy = true; controls(); status.textContent = '추출을 준비하고 있습니다…'; results.hidden = true;
try { const data = await json<{ job: Job }>('start', { folder: folder.value.trim() }); rendered = ''; render(data.job); void poll(); }
catch (error) { showError(error); }
finally { busy = false; controls(); }
});
popup.querySelectorAll('.drawing-popup-close, .drawing-popup-done').forEach(button => button.addEventListener('click', () => popup.close()));
popup.addEventListener('close', () => { clearTimeout(timer); cardButton.focus(); });
return { open() { beforeOpen(); if (!popup.open) popup.showModal(); controls(); void poll(); } };
}