import './drawing-popup.css'; export const drawingPopupMarkup = `
DRAWING IMPORT

도면에서 교각 정보 추출

종평면도·받침배치도·ELEVATION DWG가 들어 있는 폴더를 선택하세요. 기존 추출 엔진으로 Excel을 만들고 바로 불러옵니다.

도면 폴더를 선택하세요.

진행 내용

아직 추출을 시작하지 않았습니다.
`; 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('#drawing-popup')!; const folder = popup.querySelector('#drawing-folder')!; const choose = popup.querySelector('#drawing-choose')!; const start = popup.querySelector('#drawing-start')!; const status = popup.querySelector('#drawing-status')!; const progress = popup.querySelector('#drawing-progress')!; const logs = popup.querySelector('#drawing-logs')!; const results = popup.querySelector('#drawing-results')!; const books = popup.querySelector('#drawing-books')!; const cardNote = root.querySelector('#landing-drawing-status')!; const cardButton = root.querySelector('[data-card="drawing"] button')!; let job: Job | undefined, timer: number | undefined, busy = false, rendered = ''; const request = async (action: string, body: object = {}): Promise => { 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 (action: string, body: object = {}): Promise => { 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 => { 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('#project-name')!; const bridge = document.querySelector('#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('#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('#drawing-result-note')!.textContent = `Excel ${value.result.workbooks.length}개 · 검토 항목 ${value.result.reviewCount}개 · 파일 읽기 실패 ${value.result.failedFileCount}개`; popup.querySelector('#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 => { 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(); } }; }