// ─── App Root ──────────────────────────────────────────────────────────────── const App = () => { const [lang, setLang] = React.useState('ko'); const [page, setPage] = React.useState('home'); const [quoteOpen, setQuoteOpen] = React.useState(false); const [quoteSol, setQuoteSol] = React.useState(null); const [demoOpen, setDemoOpen] = React.useState(false); const [demoSol, setDemoSol] = React.useState(null); const t = T[lang]; const openQuote = (solTitle) => { setQuoteSol(typeof solTitle === 'string' ? solTitle : null); setQuoteOpen(true); }; const openDemo = (sol) => { setDemoSol((sol && sol.title) ? sol : t.solutions.items[0]); setDemoOpen(true); }; // 현재 보고 있는 솔루션 — 플로팅 CTA가 해당 솔루션 전용 견적 폼을 열도록 const curSol = React.useMemo(() => { if (!page || page.indexOf('sol_') !== 0) return null; const key = page.slice(4); return (t.solutions.items || []).find(i => i.key === key) || { key, title: key }; }, [page, t]); // Lock scroll + hide floating UI while any modal is open // 매 렌더마다 실제 상태로 재조정 (언마운트 타이밍에 클래스가 남는 문제 방지) React.useEffect(()=>{ const on = quoteOpen || demoOpen; document.body.classList.toggle('modal-open', on); if (on) document.body.style.overflow = 'hidden'; else if (!document.body.classList.contains('nav-open')) document.body.style.overflow = ''; }); React.useEffect(()=>()=>{ document.body.classList.remove('modal-open'); document.body.style.overflow=''; },[]); // SEO: title/meta/JSON-LD + URL 동기화 useSEO(page, lang); // ESC closes modals React.useEffect(()=>{ window.__goPrivacy = () => setPage('privacy'); window.__goNews = () => setPage('news'); window.__goAdmin = () => setPage('admin'); // #admin 해시로 직접 진입 (로그인 게이트가 접근을 통제) const applyHash = () => { if (location.hash === '#admin') setPage('admin'); }; applyHash(); // 검색엔진이 색인한 실제 URL(/solutions//)로 직접 들어온 경우 해당 화면을 연다 const initial = window.pageForPath(location.pathname); if (initial) setPage(initial); const onPop = () => { const p = window.pageForPath(location.pathname); if (p) setPage(p); }; const onScroll = () => { const el = document.querySelector('.nav-wrap'); if (el) el.classList.toggle('scrolled', window.scrollY > 12); }; onScroll(); window.addEventListener('scroll', onScroll, { passive: true }); window.addEventListener('popstate', onPop); window.addEventListener('hashchange', applyHash); const onKey = e => { if (e.key==='Escape') { setQuoteOpen(false); setDemoOpen(false); } }; window.addEventListener('keydown', onKey); return ()=>{ window.removeEventListener('keydown', onKey); window.removeEventListener('hashchange', applyHash); window.removeEventListener('popstate', onPop); window.removeEventListener('scroll', onScroll); }; },[]); // Inject global CSS once React.useEffect(() => { const el = document.createElement('style'); el.id = 'global-styles-3es'; el.textContent = globalStyles + interactionStyles + topoStyles + topoModeStyles + landingStyles + casesStyles; if (!document.getElementById('global-styles-3es')) document.head.appendChild(el); initInteractions(); return () => { const e = document.getElementById('global-styles-3es'); if(e) e.remove(); }; }, []); // Scroll to top on page change React.useEffect(() => { if (page !== 'admin') window.scrollTo({ top: 0, behavior: 'smooth' }); }, [page]); // 페이지 전환마다 reveal 옵저버 재실행 (전용 페이지에서 opacity:0 으로 남는 문제 방지) React.useEffect(() => { let obs; const run = () => { const els = document.querySelectorAll('.reveal:not(.revealed)'); if (!els.length) return; obs = new IntersectionObserver(entries => { entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('revealed'); obs.unobserve(e.target); } }); }, { threshold: 0.05 }); els.forEach(el => obs.observe(el)); // 안전망: 뷰포트 안이지만 관찰이 늦는 경우 강제 표시 setTimeout(() => { document.querySelectorAll('.reveal:not(.revealed)').forEach(el => { const r = el.getBoundingClientRect(); if (r.top < window.innerHeight && r.bottom > 0) el.classList.add('revealed'); }); }, 600); }; const t = setTimeout(run, 60); return () => { clearTimeout(t); if (obs) obs.disconnect(); }; }, [page, lang]); const solKey = page.startsWith('sol_') ? page.replace('sol_','') : null; const showChrome = page !== 'admin'; // Wrapper for pages that need a top spacer const PageWrap = ({ children, heroBg }) => (
{!heroBg && (
{heroBg}
)} {children}
); return ( <> {showChrome &&