// ─── Generic Product Detail Page (EDR · MDR) ───────────────────────────────── // 데이터 기반 렌더러. SpamOut/CheckOut/Arch5와 동일 디자인 체계. // PRODUCTS[key] = { accent, accent2, badge, product, tagline, hero..., sections... } const ProductPage = ({ pkey, lang, setPage, openQuote }) => { // 데이터 파생 — 훅보다 먼저 (훅 아님 → 안전) const D = (window.PRODUCTS && window.PRODUCTS[pkey]) ? window.PRODUCTS[pkey] : null; const t = D ? (D[lang] || D.en) : null; const ACC = D ? D.accent : '#2563EB'; const ACC2= D ? D.accent2 : '#60A5FA'; const label = D ? D.product : ''; const hasDash = !!(t && t.dash); const [dlOpen, setDlOpen] = React.useState(false); const [dlSent, setDlSent] = React.useState(false); const [dlSending, setDlSending] = React.useState(false); const [dlErr, setDlErr] = React.useState(''); const [dlForm, setDlForm] = React.useState({ company:'', name:'', email:'', phone:'', users:'', message:'' }); const [dlErrs, setDlErrs] = React.useState({}); const dlUp = (k,v) => { setDlForm(p=>({...p,[k]:v})); if (dlErrs[k]) setDlErrs(e=>{const n={...e};delete n[k];return n;}); }; const dlRules = [{ k:'company', type:'text' }, { k:'name', type:'text' }, { k:'email', type:'email' }, { k:'_agree', type:'agree' }]; const [openFaqs, setOpenFaqs] = React.useState([0]); const toggleFaq = (i) => setOpenFaqs(prev => prev.includes(i) ? prev.filter(x=>x!==i) : [...prev, i]); const isKo = lang === 'ko'; React.useEffect(() => { const els = document.querySelectorAll('.reveal'); const obs = new IntersectionObserver(entries => { entries.forEach(e => { if (e.isIntersecting) e.target.classList.add('revealed'); }); }, { threshold: 0.1 }); els.forEach(el => obs.observe(el)); return () => obs.disconnect(); }); const dashRef = React.useRef(null); const [dashIn, setDashIn] = React.useState(false); const [hKpi, setHKpi] = React.useState(null); const [hSlice, setHSlice] = React.useState(null); const [hRow, setHRow] = React.useState(null); const [hBar, setHBar] = React.useState(null); React.useEffect(() => { setDashIn(false); if (!hasDash) return; if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { setDashIn(true); return; } let ob = null, raf = 0; const fire = () => { setDashIn(true); cleanup(); }; const check = () => { const el = dashRef.current; if (!el) return false; const r = el.getBoundingClientRect(); if (r.top < window.innerHeight * 0.85 && r.bottom > 0) { fire(); return true; } return false; }; const cleanup = () => { if (ob) { ob.disconnect(); ob = null; } window.removeEventListener('scroll', onScroll); if (raf) cancelAnimationFrame(raf); }; const onScroll = () => { if (raf) return; raf = requestAnimationFrame(() => { raf = 0; check(); }); }; // ref 부착을 기다린 뒤 옵저버 연결 (센티넬 없이 threshold 0 + rootMargin 사용) const attach = () => { const el = dashRef.current; if (!el) { raf = requestAnimationFrame(attach); return; } if (check()) return; ob = new IntersectionObserver(es => { if (es.some(e => e.isIntersecting)) fire(); }, { threshold: 0, rootMargin: '0px 0px -15% 0px' }); ob.observe(el); window.addEventListener('scroll', onScroll, { passive: true }); }; attach(); return cleanup; }, [pkey, hasDash]); const NAVY='#0F1E3C', NAVY2='#13294F', CYAN='#06B6D4'; if (!D || !t) return null; const handleDownload = async () => { const _v = window.validateForm(dlForm, dlRules, lang); if (!_v.ok) { setDlErrs(_v.errors); return; } setDlSending(true); const msg = `🗂️ ${label} 자료 요청 / 리드\n회사: ${dlForm.company||'-'}\n담당자: ${dlForm.name||'-'}\n이메일: ${dlForm.email||'-'}\n연락처: ${dlForm.phone||'-'}\n예상 규모: ${dlForm.users||'-'}\n문의: ${dlForm.message||'-'}\n시간: ${new Date().toLocaleString('ko-KR')}`; const _r = await sendTelegram(msg, { consent: dlForm._agree === true }); if (!_r || !_r.ok) { setDlSending(false); setDlErr(isKo ? '요청 전달에 실패했습니다. sales@3es.co.kr 또는 031-212-3390 으로 연락해 주세요.' : 'Submission failed. Please email sales@3es.co.kr or call +82-31-212-3390.'); return; } try { if (typeof LS !== 'undefined') { const list = LS.get('3es_inquiries', []); const nextId = (list.reduce((m,x)=>Math.max(m, x.id||0), 0) || 0) + 1; list.unshift({ id:nextId, company:dlForm.company||'-', name:dlForm.name||'-', date:new Date().toLocaleDateString('ko-KR').replace(/\. /g,'.').replace(/\.$/,''), type:label+' 자료요청', status:'new', message:`이메일:${dlForm.email} / 연락처:${dlForm.phone} / 규모:${dlForm.users} / ${dlForm.message}` }); LS.set('3es_inquiries', list); } } catch(e) {} setDlSending(false); setDlSent(true); }; const Eyebrow = ({ children, light }) => (
{children}
); // 숫자 카운트업 const useCU = (text, run, dur = 1100) => { const [out, setOut] = React.useState(() => String(text).replace(/[\d]/g, '0')); React.useEffect(() => { if (!run) return; const src = String(text); const m = src.match(/[\d][\d,.]*/); if (!m) { setOut(src); return; } const raw = m[0], target = parseFloat(raw.replace(/,/g,'')); if (!isFinite(target)) { setOut(src); return; } const dec = (raw.split('.')[1]||'').length, comma = raw.includes(','); const t0 = performance.now(); let id = 0; const tick = (now) => { const pr = Math.min(1,(now-t0)/dur), e = 1-Math.pow(1-pr,3), v = target*e; let str = dec ? v.toFixed(dec) : String(Math.round(v)); if (comma) { const [i,d] = str.split('.'); str = Number(i).toLocaleString()+(d?'.'+d:''); } setOut(src.replace(raw, str)); if (pr < 1) id = requestAnimationFrame(tick); }; id = requestAnimationFrame(tick); return () => cancelAnimationFrame(id); }, [text, run, dur]); return out; }; const CV = ({ value, run, dur, style }) => {useCU(value, run, dur)}; const SectionHead = ({ k, title, sub, light, center }) => (
{k}

