136 lines
10 KiB
TypeScript
136 lines
10 KiB
TypeScript
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.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(); } };
|
||
}
|