Update Pier_v1 project

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
CopilotandCopilot committed 2026-11-03 12:04:58 +09:00
1 parent f25e36c050
commit ad73ddec2b
2476 files changed
+3423 -4902

No files matched your search

+135
View File
@@ -0,0 +1,135 @@
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(); } };
}