{title}

{sub &&

{sub}

}
); const ArrowR = ({size=16}) => (); const Check = ({c=ACC}) => (); return (
{/* HERO */}
setPage('solutions')} style={{ cursor:'pointer', display:'flex', width:'fit-content', alignItems:'center', gap:8, color:'rgba(255,255,255,0.5)', fontSize:13, marginBottom:36 }}> {isKo?'솔루션으로':'Back to Solutions'}
{t.badge}
{D.product} {t.tagline}

{t.h1a} {t.h1b}

{t.sub}

{t.chips.map((c,i)=>({c}))}
{/* 01 PROBLEM */}
{t.problems.map((p,i)=>(
{e.currentTarget.style.borderColor='#FCA5A5';e.currentTarget.style.boxShadow='0 12px 32px rgba(239,68,68,0.08)';e.currentTarget.style.transform='translateY(-3px)';}} onMouseLeave={e=>{e.currentTarget.style.borderColor='#E6EAF2';e.currentTarget.style.boxShadow='none';e.currentTarget.style.transform='none';}}>
{p.tag}
{p.title}
{p.d}
))}
{/* 02 SOLUTION */}
{t.sol.points.map((p,i)=>(
{p}
))}
{/* 03 CONSOLE DASHBOARD (t.dash 있을 때만) */} {t.dash && (
{/* 콘솔 상단 바 */}
{['#F87171','#F59E0B','#34D399'].map((c,i)=>)}
{label} · Management Console LIVE
{isKo?'※ 아래 화면은 기능 이해를 돕기 위한 시뮬레이션이며, 실제 제품 UI 및 수치와 다를 수 있습니다.' :'* The panel below is a functional simulation and may differ from the actual product UI and figures.'}
{/* LEFT */}
{t.dash.stats.map((x,i)=>{ const on = hKpi===i; return (
setHKpi(i)} onMouseLeave={()=>setHKpi(null)} style={{ background: on?'rgba(255,255,255,.055)':'rgba(255,255,255,.028)', border:`1px solid ${on?ACC+'55':'rgba(255,255,255,.06)'}`, borderRadius:14, padding:'18px 16px', minWidth:0, cursor:'default', transform: on?'translateY(-3px)':'none', boxShadow: on?`0 12px 28px ${ACC}14`:'none', transition:'all .22s cubic-bezier(.2,.8,.3,1)' }}>
{x.k}
{x.s}
);})}
{/* MODULES */}
{t.dash.mod.k}
{t.dash.mod.title}
{t.dash.mod.rows.map((r,i)=>{ const on = hRow===i; return (
setHRow(i)} onMouseLeave={()=>setHRow(null)} style={{ display:'grid', gridTemplateColumns:'minmax(88px,auto) 1fr minmax(56px,auto)', gap:12, alignItems:'center', cursor:'default', opacity: hRow!==null && !on ? .45 : 1, transition:'opacity .22s' }}>
{r.n}
{r.s}
);})}
{/* RIGHT */}
{/* THREAT BREAKDOWN */}
{t.dash.cls.k}
{t.dash.cls.title}
{t.dash.cls.items.map((c,i)=>{ const on = hSlice===i; return (
setHSlice(i)} onMouseLeave={()=>setHSlice(null)} style={{ display:'flex', alignItems:'center', gap:9, padding:'8px 11px', background: on?c.c+'1A':'rgba(255,255,255,.03)', border:`1px solid ${on?c.c+'55':'rgba(255,255,255,.05)'}`, borderRadius:9, cursor:'default', transform: on?'translateX(3px)':'none', opacity: hSlice!==null && !on ? .5 : 1, transition:'all .2s cubic-bezier(.2,.8,.3,1)' }}> {c.t}
);})}
{(()=>{ let off=0; const R=15.9155; const n=t.dash.cls.items.length; const vals=t.dash.cls.items.map(c=>{ const raw=(typeof c.pct==='number')?c.pct:parseFloat(c.v); return isFinite(raw)?raw:100/n; }); return t.dash.cls.items.map((c,i)=>{ const v=vals[i], on=hSlice===i; const el=(setHSlice(i)} onMouseLeave={()=>setHSlice(null)} style={{ cursor:'default', transition:`stroke-dasharray .9s cubic-bezier(.2,.8,.3,1) ${.2+i*.1}s, stroke-width .2s, opacity .2s` }}/>); off+=v; return el; }); })()}
{hSlice!==null ? t.dash.cls.items[hSlice].t : t.dash.cls.centerLabel}
{/* TREND + COMPLIANCE */}
{t.dash.trend.k}
{t.dash.trend.title}
peak {t.dash.trend.peak}
{[[ACC2,t.dash.trend.legend[0]],['#F87171',t.dash.trend.legend[1]]].map(([c,l],i)=>( {l} ))}
{t.dash.trend.hours.map((h,i)=>{ const on = hBar===i; return (
setHBar(i)} onMouseLeave={()=>setHBar(null)} style={{ flex:1, display:'flex', flexDirection:'column', alignItems:'center', gap:7, minWidth:0, position:'relative', cursor:'default', opacity: hBar!==null && !on ? .42 : 1, transition:'opacity .2s' }}> {on && (
{h}:00
{t.dash.trend.legend[0]} {t.dash.trend.recv[i]}% {t.dash.trend.legend[1]} {t.dash.trend.block[i]}%
)}
{h}
);})}
{t.dash.compliance && (
{t.dash.compliance.k}
{(()=>{ const total = t.dash.compliance.items.reduce((a,b)=>a+b.v,0); return ( <>
{t.dash.compliance.items.map((c,i)=>(
))}
{t.dash.compliance.items.map((c,i)=>(
{c.t}
))}
);})()}
)}
{t.dash.note}
)} {/* 03 HOW IT WORKS */} {t.flow && (
{t.flow.steps.map((s,i)=>(
{s.n}
{s.t}
{s.d}
))}
)} {/* 04 FEATURES */}
{t.features.map((f,i)=>(
{e.currentTarget.style.borderColor=ACC;e.currentTarget.style.boxShadow=`0 14px 36px ${ACC}12`;e.currentTarget.style.transform='translateY(-4px)';}} onMouseLeave={e=>{e.currentTarget.style.borderColor='#E6EAF2';e.currentTarget.style.boxShadow='none';e.currentTarget.style.transform='none';}}>
{f.t}
{f.d}
))}
{/* 05 COMPARISON */}
{t.compHead.map((h,i)=>())} {t.compRows.map((r,i)=>( {r.map((cell,ci)=>())} ))}
{h}
{cell}
{/* 06 BENEFITS */}
{t.benefits.map((b,i)=>(
{b.n}
{b.t}
{b.d}
))}
{/* 07 USE CASES */}
{t.useCases.map((u,i)=>(
{u.ind}
{u.d}
))}
{/* ARCHITECTURE (t.arch 있을 때만) */} {t.arch && (
{t.arch.groups.map((g,gi)=>(
{g.t}
{g.s}
{g.items.map((it,ii)=>(
{it.n}
{it.d}
))}
))}
{t.arch.note && (
🔗 {t.arch.note}
)}
)} {/* AI ANALYST (t.ai 있을 때만) */} {t.ai && (
{t.ai.blocks.map((b,bi)=>(
{['🛰️','💬','🤖'][bi] || '✨'}
{b.t}
{b.d}
{b.items && b.items.length>0 && (
{b.items.map((x,xi)=>(
✓{x}
))}
)}
))}
)} {window.SITE_CONFIG && window.SITE_CONFIG.SHOW_PRICE_GUIDE && (
{t.price.plans.map((p,i)=>(
{p.primary &&
{isKo?'추천':'Popular'}
}
{p.name}
{isKo?'상담 후 산정':'Quote on consult'}
BEST · {p.best}
{p.features.map((f,fi)=>(
{f}
))}
))}
💡 {t.price.note}
)} {/* 09 SUPPLY */}
{t.maker.points.map((p,i)=>(
{p.t}
{p.d}
))}
{/* 10 DOWNLOAD */}
{t.dl.summary.map((s,i)=>(
0{i+1}
{s}
))}
{t.dl.files.map((file,i)=>( ))}
{/* 도입 시나리오 (해당 제품) */} {/* 조달 등재 정보 */} {window.hasG2B && window.hasG2B(pkey) && (
)} {/* 11 FAQ */}
{t.faqs.map((f,i)=>(
toggleFaq(i)} style={{ padding:'20px 24px', display:'flex', justifyContent:'space-between', alignItems:'center', cursor:'pointer', gap:18 }}>
Q{i+1} {f.q}
+
{openFaqs.includes(i) &&
{f.a}
}
))}
{/* 12 FINAL CTA */}
3ES SOFT · {label.toUpperCase()}

