// ─── 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} ))}
setPage('contact')} style={{ padding:'15px 26px', background:`linear-gradient(135deg,${ACC},${D.accentDark})`, color:'#fff', fontSize:15, fontWeight:600, borderRadius:10, border:'none', cursor:'pointer', fontFamily:'inherit', display:'inline-flex', alignItems:'center', gap:8, boxShadow:`0 12px 32px ${ACC}40` }}>{isKo?'도입 상담':'Talk to Sales'}
openQuote(label)} style={{ padding:'15px 22px', background:'rgba(255,255,255,0.06)', color:'#fff', fontSize:15, fontWeight:600, borderRadius:10, border:'1px solid rgba(255,255,255,0.15)', cursor:'pointer', fontFamily:'inherit' }}>{isKo?'견적 문의':'Get Quote'}
setDlOpen(true)} style={{ padding:'15px 22px', background:'transparent', color:ACC2, fontSize:15, fontWeight:600, borderRadius:10, border:`1px solid ${ACC}40`, cursor:'pointer', fontFamily:'inherit' }}>{isKo?'자료 요청':'Download'}
{/* 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.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' }}>
);})}
{/* 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)=>(
))}
)}
{/* 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)=>({h} ))}
{t.compRows.map((r,i)=>(
{r.map((cell,ci)=>({cell} ))}
))}
{/* 06 BENEFITS */}
{t.benefits.map((b,i)=>(
))}
{/* 07 USE CASES */}
{t.useCases.map((u,i)=>(
))}
{/* ARCHITECTURE (t.arch 있을 때만) */}
{t.arch && (
{t.arch.groups.map((g,gi)=>(
{g.t}
{g.s}
{g.items.map((it,ii)=>(
))}
))}
{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}
))}
openQuote(label+' '+p.name)} style={{ width:'100%', padding:'13px', background: p.primary?`linear-gradient(135deg,${ACC},${D.accentDark})`:NAVY, color:'#fff', border:'none', borderRadius:10, fontSize:14, fontWeight:600, cursor:'pointer', fontFamily:'inherit' }}>{isKo?'견적 문의':'Get Quote'}
))}
💡
{t.price.note}
)}
{/* 09 SUPPLY */}
{t.maker.points.map((p,i)=>(
))}
{/* 10 DOWNLOAD */}
{t.dl.summary.map((s,i)=>(
))}
{t.dl.files.map((file,i)=>(
setDlOpen(true)} style={{ padding:22, background:'#fff', border:'1px solid #E6EAF2', borderRadius:14, cursor:'pointer', fontFamily:'inherit', display:'flex', gap:14, alignItems:'center', transition:'all 0.2s' }}
onMouseEnter={e=>{e.currentTarget.style.borderColor=ACC;e.currentTarget.style.boxShadow=`0 8px 24px ${ACC}12`;}}
onMouseLeave={e=>{e.currentTarget.style.borderColor='#E6EAF2';e.currentTarget.style.boxShadow='none';}}>
{file[0]}
{file[1]}
{isKo?'회사 이메일 입력 시 담당자가 자료를 보내드립니다':'We will email the materials to you'}
))}
{/* 도입 시나리오 (해당 제품) */}
{/* 조달 등재 정보 */}
{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}
setPage('contact')} style={{ padding:'15px 28px', background:`linear-gradient(135deg,${ACC},${D.accentDark})`, color:'#fff', fontSize:15, fontWeight:600, borderRadius:10, border:'none', cursor:'pointer', fontFamily:'inherit', display:'inline-flex', alignItems:'center', gap:8, boxShadow:`0 12px 32px ${ACC}45` }}>{isKo?'도입 상담 받기':'Talk to Sales'}
openQuote(label)} style={{ padding:'15px 24px', background:`${ACC}1f`, color:ACC2, border:`1px solid ${ACC}40`, fontSize:15, fontWeight:600, borderRadius:10, cursor:'pointer', fontFamily:'inherit' }}>{isKo?'견적 요청하기':'Get Quote'}
setDlOpen(true)} style={{ padding:'15px 24px', background:'rgba(255,255,255,0.06)', color:'#fff', border:'1px solid rgba(255,255,255,0.2)', fontSize:15, fontWeight:600, borderRadius:10, cursor:'pointer', fontFamily:'inherit' }}>{isKo?'자료 요청':'Download'}
{/* 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.'}
{setDlOpen(false);setDlSent(false);setDlForm({ company:'', name:'', email:'', phone:'', users:'', message:'' });}} style={{ marginTop:20, padding:'10px 24px', background:ACC, color:'#fff', border:'none', borderRadius:8, fontWeight:600, cursor:'pointer', fontFamily:'inherit' }}>{isKo?'닫기':'Close'}
) : (
<>
{isKo?'회사명':'Company'}
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 && }
{isKo?'담당자명':'Name'}
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 && }
{isKo?'회사 이메일':'Work Email'}
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 && }
{isKo?'연락처':'Phone'}
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' }}/>
{isKo?'예상 규모':'Scale'}
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}
)}
{dlSending?(isKo?'처리 중...':'Processing...'):(isKo?'제안서 요청하고 상담 연결':'Request Proposal & Connect')}
{isKo?'📱 제출 즉시 담당자에게 알림이 발송됩니다':'📱 Our team is notified instantly on submit'}
setDlOpen(false)} style={{ width:'100%', padding:'8px', background:'transparent', color:'#94A3B8', border:'none', fontSize:13, cursor:'pointer', fontFamily:'inherit' }}>{isKo?'취소':'Cancel'}
>
)}
)}
);
};
Object.assign(window, { ProductPage });