카드 제목·버튼 문구와 팝업/닫기 접근성 이름을 함께 변경. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
297 lines
13 KiB
TypeScript
297 lines
13 KiB
TypeScript
import '../pierzainer_ground/ground-link';
|
||
import { loadSampleIntoApp } from './sample-loader';
|
||
import { createSampleExplorer, sampleExplorerMarkup, type Sample } from './sample-explorer';
|
||
import { createLandingGuide } from './landing-guide';
|
||
import { createIntroVideo, introVideoTriggerMarkup } from './landing-video';
|
||
import { createHeroMotion, heroMotionMarkup } from './landing-motion';
|
||
import { heroCapture } from './assets/okseong-hero-capture';
|
||
import './landing-white.css';
|
||
import './landing-typography.css';
|
||
import './sample-library-popup.css';
|
||
import { createDrawingPopup, drawingPopupMarkup } from './drawing-popup';
|
||
|
||
const appRoot = document.querySelector<HTMLDivElement>('.app');
|
||
const homeButton = document.querySelector<HTMLButtonElement>('#home-from-app');
|
||
const promoImageUrl = new URL('./assets/okseong-model-hero.webp', 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 = `
|
||
<div class="landing-shell">
|
||
<section class="landing-hero" aria-label="프로그램 홍보 영역">
|
||
<div class="landing-engineering-grid" aria-hidden="true"></div>
|
||
<div class="landing-light-stage" aria-hidden="true">
|
||
<div class="landing-glow"></div>
|
||
<div class="landing-light-ring"></div>
|
||
<div class="landing-orbit landing-orbit-one"></div>
|
||
<div class="landing-orbit landing-orbit-two"></div>
|
||
<div class="landing-orbit landing-orbit-three"></div>
|
||
<div class="landing-particles">
|
||
<i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i>
|
||
</div>
|
||
</div>
|
||
<div class="landing-hero-copy">
|
||
<div class="landing-title-group">
|
||
<h1 class="landing-title">PIER <span>&</span> ABUT</h1>
|
||
<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 모델링, 구조검토, 철근배근, 도면작성 및 수량산출까지 하나의 플랫폼에서
|
||
</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>${introVideoTriggerMarkup}
|
||
</div>
|
||
</div>
|
||
<div class="landing-hero-visual" aria-hidden="true">
|
||
<div class="landing-visual">
|
||
<div class="landing-artwork" style="aspect-ratio:${heroCapture.width}/${heroCapture.height}">
|
||
<img class="landing-promo-image" src="${promoImageUrl}" width="${heroCapture.width}" height="${heroCapture.height}" alt="" draggable="false" fetchpriority="high" />
|
||
${heroMotionMarkup()}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<section class="landing-cards" aria-label="입력 방식 선택">
|
||
<article class="landing-card" data-card="excel">
|
||
<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>설계정보를 불러와 교각·교대 3D 모델을 자동 생성합니다.</p>
|
||
</div>
|
||
<div class="landing-card-actions">
|
||
<span class="landing-card-note">Excel · .xlsx</span>
|
||
<button class="landing-primary" type="button">Excel 파일 불러오기</button>
|
||
</div>
|
||
</article>
|
||
|
||
<article class="landing-card" data-card="drawing">
|
||
<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>기존 도면에서 교각·교대 정보를 추출하여 3D 모델을 생성합니다.</p>
|
||
</div>
|
||
<div class="landing-card-actions">
|
||
<span class="landing-card-note" id="landing-drawing-status" role="status" aria-live="polite">프로그램 안에서 도면 추출</span>
|
||
<button class="landing-primary" type="button">도면 불러오기</button>
|
||
</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="M3 20V8l6-4 6 4 6-4v16H3zM9 4v16M15 8v12"/></svg></div><span class="landing-card-number">03 / EXPLORE</span></div><span class="landing-card-eyebrow">Design Library</span><h3>설계 DATA 아카이브</h3><p>기존 설계 프로젝트의 교각·지형·지도를 살펴보고 불러옵니다.</p></div>
|
||
<div class="landing-card-actions"><span class="landing-card-note">12개 설계 프로젝트</span><button class="landing-primary" type="button" aria-haspopup="dialog">둘러보기</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>
|
||
<div class="landing-company-logos"><img src="${companyLogosUrl}" width="9719" height="295" alt="한맥기술, 삼안, 장헌산업, 피티씨, 장헌, 한라산업개발, 바로컨설턴트 로고" draggable="false" /></div>
|
||
</footer>
|
||
</section>
|
||
|
||
<dialog class="sample-library-popup" aria-label="설계 DATA 아카이브"><button type="button" class="sample-library-close" aria-label="설계 DATA 아카이브 닫기">×</button>${sampleExplorerMarkup}</dialog>
|
||
<section id="landing-excel-panel" class="landing-panel" aria-label="엑셀 입력 패널">
|
||
<div class="landing-panel-head">
|
||
<h3>엑셀 입력</h3>
|
||
<button type="button" class="landing-close-button" data-close-panel="landing-excel-panel">닫기</button>
|
||
</div>
|
||
<div class="landing-panel-body">
|
||
<div class="landing-file-meta">
|
||
<span>선택 파일</span>
|
||
<strong id="landing-excel-name">선택된 파일이 없습니다.</strong>
|
||
</div>
|
||
<div class="landing-actions-row">
|
||
<input id="landing-excel-input" type="file" accept=".xlsx,.xls,.xlsm" hidden />
|
||
<button class="landing-ghost" type="button" data-pick-file="excel">파일 선택</button>
|
||
<button id="landing-excel-start" class="landing-primary" type="button" disabled>모델링 시작</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
${drawingPopupMarkup}
|
||
|
||
|
||
</div>
|
||
`;
|
||
|
||
landingOverlay.innerHTML = landingPanelMarkup;
|
||
document.body.prepend(landingOverlay);
|
||
const landingGuide = createLandingGuide(landingOverlay);
|
||
const introVideo = createIntroVideo(landingOverlay, promoImageUrl);
|
||
createHeroMotion(landingOverlay);
|
||
|
||
const selectPanel = (panelId: string): void => {
|
||
landingGuide.close(false);
|
||
document.querySelectorAll<HTMLElement>('.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<HTMLElement>('.landing-panel').forEach((panel) => panel.classList.remove('visible'));
|
||
};
|
||
|
||
const showApp = (): void => {
|
||
landingOverlay.querySelector<HTMLDialogElement>('.sample-library-popup')?.close();
|
||
landingGuide.close(false);
|
||
introVideo.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<HTMLInputElement>('#landing-excel-input');
|
||
const excelName = document.querySelector<HTMLElement>('#landing-excel-name');
|
||
const excelStart = document.querySelector<HTMLButtonElement>('#landing-excel-start');
|
||
|
||
const ensureNoLegacyImportDialog = (): void => {
|
||
const dialog = document.querySelector<HTMLDialogElement>('#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<HTMLInputElement>(`#${targetId}`);
|
||
const output = document.querySelector<HTMLElement>(`#${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<HTMLElement>(cardSelector);
|
||
const trigger = card?.querySelector<HTMLButtonElement>('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<HTMLDialogElement>('#import-dialog');
|
||
if (dialog && !dialog.open) {
|
||
dialog.showModal();
|
||
}
|
||
const projectName = document.querySelector<HTMLInputElement>('#project-name');
|
||
if (projectName) projectName.focus();
|
||
return;
|
||
}
|
||
}
|
||
selectPanel(panelId);
|
||
});
|
||
};
|
||
|
||
bindCard('[data-card="excel"]', 'landing-excel-panel', 'picker');
|
||
|
||
|
||
const drawingPopup = createDrawingPopup(landingOverlay, () => {
|
||
landingGuide.dismissHint();
|
||
landingGuide.close(false);
|
||
closePanels();
|
||
});
|
||
document.querySelector<HTMLButtonElement>('[data-card="drawing"] button')!.addEventListener('click', () => drawingPopup.open());
|
||
|
||
document.querySelectorAll<HTMLElement>('[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<HTMLButtonElement>('[data-pick-file]').forEach((button) => {
|
||
const kind = button.getAttribute('data-pick-file');
|
||
if (!kind) return;
|
||
|
||
const targetMap: Record<string, string> = {
|
||
excel: 'landing-excel-input',
|
||
};
|
||
|
||
const targetId = targetMap[kind];
|
||
if (!targetId) return;
|
||
|
||
const input = document.querySelector<HTMLInputElement>(`#${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 libraryPopup=landingOverlay.querySelector<HTMLDialogElement>('.sample-library-popup')!;
|
||
landingOverlay.querySelector<HTMLButtonElement>('[data-card="sample"] button')!.addEventListener('click',()=>{landingGuide.close(false);landingGuide.dismissHint();if(!libraryPopup.open)libraryPopup.showModal();});
|
||
libraryPopup.querySelector('.sample-library-close')!.addEventListener('click',()=>libraryPopup.close());
|
||
libraryPopup.addEventListener('close',()=>{requestAnimationFrame(()=>{landingOverlay.querySelectorAll<HTMLElement>('.landing-shell,.landing-hero').forEach(element=>element.scrollTo(0,0));landingOverlay.querySelector<HTMLButtonElement>('[data-card=sample] button')?.focus({preventScroll:true});});});
|
||
createSampleExplorer(landingOverlay,(sample:Sample)=>loadSampleIntoApp(sample,showApp,showLanding));
|
||
|
||
if (homeButton) {
|
||
homeButton.addEventListener('click', () => {
|
||
showLanding();
|
||
});
|
||
}
|
||
|
||
if (appRoot) {
|
||
appRoot.hidden = true;
|
||
appRoot.setAttribute('aria-hidden', 'true');
|
||
}
|
||
if (homeButton) {
|
||
homeButton.hidden = true;
|
||
}
|
||
|
||
showLanding();
|
||
|