{t.bottom.title}

{t.bottom.sub}

{/* DOWNLOAD / LEAD MODAL */} {dlOpen && (
{isKo?'제안서 요청하고 상담 연결하기':'Request Proposal & Connect'}
{isKo?'정보를 남겨주시면 자료와 함께 담당자가 빠르게 연락드립니다.':'Leave your details — we\'ll send materials and follow up quickly.'}
{dlSent ? (
{isKo?'요청이 접수되었습니다':'Request Received'}
{isKo?'담당자가 빠르게 연락드리겠습니다.\n관리자 알림이 발송되었습니다.':'Our team will contact you shortly.\nAn admin notification has been sent.'}
) : ( <>
dlUp('company',e.target.value)} style={{ width:'100%', padding:'11px 13px', background:'#F6F8FC', border:'1px solid #E6EAF2', borderRadius:8, fontSize:14, fontFamily:'inherit', outline:'none', color:'#1E293B' }}/>{dlErrs.company && }
dlUp('name',e.target.value)} style={{ width:'100%', padding:'11px 13px', background:'#F6F8FC', border:'1px solid #E6EAF2', borderRadius:8, fontSize:14, fontFamily:'inherit', outline:'none', color:'#1E293B' }}/>{dlErrs.name && }
dlUp('email',e.target.value)} placeholder="name@company.com" style={{ width:'100%', padding:'11px 13px', background:'#F6F8FC', border:'1px solid #E6EAF2', borderRadius:8, fontSize:14, fontFamily:'inherit', outline:'none', color:'#1E293B' }}/>{dlErrs.email && }
dlUp('phone',e.target.value)} placeholder={isKo?'010-0000-0000':'Phone'} style={{ width:'100%', padding:'11px 13px', background:'#F6F8FC', border:'1px solid #E6EAF2', borderRadius:8, fontSize:14, fontFamily:'inherit', outline:'none', color:'#1E293B' }}/>
dlUp('users',e.target.value)} placeholder={isKo?'예: 200':'e.g. 200'} style={{ width:'100%', padding:'11px 13px', background:'#F6F8FC', border:'1px solid #E6EAF2', borderRadius:8, fontSize:14, fontFamily:'inherit', outline:'none', color:'#1E293B' }}/>
setDlForm({...dlForm,_agree:v})} onOpenPolicy={()=>{ setDlOpen(false); setTimeout(()=>window.__goPrivacy&&window.__goPrivacy(),0); }} purpose={isKo?'제안서·자료 제공 및 도입 상담 회신':undefined}/> {dlErr && (
{dlErr}
)}
{isKo?'📱 제출 즉시 담당자에게 알림이 발송됩니다':'📱 Our team is notified instantly on submit'}
)}
)}
); }; Object.assign(window, { ProductPage });