Update landing page and ground link features
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
1 parent
a5cc458252
commit
7efe9097ce
28 files changed
+928
-120
No files matched your search
+15
-106
@@ -1,8 +1,10 @@
|
||||
import '../pierzainer_ground/ground-link';
|
||||
import { loadSampleIntoApp } from './sample-loader';
|
||||
import { createSampleExplorer, sampleExplorerMarkup, type Sample } from './sample-explorer';
|
||||
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 './landing-typography.css';
|
||||
import { createDrawingPopup, drawingPopupMarkup } from './drawing-popup';
|
||||
|
||||
const appRoot = document.querySelector<HTMLDivElement>('.app');
|
||||
@@ -33,12 +35,12 @@ const landingPanelMarkup = `
|
||||
<div class="landing-hero-copy">
|
||||
<div class="landing-title-group">
|
||||
<h1 class="landing-title">PIER <span>&</span> ABUT</h1>
|
||||
<h2 class="landing-headline">교각 설계의 새로운 시작</h2>
|
||||
<h2 class="landing-headline">교량 하부구조 3D 통합모델링</h2>
|
||||
</div>
|
||||
<div class="landing-brand-meta">
|
||||
<span class="landing-kicker"><span class="landing-status-light"></span> PIERZAINER <span class="landing-kicker-divider">/</span><span class="landing-kicker-detail">BRIDGE DESIGN STUDIO</span></span>
|
||||
<p class="landing-subtitle">
|
||||
설계정보 입력부터 3D 모델링, 구조해석, 철근배근, 도면 및 수량산출까지 하나의 플랫폼에서
|
||||
설계정보 입력부터 3D 모델링, 구조검토, 철근배근, 도면작성 및 수량산출까지 하나의 플랫폼에서
|
||||
</p>
|
||||
<button class="landing-guide-trigger" type="button" aria-haspopup="dialog"><span aria-hidden="true">?</span>사용 가이드 <span class="landing-guide-trigger-arrow" aria-hidden="true">↗</span></button>
|
||||
</div>
|
||||
@@ -50,7 +52,6 @@ const landingPanelMarkup = `
|
||||
${heroMotionMarkup(promoImageUrl)}
|
||||
</div>
|
||||
</div>
|
||||
<span class="landing-showcase-label">DIGITAL TWIN SHOWCASE <span>브랜딩 모션 · 해석 결과 아님</span></span>
|
||||
</div>
|
||||
|
||||
<section class="landing-cards" aria-label="입력 방식 선택">
|
||||
@@ -58,8 +59,8 @@ const landingPanelMarkup = `
|
||||
<div>
|
||||
<div class="landing-card-top"><div class="landing-card-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9zM14 3v6h6M8 13h8M8 17h8M12 13v4"/></svg></div><span class="landing-card-number">01 / IMPORT</span></div>
|
||||
<span class="landing-card-eyebrow">Excel Import</span>
|
||||
<h3>엑셀 불러오기</h3>
|
||||
<p>기존에 작성된 설계정보 Excel 파일을 불러와 모델링을 시작합니다.</p>
|
||||
<h3>엑셀로 시작하기</h3>
|
||||
<p>설계정보를 불러와 교각·교대 3D 모델을 자동 생성합니다.</p>
|
||||
</div>
|
||||
<div class="landing-card-actions">
|
||||
<span class="landing-card-note">Excel · .xlsx</span>
|
||||
@@ -71,8 +72,8 @@ const landingPanelMarkup = `
|
||||
<div>
|
||||
<div class="landing-card-top"><div class="landing-card-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 4h16v16H4zM4 9h7v11M11 4v5h9M15 13h2v3h-2M7 13v3"/></svg></div><span class="landing-card-number">02 / EXTRACT</span></div>
|
||||
<span class="landing-card-eyebrow">Drawing Import</span>
|
||||
<h3>도면 불러오기</h3>
|
||||
<p>도면 폴더에서 교각 정보를 추출하고 결과 Excel을 바로 불러옵니다.</p>
|
||||
<h3>도면으로 시작하기</h3>
|
||||
<p>기존 도면에서 교각·교대 정보를 추출하여 3D 모델을 생성합니다.</p>
|
||||
</div>
|
||||
<div class="landing-card-actions">
|
||||
<span class="landing-card-note" id="landing-drawing-status" role="status" aria-live="polite">프로그램 안에서 도면 추출</span>
|
||||
@@ -80,18 +81,7 @@ const landingPanelMarkup = `
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="landing-card" data-card="sample">
|
||||
<div>
|
||||
<div class="landing-card-top"><div class="landing-card-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m12 3 9 5-9 5-9-5zM3 8v9l9 5 9-5V8M12 13v9"/></svg></div><span class="landing-card-number">03 / EXPLORE</span></div>
|
||||
<span class="landing-card-eyebrow">Explore Sample</span>
|
||||
<h3>샘플 프로젝트</h3>
|
||||
<p>실제 교량의 교각·지반·위성사진·2D 지도를 함께 살펴봅니다.</p>
|
||||
</div>
|
||||
<div class="landing-card-actions">
|
||||
<span class="landing-card-note">자료 12개 · 바로 열기 ${sampleProjects.filter(s => !s.blockReason).length}개</span>
|
||||
<button class="landing-primary" type="button">샘플 모델 선택</button>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
</section>
|
||||
<footer class="landing-footer">
|
||||
<div class="landing-footer-note" aria-hidden="true"><span>PRECISION IN EVERY DIMENSION</span><span>PIERZAINER · PIER & ABUT</span></div>
|
||||
@@ -99,6 +89,7 @@ const landingPanelMarkup = `
|
||||
</footer>
|
||||
</section>
|
||||
|
||||
${sampleExplorerMarkup}
|
||||
<section id="landing-excel-panel" class="landing-panel" aria-label="엑셀 입력 패널">
|
||||
<div class="landing-panel-head">
|
||||
<h3>엑셀 입력</h3>
|
||||
@@ -119,18 +110,7 @@ const landingPanelMarkup = `
|
||||
|
||||
${drawingPopupMarkup}
|
||||
|
||||
<section id="landing-sample-panel" class="landing-panel" aria-label="샘플 모델 패널">
|
||||
<div class="landing-panel-head">
|
||||
<h3>교각 + 지반 샘플 프로젝트</h3>
|
||||
<button type="button" class="landing-close-button" data-close-panel="landing-sample-panel">닫기</button>
|
||||
</div>
|
||||
<div class="landing-panel-body">
|
||||
<p class="landing-sample-intro">검증된 교량은 교각·지반·위성사진·2D 지도를 함께 엽니다. 확인이 필요한 샘플은 원인을 안내합니다. Excel 설계 지반고는 유지합니다.</p>
|
||||
<p class="landing-sample-status" id="landing-sample-status" role="status" aria-live="polite"></p>
|
||||
<div class="landing-sample-grid" id="landing-sample-grid"></div>
|
||||
<p class="landing-sample-caption">지도 좌표계는 원자료 확인이 필요한 추정값을 포함합니다. 지반면과 설계 지반고의 차이 및 기존 모델 검증 알림은 보존됩니다.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -240,17 +220,13 @@ const bindCard = (cardSelector: string, panelId: string, mode: 'panel' | 'picker
|
||||
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();
|
||||
@@ -286,74 +262,7 @@ document.querySelectorAll<HTMLButtonElement>('[data-pick-file]').forEach((button
|
||||
|
||||
bindFilePicker(document.querySelector('[data-pick-file="excel"]') as HTMLButtonElement, 'landing-excel-input', 'landing-excel-name');
|
||||
|
||||
const sampleGrid = document.querySelector<HTMLElement>('#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 = `
|
||||
<span class="landing-sample-badge">${String(sample.sample).padStart(2, '0')}</span>
|
||||
<strong>${sample.bridge}</strong>
|
||||
<span>교각 ${sample.sourcePierCount}기 · 지반 + 지도 2종</span>
|
||||
<span class="landing-sample-open">${sample.blockReason ? (sample.pierCount ? '앱 호환 오류 확인' : '방위각 입력 확인 필요') : `형상 ${sample.builtCount}/${sample.sourcePierCount}기 · 열기 ↗`}</span>
|
||||
`;
|
||||
item.dataset.sample = String(sample.sample);
|
||||
item.addEventListener('click', async () => {
|
||||
if (sampleLoading) return;
|
||||
const status = document.querySelector<HTMLElement>('#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<HTMLInputElement>('#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<void>((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);
|
||||
});
|
||||
}
|
||||
createSampleExplorer(landingOverlay,(sample:Sample)=>loadSampleIntoApp(sample,showApp,showLanding));
|
||||
|
||||
if (homeButton) {
|
||||
homeButton.addEventListener('click', () => {
|
||||
|
||||
Reference in new issue
Block a user