Files
Pier_v1/랜딩페이지/landing-page.ts
T
ypshimandClaude Haiku 5.5 fc520ec03d feat: 프로젝트 공통 지반모델·교대 지반 통합·재료표/수량 진입·옥성교 랜딩 히어로 (11-06~11-09 작업분)
공용 파일(main.ts·index.html·style.css)에서 서로 얽혀 있어 한 커밋으로 묶음.
- 지반: ① 형상 프로젝트 지반모델 관리창(생성·JSON 불러오기·교각별 비교·선택 적용),
  프로젝트 JSON 내장 groundModel 추출, 교대 8행 통합 적용·공통지형 중복격자 제외
- 재료표·수량: 상단 ⑤ 메뉴·배근 바로가기, 결과 교각/탭 기억, 수량 워크북·정합 보정
- 랜딩: 옥성교 6교각 히어로 이미지·투영 강조점·좌→우 광선·지형 라운드/도로 연결
- 검증 스크립트(verify-project-ground/ground-abutments/okseong-user-files/quantity-*/landing-hero 등)
정본: DOC/04_실행/실행_*_2026-11-09_v0.1.00.md

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-11-09 18:01:02 +09:00

294 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { 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>&amp;</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>
</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>기설계교량 라이브러리</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 &amp; 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="기설계교량 라이브러리"><button type="button" class="sample-library-close" aria-label="설계 라이브러리 닫기">×</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);
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);
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();