import { createLandingGuide } from './landing-guide'; import { createHeroMotion, heroMotionMarkup } from './landing-motion'; import './landing-white.css'; import { sampleProjects } from './sample-projects'; import { parseProject } from '../src/core/project'; import { createDrawingPopup, drawingPopupMarkup } from './drawing-popup'; const appRoot = document.querySelector('.app'); const homeButton = document.querySelector('#home-from-app'); const promoImageUrl = new URL('./assets/pierzainer-terrain-promo.png', import.meta.url).href; const companyLogosUrl = new URL('./assets/family-company-logos.png', import.meta.url).href; const landingOverlay = document.createElement('div'); landingOverlay.className = 'landing-overlay landing-white'; landingOverlay.setAttribute('role', 'dialog'); landingOverlay.setAttribute('aria-modal', 'true'); landingOverlay.setAttribute('aria-label', '프로그램 시작 선택'); const landingPanelMarkup = `

PIER & ABUT

교각 설계의 새로운 시작

PIERZAINER /BRIDGE DESIGN STUDIO

설계정보 입력부터 3D 모델링, 구조해석, 철근배근, 도면 및 수량산출까지 하나의 플랫폼에서

01 / IMPORT
Excel Import

엑셀 불러오기

기존에 작성된 설계정보 Excel 파일을 불러와 모델링을 시작합니다.

Excel · .xlsx
02 / EXTRACT
Drawing Import

도면 불러오기

도면 폴더에서 교각 정보를 추출하고 결과 Excel을 바로 불러옵니다.

프로그램 안에서 도면 추출
03 / EXPLORE
Explore Sample

샘플 프로젝트

실제 교량의 교각·지반·위성사진·2D 지도를 함께 살펴봅니다.

자료 12개 · 바로 열기 ${sampleProjects.filter(s => !s.blockReason).length}개
한맥기술, 삼안, 장헌산업, 피티씨, 장헌, 한라산업개발, 바로컨설턴트 로고

엑셀 입력

선택 파일 선택된 파일이 없습니다.
${drawingPopupMarkup}

교각 + 지반 샘플 프로젝트

검증된 교량은 교각·지반·위성사진·2D 지도를 함께 엽니다. 확인이 필요한 샘플은 원인을 안내합니다. Excel 설계 지반고는 유지합니다.

지도 좌표계는 원자료 확인이 필요한 추정값을 포함합니다. 지반면과 설계 지반고의 차이 및 기존 모델 검증 알림은 보존됩니다.

`; landingOverlay.innerHTML = landingPanelMarkup; document.body.prepend(landingOverlay); const landingGuide = createLandingGuide(landingOverlay); createHeroMotion(landingOverlay); const selectPanel = (panelId: string): void => { landingGuide.close(false); document.querySelectorAll('.landing-panel').forEach((panel) => { panel.classList.toggle('visible', panel.id === panelId); }); const visible = document.getElementById(panelId); if (visible) { visible.scrollTo({ top: 0, behavior: 'auto' }); } }; const closePanels = (): void => { document.querySelectorAll('.landing-panel').forEach((panel) => panel.classList.remove('visible')); }; const showApp = (): void => { landingGuide.close(false); if (appRoot) { appRoot.hidden = false; appRoot.setAttribute('aria-hidden', 'false'); } if (homeButton) { homeButton.hidden = false; } landingOverlay.hidden = true; }; const showLanding = (): void => { if (appRoot) { appRoot.hidden = true; appRoot.setAttribute('aria-hidden', 'true'); } if (homeButton) { homeButton.hidden = true; } landingOverlay.hidden = false; landingOverlay.classList.remove('modal-open'); closePanels(); }; const selectFile = (input: HTMLInputElement): void => { input.click(); }; const excelInput = document.querySelector('#landing-excel-input'); const excelName = document.querySelector('#landing-excel-name'); const excelStart = document.querySelector('#landing-excel-start'); const ensureNoLegacyImportDialog = (): void => { const dialog = document.querySelector('#import-dialog'); if (dialog && dialog.open) dialog.close(); }; if (excelInput && excelName && excelStart) { excelInput.addEventListener('change', () => { const file = excelInput.files?.[0]; const name = file ? file.name : '선택된 파일이 없습니다.'; excelName.textContent = name; excelStart.disabled = !file; ensureNoLegacyImportDialog(); }); excelStart.addEventListener('click', () => { if (!excelInput.files?.[0]) return; ensureNoLegacyImportDialog(); showApp(); }); } const bindFilePicker = (button: Element, targetId: string, nameTargetId: string): void => { const input = document.querySelector(`#${targetId}`); const output = document.querySelector(`#${nameTargetId}`); if (!input || !output) return; button.addEventListener('click', () => selectFile(input)); input.addEventListener('change', () => { const file = input.files?.[0]; output.textContent = file ? file.name : '미선택'; }); }; const bindCard = (cardSelector: string, panelId: string, mode: 'panel' | 'picker' = 'panel'): void => { const card = document.querySelector(cardSelector); const trigger = card?.querySelector('button'); if (!trigger) return; trigger.addEventListener('click', () => { landingGuide.dismissHint(); landingGuide.close(false); if (mode === 'picker') { if (cardSelector === '[data-card="excel"]') { if (landingOverlay) { landingOverlay.hidden = false; landingOverlay.classList.add('modal-open'); } const dialog = document.querySelector('#import-dialog'); if (dialog && !dialog.open) { dialog.showModal(); } const projectName = document.querySelector('#project-name'); if (projectName) projectName.focus(); return; } if (cardSelector === '[data-card="sample"]') { void window.openSampleProject?.(); return; } } selectPanel(panelId); }); }; bindCard('[data-card="excel"]', 'landing-excel-panel', 'picker'); bindCard('[data-card="sample"]', 'landing-sample-panel'); const drawingPopup = createDrawingPopup(landingOverlay, () => { landingGuide.dismissHint(); landingGuide.close(false); closePanels(); }); document.querySelector('[data-card="drawing"] button')!.addEventListener('click', () => drawingPopup.open()); document.querySelectorAll('[data-close-panel]').forEach((button) => { button.addEventListener('click', () => { const panelId = button.getAttribute('data-close-panel'); if (!panelId) return; const panel = document.getElementById(panelId); panel?.classList.remove('visible'); }); }); document.querySelectorAll('[data-pick-file]').forEach((button) => { const kind = button.getAttribute('data-pick-file'); if (!kind) return; const targetMap: Record = { excel: 'landing-excel-input', }; const targetId = targetMap[kind]; if (!targetId) return; const input = document.querySelector(`#${targetId}`); if (!input) return; button.addEventListener('click', () => selectFile(input)); }); bindFilePicker(document.querySelector('[data-pick-file="excel"]') as HTMLButtonElement, 'landing-excel-input', 'landing-excel-name'); const sampleGrid = document.querySelector('#landing-sample-grid'); let sampleLoading = false; if (sampleGrid) { sampleProjects.forEach((sample) => { const item = document.createElement('button'); item.type = 'button'; item.className = 'landing-sample'; item.innerHTML = ` ${String(sample.sample).padStart(2, '0')} ${sample.bridge} 교각 ${sample.sourcePierCount}기 · 지반 + 지도 2종 ${sample.blockReason ? (sample.pierCount ? '앱 호환 오류 확인' : '방위각 입력 확인 필요') : `형상 ${sample.builtCount}/${sample.sourcePierCount}기 · 열기 ↗`} `; item.dataset.sample = String(sample.sample); item.addEventListener('click', async () => { if (sampleLoading) return; const status = document.querySelector('#landing-sample-status')!; if (sample.blockReason) { status.textContent = `${sample.bridge}: ${sample.blockReason}`; return; } sampleLoading = true; status.textContent = `${sample.bridge} · 교각과 지반·지도를 불러오는 중…`; sampleGrid.setAttribute('aria-busy', 'true'); sampleGrid.querySelectorAll('button').forEach(button => { button.disabled = true; }); try { const response = await fetch(sample.url); if (!response.ok) throw new Error(`샘플 다운로드 실패 (${response.status})`); const text = await response.text(); const project = parseProject(text); if (project.project.bridge !== sample.bridge || project.piers.length !== sample.pierCount || project.groundModel?.mapLayers?.length !== 2) { throw new Error('샘플의 교각 또는 지도 데이터가 일치하지 않습니다.'); } const picker = document.querySelector('#project-file'); const toasts = document.querySelector('#toast-host'); if (!picker || !toasts) throw new Error('프로젝트 열기 기능을 찾을 수 없습니다.'); // Reuse the app's existing project-file import. No parallel model loader or global state edits. const opened = new Promise((resolve, reject) => { const observer = new MutationObserver(records => { for (const record of records) for (const node of record.addedNodes) { const message = node.textContent ?? ''; if (message.includes(`${sample.filename}을 열었습니다.`)) { cleanup(); resolve(); } else if (message.includes('프로젝트 파일을 열지 못했습니다:')) { cleanup(); reject(new Error(message)); } } }); const timer = window.setTimeout(() => { cleanup(); reject(new Error('프로젝트 열기 완료를 확인하지 못했습니다. 다시 선택해 주세요.')); }, 60000); const cleanup = (): void => { observer.disconnect(); window.clearTimeout(timer); }; observer.observe(toasts, {childList:true, subtree:true}); }); const transfer = new DataTransfer(); transfer.items.add(new File([text], sample.filename, {type:'application/json'})); picker.files = transfer.files; showApp(); picker.dispatchEvent(new Event('change', {bubbles:true})); await opened; sampleGrid.querySelectorAll('.primary').forEach(button => button.classList.remove('primary')); item.classList.add('primary'); status.textContent = `${sample.bridge} · 교각 ${sample.pierCount}기와 지도 포함 지반을 열었습니다.`; } catch (error) { showLanding(); selectPanel('landing-sample-panel'); status.textContent = `${sample.bridge}: ${(error as Error).message}`; } finally { sampleLoading = false; sampleGrid.removeAttribute('aria-busy'); sampleGrid.querySelectorAll('button').forEach(button => { button.disabled = false; }); } }); sampleGrid.appendChild(item); }); } if (homeButton) { homeButton.addEventListener('click', () => { showLanding(); }); } if (appRoot) { appRoot.hidden = true; appRoot.setAttribute('aria-hidden', 'true'); } if (homeButton) { homeButton.hidden = true; } showLanding();