import './drawing-popup.css';
export const drawingPopupMarkup = `
`;
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(); } };
}