/* EntecPower corporate site — Home page content */ /* EntecPower corporate site — Home page content */ /* EntecPower corporate site — Home page content */ const getImageCoverCoords = (xPercent, yPercent, containerW, containerH, imgW = 1024, imgH = 1024) => { const imgRatio = imgW / imgH; const containerRatio = containerW / containerH; let scaledW, scaledH, offsetX, offsetY; if (containerRatio > imgRatio) { scaledW = containerW; scaledH = containerW / imgRatio; offsetX = 0; offsetY = (containerH - scaledH) / 2; } else { scaledW = containerH * imgRatio; scaledH = containerH; offsetX = (containerW - scaledW) / 2; offsetY = 0; } return { x: offsetX + (xPercent / 100) * scaledW, y: offsetY + (yPercent / 100) * scaledH }; }; const SPOT_LOCK_PX = 90; const SCENES = [ { id: 'substation', title: { ko: '변전소 주요 설비 안전 진단', en: 'Substation Asset Diagnostics' }, sub: { ko: '초고압 애자련 누전 분석 및 대형 변압기 부싱 실시간 열화 진단', en: 'UHV insulator leak analysis & transformer bushing thermal degradation' }, bg: 'company/assets/imagery/substation_day.png', stats: [ { v: '98.7', u: '%', l: { ko: '진단 정밀도', en: 'PD Detection' } }, { v: '50', u: 'm', l: { ko: '최대 검출 거리', en: 'Max Standoff' } } ], hotspots: [ { x: 28, y: 38, type: 'thermal', label: { ko: '변압기 R상 부싱 접속부 과열 (CRITICAL)', en: 'Transformer R-Phase Bushing Overheat (CRITICAL)' }, telemetry: 'TR_BUSH_R', temp: 84.6, db: 11, dist: 12.4, desc: { ko: '부싱 전극 결선 풀림에 따른 접촉저항 및 전류 집중 과열.', en: 'Overheating due to loose connector contact resistance & current load concentration.' } }, { x: 72, y: 28, type: 'thermal', label: { ko: '변압기 S상 부싱 이상 발열 (WARNING)', en: 'Transformer S-Phase Bushing Overheat (WARNING)' }, telemetry: 'TR_BUSH_S', temp: 59.3, db: 7, dist: 12.8, desc: { ko: '부싱 절연 노화에 따른 초기 미세 유도 열화.', en: 'Early stage induction heating due to bushing insulation degradation.' } }, { x: 18, y: 72, type: 'thermal', label: { ko: '변압기 냉각 방열핀 상단 폐쇄 과열 (WARNING)', en: 'Transformer Radiator Fin Overheat (WARNING)' }, telemetry: 'TR_RAD_FIN_04', temp: 68.5, db: 8, dist: 18.2, desc: { ko: '방열판 내부 냉각 오일 순환 마찰 및 배관 오염 누적에 따른 방열 방해.', en: 'Poor heat dissipation due to cooling oil flow blockage in radiator fin.' } }, { x: 42, y: 55, type: 'prpd', label: { ko: '154kV 현수애자련 코로나 방전 (WARNING)', en: '154kV Suspension Insulator PD (WARNING)' }, telemetry: 'INS_CHAIN_08', temp: 24.5, db: 46, dist: 15.6, desc: { ko: '애자 표면 염해 노출에 따른 누설 전류 아크 코로나 방전.', en: 'Surface leakage arc corona discharge due to salt & moisture build-up.' } }, { x: 62, y: 68, type: 'prpd', label: { ko: '피뢰기(SA) 열화에 의한 내부 방전', en: 'Surge Arrester Internal PD' }, telemetry: 'SURGE_ARR_B3', temp: 25.1, db: 41, dist: 28.3, desc: { ko: '피뢰기 내부 소자 노화로 인한 연속 부분방전 신호 및 누전 감지.', en: 'Continuous partial discharge inside SA housing due to element aging.' } }, { x: 48, y: 44, type: 'mixed', label: { ko: '변압기 2차측 인입선 접촉 아크 및 과열 (DANGER)', en: 'Transformer Secondary Busway Arcing & Overheat (DANGER)' }, telemetry: 'TR_SEC_BUSBAR', temp: 42.8, db: 48, dist: 19.5, desc: { ko: '초기 부분 방전이 지속되며 아크에 의한 금속 접촉부 온도 상승 단계 (융복합 진단 핵심 지점).', en: 'Early stage partial discharge transforming to heat due to constant arcing.' } } ] }, { id: 'gas_plant', title: { ko: '석유화학 플랜트 배관 연결부', en: 'Petrochemical Pipeline Joints' }, sub: { ko: '송유관 고압 밸브 가스 누출 및 플랜지 용접부 균열 동시 감시', en: 'High-pressure valve leaks & flange weld crack inspection' }, bg: 'company/assets/imagery/gas_plant_day.png', stats: [ { v: '0.1', u: 'l/min', l: { ko: '최소 가스 누출', en: 'Micro Leak Limit' } }, { v: '45', u: 'm', l: { ko: '안전 측정 이격', en: 'Safe Distance' } } ], hotspots: [ { x: 22, y: 48, type: 'thermal', label: { ko: '고압 증기 밸브 패킹 마찰 고열 (CRITICAL)', en: 'High-Pressure Steam Valve Overheat (CRITICAL)' }, telemetry: 'STEAM_VALVE_02', temp: 92.1, db: 8, dist: 16.5, desc: { ko: '고압 스팀 유체 개폐 마찰 및 차단막 밀봉 성능 노후화로 인한 열화.', en: 'Friction overheat at steam block valve due to sealant degradation.' } }, { x: 68, y: 30, type: 'thermal', label: { ko: '이송 유체 급유 배관 마찰 온도 상승', en: 'Fuel pipe friction heat' }, telemetry: 'FEED_PIPE_V12', temp: 72.4, db: 11, dist: 22.8, desc: { ko: '이송 압력 급상승으로 인한 배관 곡관부 기계적 마찰 온도 누적.', en: 'Localized pipe friction heating from sudden product flow turbulence.' } }, { x: 15, y: 25, type: 'thermal', label: { ko: '오일 저장 탱크 히팅 히터 오동작 고열', en: 'Oil Tank Heater Anomaly' }, telemetry: 'OIL_TANK_H3', temp: 80.4, db: 7, dist: 34.0, desc: { ko: '탱크 하단 전기 히터 접촉 불량 및 온도 센서 고장으로 인한 지속 가열.', en: 'Uncontrolled continuous heating due to level switch failure.' } }, { x: 38, y: 66, type: 'prpd', label: { ko: '가스 배관 연결용 플랜지 미세 가스 누출 (WARNING)', en: 'Gas Pipe Flange Micro-Leak (WARNING)' }, telemetry: 'FLANGE_GAS_L10', temp: 18.2, db: 52, dist: 11.2, desc: { ko: '가스 고압 분출에 따른 고주파 초음파 충격음 발생. PRPD 신호 뚜렷함.', en: 'High-frequency ultrasonic impact acoustic signal from high-pressure gas escape.' } }, { x: 58, y: 80, type: 'prpd', label: { ko: '가스 저장 기화기 안전 릴리프 밸브 바이패스', en: 'Vaporizer Relief Valve Bypass Leak' }, telemetry: 'SAFETY_VALVE_B4', temp: 21.4, db: 44, dist: 29.4, desc: { ko: '안전밸브 노화에 따른 압력 미세 방출 가스 누출 신호 포착.', en: 'Ultrasonic leak detection showing vapor leakage from relief valve seat.' } }, { x: 42, y: 40, type: 'mixed', label: { ko: '고압 배관 엘보 용접 크랙 가스 분출 및 가열 (DANGER)', en: 'High-Pressure Pipe Elbow Crack (DANGER)' }, telemetry: 'PIPE_ELBOW_CRACK', temp: 38.5, db: 49, dist: 22.3, desc: { ko: '파이프 피로 파괴 미세 균열로 유체가 분출되며 주변 마찰 마모 온도와 초음파 융복합 검출.', en: 'Fluid friction and leak acoustic signature simultaneously detected at elbow weld.' } } ] }, { id: 'datacenter', title: { ko: '실내 대형 데이터센터 전원 및 냉각 설비', en: 'Data Center Power & Cooling Cabinets' }, sub: { ko: '서버 랙 PDU 단자 접촉 이상 과열 및 UPS 배터리실 아크 방전 상시 감시', en: 'PDU contact overheating & UPS room arc discharge monitoring' }, bg: 'company/assets/imagery/datacenter_day.png', stats: [ { v: '24/7', u: '상시', l: { ko: '중단 없는 감시', en: 'Non-stop Scan' } }, { v: '15', u: 'm', l: { ko: '실내 밀착 진단', en: 'Indoor Close-Up' } } ], hotspots: [ { x: 20, y: 62, type: 'thermal', label: { ko: '서버 랙 B열 전원분배 장치(PDU) 접속부 과열 (CRITICAL)', en: 'Server Rack Row B PDU Contact Overheat (CRITICAL)' }, telemetry: 'DC_PDU_ROW_B', temp: 71.5, db: 10, dist: 9.8, desc: { ko: '과부하 전류 인입 및 PDU 단자 체결 저하에 따른 이상 고열.', en: 'High-current draw on loose PDU copper connection terminals.' } }, { x: 55, y: 78, type: 'thermal', label: { ko: '서버 랙 C열 메인 인입선 접촉 과열', en: 'Server Rack Row C Main Contact Heat' }, telemetry: 'DC_PDU_ROW_C', temp: 55.4, db: 5, dist: 10.2, desc: { ko: '배전 보드 커넥터 산화막 형성에 따른 저항 증가 발열.', en: 'Contact degradation and resistance heating on row distribution connector.' } }, { x: 72, y: 22, type: 'thermal', label: { ko: '메인 항온항습 냉각 루버 팬 유닛 과열 (WARNING)', en: 'Main HVAC Cooling Fan Unit Overheat (WARNING)' }, telemetry: 'HVAC_FAN_03', temp: 58.2, db: 12, dist: 14.2, desc: { ko: '팬 베어링 윤활 고갈로 인한 회전 마찰 부하 이상 발열 현상.', en: 'HVAC fan motor friction heat due to bearing grease drying out.' } }, { x: 30, y: 32, type: 'prpd', label: { ko: 'UPS 백업 배터리 모듈실 내부 아크 방전 (WARNING)', en: 'UPS Backup Battery Room Internal Arc (WARNING)' }, telemetry: 'UPS_BATTERY_PD', temp: 26.8, db: 42, dist: 18.5, desc: { ko: 'DC 버스바 절연 노후화로 인한 절연 트래킹 및 미세 아크 방전 발생.', en: 'DC busbar insulation degradation causing arc tracking & discharge.' } }, { x: 58, y: 50, type: 'prpd', label: { ko: '천장 케이블 트레이 광역 전원선 절연 파괴', en: 'Overhead Cable Tray Cable PD' }, telemetry: 'CABLE_TRAY_SEC3', temp: 24.2, db: 38, dist: 11.4, desc: { ko: '천장 전력 배선 트래킹 방전 신호 포착. 눈으로 보이지 않는 케이블 내부 결함.', en: 'Hidden insulation cracking inside overhead power supply cables.' } }, { x: 38, y: 46, type: 'mixed', label: { ko: '메인 전력 버스덕트 조인트 누설 및 발열 (DANGER)', en: 'Main Bus Duct Joint Leakage & Overheat (DANGER)' }, telemetry: 'MAIN_BUS_DUCT', temp: 40.2, db: 47, dist: 16.2, desc: { ko: '전력 배전 버스덕트 이음새의 내부 절연 틈새 미세 방전(초음파)이 열(온도 상승)로 진행 중인 단계.', en: 'Insulation tracking transitioning into contact heat at the bus duct coupling joint.' } } ] }, { id: 'switchgear', title: { ko: '특고압 폐쇄형 배전반 내부', en: 'HV Closed Switchgear Panel' }, sub: { ko: '차단 장치 내부 접속 단자 볼트 풀림 및 부분 방전 상시 감시', en: 'Cradle contact loose bolts & vacuum circuit breaker partial discharge' }, bg: 'company/assets/imagery/switchgear_day.png', stats: [ { v: '19', u: '개국', l: { ko: '글로벌 레퍼런스', en: 'Global Reference' } }, { v: '98.7', u: '%', l: { ko: '진단 정밀도', en: 'Diagnostic Accuracy' } } ], hotspots: [ { x: 25, y: 35, type: 'thermal', label: { ko: 'R상 동부스바 접속부 접촉 결함 (CRITICAL)', en: 'R-Phase Busbar Joint Thermal Defect (CRITICAL)' }, telemetry: 'BUSBAR_JOINT_R', temp: 110.5, db: 9, dist: 5.4, desc: { ko: '볼트 조임 풀림으로 접촉 저항 급상승. 화재 위험의 특고압 접속부.', en: 'Severe contact resistance from a loose bolt leading to high thermal buildup.' } }, { x: 65, y: 25, type: 'thermal', label: { ko: 'S상 부스바 볼트 체결 이상 발열 (WARNING)', en: 'S-Phase Busbar Joint Overheat (WARNING)' }, telemetry: 'BUSBAR_JOINT_S', temp: 75.8, db: 6, dist: 5.8, desc: { ko: '부스바 겹침부 접촉면 산화로 인한 경미한 온도 상승.', en: 'Mild overheat from surface oxidation on busbar overlapping joints.' } }, { x: 18, y: 68, type: 'thermal', label: { ko: '계기용 변류기(CT) primary 단자 접촉 과열', en: 'Current Transformer Primary Contact Heat' }, telemetry: 'CT_TERM_A2', temp: 85.2, db: 11, dist: 6.8, desc: { ko: '단자 체결 불량 및 장시간 하중 노후화로 인한 이상 발열 발생.', en: 'Terminal load heating at Current Transformer connection bushings.' } }, { x: 44, y: 55, type: 'prpd', label: { ko: '진공차단기 크래들 절연 방전 (WARNING)', en: 'VCB Cradle Insulation Discharge (WARNING)' }, telemetry: 'VCB_CRADLE_B', temp: 26.8, db: 45, dist: 8.9, desc: { ko: '크래들 지지 절연 배리어 부분 방전. 초음파 고유 위상 신호 확인.', en: 'Partial arc discharge detected at the circuit breaker socket insulation barrier.' } }, { x: 60, y: 72, type: 'prpd', label: { ko: '계기용 변압기(GPT) 1차측 부분방전', en: 'GPT Primary Partial Discharge' }, telemetry: 'CT_TERM_A3', temp: 24.8, db: 37, dist: 7.8, desc: { ko: '변압기 외함 부싱 절연 파괴에 의한 소규모 방전음 감지.', en: 'Small discharge sound detected on Transformer casing bushing.' } }, { x: 36, y: 46, type: 'mixed', label: { ko: 'VCB 접점 얼라인먼트 어긋남 아크 및 발열 (DANGER)', en: 'VCB Contact Alignment Arc & Overheat (DANGER)' }, telemetry: 'VCB_ALIGN_FAULT', temp: 44.2, db: 48, dist: 6.2, desc: { ko: '접촉 단자 중심 정렬 결함으로 상간 절연 틈새 전기 스파크(초음파)와 접촉부 열화가 함께 진행 중인 융복합 핵심 부분.', en: 'Circuit breaker main finger contact misalignment causing electrical arcing and heat.' } } ] } ]; function HomeHero() { const { lang } = useSite(); const [activeScene, setActiveScene] = React.useState(0); const [mode, setMode] = React.useState('fusion'); // 'fusion' | 'thermal' | 'ultrasound' const [hoveredHotspot, setHoveredHotspot] = React.useState(null); const [isAutoPlaying, setIsAutoPlaying] = React.useState(true); const [logs, setLogs] = React.useState([]); const [containerSize, setContainerSize] = React.useState({ width: 0, height: 0 }); const [bgImage, setBgImage] = React.useState(null); const canvasRef = React.useRef(null); const containerRef = React.useRef(null); const miniPrpdRef = React.useRef(null); const miniTempRef = React.useRef(null); const animationFrameId = React.useRef(null); const mouseRef = React.useRef({ x: 0, y: 0, active: false }); const crosshairRef = React.useRef({ x: 0, y: 0 }); const tempHistoryRef = React.useRef(Array(20).fill(24.0)); const sceneTimerRef = React.useRef(null); const prpdOpacityRef = React.useRef(0); const activePrpdHotspotRef = React.useRef(null); // Web Audio API refs for ultrasound crackle sound simulation const audioCtxRef = React.useRef(null); const noiseSourceRef = React.useRef(null); const sparkGainRef = React.useRef(null); const hissGainRef = React.useRef(null); const masterGainRef = React.useRef(null); const audioTimeoutRef = React.useRef(null); const glitchRef = React.useRef({ active: false, frames: 0, totalFrames: 0 }); const scene = SCENES[activeScene]; const activeHotspots = scene.hotspots; // Filter hotspots based on current mode: // - 'thermal' shows only Red (thermal) hotspots // - 'ultrasound' shows only Cyan (prpd) hotspots // - 'fusion' shows all (Red, Cyan, and Orange) hotspots const visibleHotspots = activeHotspots.filter(hs => { if (mode === 'thermal') return hs.type === 'thermal'; if (mode === 'ultrasound') return hs.type === 'prpd'; return true; // fusion }); // Initialize Web Audio API components for partial discharge sound synthesis const initAudio = React.useCallback(() => { if (audioCtxRef.current) return; try { const AudioContextClass = window.AudioContext || window.webkitAudioContext; if (!AudioContextClass) return; const audioCtx = new AudioContextClass(); audioCtxRef.current = audioCtx; // 1. Create a looped white noise buffer (2 seconds long) const bufferSize = audioCtx.sampleRate * 2; const noiseBuffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate); const output = noiseBuffer.getChannelData(0); for (let i = 0; i < bufferSize; i++) { output[i] = Math.random() * 2 - 1; } const noiseSource = audioCtx.createBufferSource(); noiseSource.buffer = noiseBuffer; noiseSource.loop = true; noiseSourceRef.current = noiseSource; // 2. Bandpass Filter (between 1.5kHz and 3.5kHz for fuller, more audible crackle) const filter = audioCtx.createBiquadFilter(); filter.type = 'bandpass'; filter.frequency.value = 2400; // Center frequency for crackle hiss filter.Q.value = 1.0; // Lower Q for a fuller, warmer sound // 3. Hiss Gain Node (for continuous background discharge hiss) const hissGain = audioCtx.createGain(); hissGain.gain.setValueAtTime(0, audioCtx.currentTime); hissGainRef.current = hissGain; // 4. Spark Gain Node (for rapid crackles / electrical arcing impulses) const sparkGain = audioCtx.createGain(); sparkGain.gain.setValueAtTime(0, audioCtx.currentTime); sparkGainRef.current = sparkGain; // 5. Master Gain Node (safely capped to avoid loud blasts) const masterGain = audioCtx.createGain(); masterGain.gain.setValueAtTime(0, audioCtx.currentTime); masterGainRef.current = masterGain; // Connections: NoiseSource -> Filter -> Hiss/Spark Gains -> MasterGain -> Destination noiseSource.connect(filter); filter.connect(hissGain); filter.connect(sparkGain); hissGain.connect(masterGain); sparkGain.connect(masterGain); masterGain.connect(audioCtx.destination); // Start playing noise buffer noiseSource.start(0); // Spark scheduler loop for crackle sound simulation const runCrackle = () => { if (!audioCtxRef.current) return; const opacity = prpdOpacityRef.current; const now = audioCtx.currentTime; if (audioCtx.state === 'suspended') { audioTimeoutRef.current = setTimeout(runCrackle, 100); return; } if (opacity > 0.01) { // Density of sparks increases as opacity approaches 1 (hover focus) const nextDelay = 15 + Math.random() * (120 - 100 * opacity); // Random spark impulse volume scaled by opacity (louder than before) const sparkVolume = (0.35 + Math.random() * 0.5) * opacity * 0.8; const duration = 0.004 + Math.random() * 0.018; // 4ms to 22ms decay duration sparkGain.gain.cancelScheduledValues(now); sparkGain.gain.setValueAtTime(0, now); sparkGain.gain.linearRampToValueAtTime(sparkVolume, now + 0.001); sparkGain.gain.exponentialRampToValueAtTime(0.0001, now + duration); // Soft background ionization hiss scaled by proximity (louder than before) hissGain.gain.setValueAtTime(opacity * 0.22, now); audioTimeoutRef.current = setTimeout(runCrackle, nextDelay); } else { // No active PRPD node hovered, keep output quiet hissGain.gain.setValueAtTime(0, now); sparkGain.gain.setValueAtTime(0, now); audioTimeoutRef.current = setTimeout(runCrackle, 80); } }; runCrackle(); } catch (err) { console.warn("Failed to initialize Web Audio API:", err); } }, []); // Cleanup Web Audio resources on unmount React.useEffect(() => { return () => { if (audioTimeoutRef.current) { clearTimeout(audioTimeoutRef.current); } if (noiseSourceRef.current) { try { noiseSourceRef.current.stop(); } catch (e) {} } if (audioCtxRef.current) { audioCtxRef.current.close().catch(() => {}); audioCtxRef.current = null; } }; }, []); // Ensure Web Audio API is unlocked on first user interaction (click/tap) const unlockAudio = React.useCallback(() => { if (!audioCtxRef.current) { initAudio(); } else if (audioCtxRef.current.state === 'suspended') { audioCtxRef.current.resume().catch(() => {}); } }, [initAudio]); React.useEffect(() => { window.addEventListener('click', unlockAudio, { passive: true }); window.addEventListener('touchstart', unlockAudio, { passive: true }); return () => { window.removeEventListener('click', unlockAudio); window.removeEventListener('touchstart', unlockAudio); }; }, [unlockAudio]); // Track container dimensions dynamically with ResizeObserver React.useEffect(() => { if (!containerRef.current) return; const observer = new ResizeObserver((entries) => { for (let entry of entries) { const { width, height } = entry.contentRect; setContainerSize({ width, height }); } }); observer.observe(containerRef.current); return () => observer.disconnect(); }, []); // System Logs generator helper const addLog = React.useCallback((text, type = 'info') => { const time = new Date().toLocaleTimeString('ko-KR', { hour12: false }); setLogs(prev => { const next = [...prev, { time, text, type }]; if (next.length > 4) next.shift(); return next; }); }, []); // Load background image for canvas drawing React.useEffect(() => { setBgImage(null); // Clear previous image const img = new Image(); img.src = scene.bg; img.onload = () => { setBgImage(img); }; }, [scene.bg]); // Resize canvas when containerSize changes React.useEffect(() => { const canvas = canvasRef.current; if (!canvas || !containerSize.width || !containerSize.height) return; const ctx = canvas.getContext('2d'); const dpr = window.devicePixelRatio || 1; canvas.width = containerSize.width * dpr; canvas.height = containerSize.height * dpr; ctx.scale(dpr, dpr); }, [containerSize]); // Initialize and handle auto-rotation React.useEffect(() => { addLog(`[SYSTEM] Initialized Fusion Diagnosis Engine.`, 'success'); addLog(`[SECTOR] Switched to ${SCENES[activeScene].id.toUpperCase()}.`, 'info'); setHoveredHotspot(null); }, [activeScene, addLog]); React.useEffect(() => { if (isAutoPlaying) { sceneTimerRef.current = setInterval(() => { setActiveScene(prev => (prev + 1) % SCENES.length); }, 12000); } return () => { if (sceneTimerRef.current) clearInterval(sceneTimerRef.current); }; }, [isAutoPlaying]); // Mode changes logger React.useEffect(() => { addLog(`[CAMERA] Switched mode to: ${mode.toUpperCase()}`, 'warn'); }, [mode, addLog]); // Canvas interaction animation loop React.useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); let frame = 0; const render = () => { frame++; const rect = containerSize; if (rect.width === 0 || rect.height === 0) { animationFrameId.current = requestAnimationFrame(render); return; } ctx.clearRect(0, 0, rect.width, rect.height); // Find locked hotspot (if mouse is active and near) let activeHotspot = null; let minDistance = SPOT_LOCK_PX; if (mouseRef.current.active && visibleHotspots.length > 0) { visibleHotspots.forEach(hs => { const coords = getImageCoverCoords(hs.x, hs.y, rect.width, rect.height); const dist = Math.hypot(mouseRef.current.x - coords.x, mouseRef.current.y - coords.y); if (dist < minDistance) { minDistance = dist; activeHotspot = hs; } }); } let targetX = mouseRef.current.x; let targetY = mouseRef.current.y; if (activeHotspot) { const coords = getImageCoverCoords(activeHotspot.x, activeHotspot.y, rect.width, rect.height); targetX = coords.x; targetY = coords.y; } crosshairRef.current.x += (targetX - crosshairRef.current.x) * 0.25; crosshairRef.current.y += (targetY - crosshairRef.current.y) * 0.25; // Update state if lock changes if (activeHotspot !== hoveredHotspot) { setHoveredHotspot(activeHotspot); if (activeHotspot) { addLog(`[LOCK] ${activeHotspot.telemetry} acquired. dist: ${activeHotspot.dist}m.`, 'success'); // Trigger glitch visual on entering hotspot glitchRef.current = { active: true, frames: 0, totalFrames: (activeHotspot.type === 'prpd' || activeHotspot.type === 'mixed') ? 14 : 8, }; } else { addLog(`[TARGET] Scan mode active. Scanning...`, 'info'); } } // Update telemetry history refs if (frame % 15 === 0) { const curTemp = activeHotspot ? activeHotspot.temp : (24.0 + Math.random() * 0.8); tempHistoryRef.current.push(curTemp); if (tempHistoryRef.current.length > 20) tempHistoryRef.current.shift(); } // Render diagnostic grid lines if in ultrasound mode if (mode === 'ultrasound') { ctx.strokeStyle = 'rgba(34, 211, 238, 0.04)'; ctx.lineWidth = 1; // Draw grid for (let i = 0; i < rect.width; i += 50) { ctx.beginPath(); ctx.moveTo(i, 0); ctx.lineTo(i, rect.height); ctx.stroke(); } for (let j = 0; j < rect.height; j += 50) { ctx.beginPath(); ctx.moveTo(0, j); ctx.lineTo(rect.width, j); ctx.stroke(); } } // Draw inactive hotspots indicators and telemetry tags visibleHotspots.forEach((hs) => { const coords = getImageCoverCoords(hs.x, hs.y, rect.width, rect.height); const hX = coords.x; const hY = coords.y; const isHot = activeHotspot === hs; // 1. Draw inactive ambient glow for thermal/mixed hotspots (when NOT hovered) if ((hs.type === 'thermal' || hs.type === 'mixed') && mode !== 'ultrasound') { if (!isHot) { const radius = 35 + Math.sin(frame * 0.08) * 4; const intensity = 0.12; ctx.save(); ctx.globalCompositeOperation = 'screen'; const grad = ctx.createRadialGradient(hX, hY, 2, hX, hY, radius); grad.addColorStop(0, `rgba(239, 68, 68, ${intensity})`); grad.addColorStop(0.4, `rgba(247, 148, 29, ${intensity * 0.5})`); grad.addColorStop(1, 'rgba(0, 0, 0, 0)'); ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(hX, hY, radius, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } } // 2. Draw ultrasound sonar pulsing waves for prpd/mixed hotspots (shown in Fusion / Ultrasound modes) if ((hs.type === 'prpd' || hs.type === 'mixed') && mode !== 'thermal') { const waveCount = 2; for (let w = 0; w < waveCount; w++) { const rad = ((frame * 1.0 + w * 40) % 80); const alpha = (1 - rad / 80) * (isHot ? 0.6 : 0.08); ctx.strokeStyle = hs.type === 'mixed' ? `rgba(249, 115, 22, ${alpha})` : `rgba(34, 211, 238, ${alpha})`; ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(hX, hY, rad, 0, Math.PI * 2); ctx.stroke(); } } }); // 3. DRAW THERMAL LENS OVERLAY ON HOVER (For thermal and mixed hotspots) if (activeHotspot && (activeHotspot.type === 'thermal' || activeHotspot.type === 'mixed') && bgImage) { const coords = getImageCoverCoords(activeHotspot.x, activeHotspot.y, rect.width, rect.height); const hX = coords.x; const hY = coords.y; const radius = 75; // scope lens size ctx.save(); // 3a. Clip drawing context inside a circle ctx.beginPath(); ctx.arc(hX, hY, radius, 0, Math.PI * 2); ctx.clip(); // 3b. Draw background image inside the clipped circle with grayscale and high contrast filters ctx.filter = 'grayscale(1) contrast(1.6) brightness(0.8)'; const imgRatio = bgImage.width / bgImage.height; const containerRatio = rect.width / rect.height; let scaledW, scaledH, offsetX, offsetY; if (containerRatio > imgRatio) { scaledW = rect.width; scaledH = rect.width / imgRatio; offsetX = 0; offsetY = (rect.height - scaledH) / 2; } else { scaledW = rect.height * imgRatio; scaledH = rect.height; offsetX = (rect.width - scaledW) / 2; offsetY = 0; } ctx.drawImage(bgImage, offsetX, offsetY, scaledW, scaledH); ctx.filter = 'none'; // 3c. Overlay thermal color mapping via 'color' global composite operation ctx.globalCompositeOperation = 'color'; const thermalGrad = ctx.createRadialGradient(hX, hY, 2, hX, hY, radius); thermalGrad.addColorStop(0, '#ffffff'); // Center (white hot) thermalGrad.addColorStop(0.18, '#ffff00'); // Yellow thermalGrad.addColorStop(0.4, '#ff4400'); // Red-Orange thermalGrad.addColorStop(0.72, '#a000ff'); // Purple thermalGrad.addColorStop(1.0, '#0022ff'); // Blue (cool background) ctx.fillStyle = thermalGrad; ctx.beginPath(); ctx.arc(hX, hY, radius, 0, Math.PI * 2); ctx.fill(); ctx.restore(); // 3d. Draw HUD lens scope borders and reticle details ctx.strokeStyle = activeHotspot.type === 'mixed' ? '#f97316' : '#ef4444'; ctx.lineWidth = 2.5; ctx.beginPath(); ctx.arc(hX, hY, radius, 0, Math.PI * 2); ctx.stroke(); ctx.strokeStyle = activeHotspot.type === 'mixed' ? 'rgba(249, 115, 22, 0.4)' : 'rgba(239, 68, 68, 0.4)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(hX - radius - 10, hY); ctx.lineTo(hX + radius + 10, hY); ctx.moveTo(hX, hY - radius - 10); ctx.lineTo(hX, hY + radius + 10); ctx.stroke(); ctx.beginPath(); ctx.arc(hX, hY, 8, 0, Math.PI * 2); ctx.stroke(); // Print temperature floating label ctx.fillStyle = '#ffffff'; ctx.font = 'bold 10px monospace'; ctx.shadowColor = '#000000'; ctx.shadowBlur = 4; ctx.fillText(`${activeHotspot.temp.toFixed(1)}°C`, hX - 22, hY - radius - 6); ctx.shadowBlur = 0; } // 4. RENDER HIGH-FIDELITY BORDERLESS PRPD GRAPH POPUP BOX ON HOVER (For prpd and mixed hotspots) const hasAnyActive = !!activeHotspot; const hasPrpdActive = activeHotspot && (activeHotspot.type === 'prpd' || activeHotspot.type === 'mixed'); if (hasPrpdActive) { activePrpdHotspotRef.current = activeHotspot; } const targetPrpdOpacity = hasAnyActive ? 1 : 0; prpdOpacityRef.current += (targetPrpdOpacity - prpdOpacityRef.current) * 0.15; // Modulate audio volume based on hotspot opacity (representing lock on active hotspots) if (masterGainRef.current && audioCtxRef.current) { masterGainRef.current.gain.value = prpdOpacityRef.current * 0.8; // Auto-resume if the browser suspended the context due to inactivity/silence if (prpdOpacityRef.current > 0.05 && audioCtxRef.current.state === 'suspended') { audioCtxRef.current.resume().catch(() => {}); } } if (prpdOpacityRef.current > 0.01 && hasPrpdActive && activePrpdHotspotRef.current) { const hs = activePrpdHotspotRef.current; const coords = getImageCoverCoords(hs.x, hs.y, rect.width, rect.height); const hX = coords.x; const hY = coords.y; const opacity = prpdOpacityRef.current; const boxW = 220; const boxH = 140; // Position PRPD box next to the hotspot/lens to avoid overlapping const radius = (hs.type === 'mixed' || hs.type === 'thermal') ? 75 : 32; const spacing = 15; let bx; if (hX + radius + spacing + boxW < rect.width) { bx = hX + radius + spacing; } else if (hX - radius - spacing - boxW > 0) { bx = hX - radius - spacing - boxW; } else { bx = Math.max(12, rect.width - boxW - 12); } const by = Math.max(12, Math.min(rect.height - boxH - 12, hY - boxH / 2)); ctx.save(); ctx.shadowColor = hs.type === 'mixed' ? `rgba(249, 115, 22, ${0.45 * opacity})` : `rgba(34, 211, 238, ${0.45 * opacity})`; ctx.shadowBlur = 12; ctx.fillStyle = `rgba(6, 15, 30, ${0.88 * opacity})`; // Borderless (no stroke border) round rect background ctx.beginPath(); ctx.roundRect(bx, by, boxW, boxH, 8); ctx.fill(); ctx.shadowBlur = 0; ctx.fillStyle = `rgba(255, 255, 255, ${opacity})`; ctx.font = 'bold 8.5px monospace'; ctx.fillText(`[${hs.telemetry}] PRPD PARTIAL DISCHARGE`, bx + 10, by + 15); ctx.fillStyle = hs.type === 'mixed' ? `rgba(249, 115, 22, ${opacity})` : `rgba(34, 211, 238, ${opacity})`; ctx.font = '7.5px monospace'; ctx.fillText(`LEVEL: ${hs.db} dB | DISTANCE: ${hs.dist.toFixed(1)} m`, bx + 10, by + 26); const gx = bx + 32; const gy = by + 34; const gw = boxW - 42; const gh = boxH - 54; ctx.strokeStyle = `rgba(255, 255, 255, ${0.12 * opacity})`; ctx.lineWidth = 1; ctx.strokeRect(gx, gy, gw, gh); ctx.strokeStyle = `rgba(255, 255, 255, ${0.06 * opacity})`; ctx.lineWidth = 0.5; ctx.setLineDash([2, 3]); for (let v = 1; v <= 3; v++) { const vx = gx + (gw / 4) * v; ctx.beginPath(); ctx.moveTo(vx, gy); ctx.lineTo(vx, gy + gh); ctx.stroke(); } for (let h = 1; h <= 3; h++) { const hy = gy + (gh / 4) * h; ctx.beginPath(); ctx.moveTo(gx, hy); ctx.lineTo(gx + gw, hy); ctx.stroke(); } ctx.setLineDash([]); ctx.fillStyle = `rgba(255, 255, 255, ${0.5 * opacity})`; ctx.font = '6.5px monospace'; ctx.fillText('0°', gx - 2, gy + gh + 10); ctx.fillText('180°', gx + gw / 2 - 8, gy + gh + 10); ctx.fillText('360°', gx + gw - 12, gy + gh + 10); ctx.fillText('Amp', gx - 22, gy + 4); ctx.fillText('(pC)', gx - 22, gy + 11); ctx.fillText('100', gx - 16, gy + 10); ctx.fillText('50', gx - 12, gy + gh / 2 + 3); ctx.fillText('0', gx - 8, gy + gh); ctx.strokeStyle = `rgba(255, 255, 255, ${0.15 * opacity})`; ctx.lineWidth = 1; ctx.beginPath(); const sineOffset = frame * 0.03; for (let px = 0; px <= gw; px++) { const phase = (px / gw) * Math.PI * 2; const py = (gy + gh / 2) + Math.sin(phase + sineOffset) * (gh * 0.38); if (px === 0) ctx.moveTo(gx + px, py); else ctx.lineTo(gx + px, py); } ctx.stroke(); let prpdLcgSeedVal = 0; for (let i = 0; i < hs.telemetry.length; i++) prpdLcgSeedVal += hs.telemetry.charCodeAt(i); let prpdLcg = 777 + prpdLcgSeedVal; const lcgNext = () => { prpdLcg = (prpdLcg * 1664525 + 1013904223) % 4294967296; return prpdLcg / 4294967296; }; const dotCount = 330; for (let d = 0; d < dotCount; d++) { const isPosHalf = lcgNext() > 0.5; const centerPhase = isPosHalf ? 50 : 230; const u1 = lcgNext(); const u2 = lcgNext(); const randNorm1 = Math.sqrt(-2.0 * Math.log(u1)) * Math.sin(2.0 * Math.PI * u2); const u3 = lcgNext(); const u4 = lcgNext(); const randNorm2 = Math.sqrt(-2.0 * Math.log(u3)) * Math.sin(2.0 * Math.PI * u4); let phaseVal = centerPhase + randNorm1 * 20; if (phaseVal < 0) phaseVal += 360; if (phaseVal > 360) phaseVal -= 360; let magVal = 55 + randNorm2 * 14; if (magVal < 2) magVal = 2; if (magVal > 98) magVal = 98; const shimmerX = Math.sin(frame * 0.08 + d) * 1.2; const shimmerY = Math.cos(frame * 0.08 + d) * 1.2; const finalPhase = phaseVal + shimmerX; const finalMag = magVal + shimmerY; const plotX = gx + (finalPhase / 360) * gw; const plotY = (gy + gh) - (finalMag / 100) * gh; if (plotX >= gx && plotX <= gx + gw && plotY >= gy && plotY <= gy + gh) { const targetPhase = isPosHalf ? 50 : 230; const dX = (finalPhase - targetPhase) / 20; const dY = (finalMag - 55) / 14; const distToCenter = Math.hypot(dX, dY); let dotColor = `rgba(34, 211, 238, ${opacity})`; if (distToCenter < 0.6) { dotColor = `rgba(239, 68, 68, ${opacity})`; } else if (distToCenter < 1.1) { dotColor = `rgba(249, 115, 22, ${opacity})`; } else if (distToCenter < 1.6) { dotColor = `rgba(234, 179, 8, ${opacity})`; } ctx.fillStyle = dotColor; ctx.fillRect(plotX, plotY, 1.2, 1.2); } } ctx.restore(); } // 4.5. GLITCH OVERLAY (triggered on hotspot enter) if (glitchRef.current.active) { const g = glitchRef.current; g.frames++; if (g.frames >= g.totalFrames) { glitchRef.current.active = false; } else { const glitchProgress = g.frames / g.totalFrames; const intensity = (1 - glitchProgress) * (activeHotspot ? 1 : 0.5); const lineCount = Math.floor(6 + Math.random() * 8); for (let gl = 0; gl < lineCount; gl++) { if (Math.random() > 0.45) continue; const gy2 = Math.random() * rect.height; const glH = 1 + Math.random() * 4; const glW = 20 + Math.random() * (rect.width * 0.55); const glX = Math.random() * (rect.width - glW); const shift = (Math.random() - 0.5) * 18; // Chromatic aberration style glitch slice ctx.save(); ctx.globalAlpha = intensity * (0.12 + Math.random() * 0.22); if (bgImage) { const imgRatio = bgImage.width / bgImage.height; const containerRatio = rect.width / rect.height; let scaledW, scaledH, offsetX, offsetY; if (containerRatio > imgRatio) { scaledW = rect.width; scaledH = rect.width / imgRatio; offsetX = 0; offsetY = (rect.height - scaledH) / 2; } else { scaledW = rect.height * imgRatio; scaledH = rect.height; offsetX = (rect.width - scaledW) / 2; offsetY = 0; } ctx.beginPath(); ctx.rect(glX, gy2, glW, glH); ctx.clip(); // Red channel shift ctx.globalCompositeOperation = 'screen'; ctx.filter = 'saturate(3) hue-rotate(0deg) brightness(1.5)'; ctx.drawImage(bgImage, offsetX + shift, offsetY, scaledW, scaledH); ctx.filter = 'none'; } ctx.restore(); // Solid glitch bar ctx.save(); ctx.globalAlpha = intensity * 0.08; ctx.fillStyle = Math.random() > 0.5 ? '#22d3ee' : '#ef4444'; ctx.fillRect(glX, gy2, glW, glH); ctx.restore(); } // Scanline flicker ctx.save(); ctx.globalAlpha = intensity * 0.06; ctx.fillStyle = '#000'; for (let sl = 0; sl < rect.height; sl += 4) { ctx.fillRect(0, sl, rect.width, 1); } ctx.restore(); } } // 5. HUD LASER CROSSHAIR SCANNER (Shown when mouse is actively hovering) if (mouseRef.current.active) { const cX = crosshairRef.current.x; const cY = crosshairRef.current.y; if (activeHotspot) { ctx.strokeStyle = activeHotspot.type === 'thermal' ? 'rgba(239, 68, 68, 0.4)' : (activeHotspot.type === 'mixed' ? 'rgba(249, 115, 22, 0.4)' : 'rgba(34, 211, 238, 0.4)'); ctx.lineWidth = 1; ctx.setLineDash([3, 4]); ctx.beginPath(); ctx.moveTo(mouseRef.current.x, mouseRef.current.y); ctx.lineTo(cX, cY); ctx.stroke(); ctx.setLineDash([]); } ctx.strokeStyle = activeHotspot ? (activeHotspot.type === 'thermal' ? '#ef4444' : (activeHotspot.type === 'mixed' ? '#f97316' : '#22d3ee')) : '#38bdf8'; ctx.lineWidth = 1.2; ctx.beginPath(); ctx.arc(cX, cY, 32, 0, Math.PI * 2); ctx.stroke(); ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)'; ctx.beginPath(); ctx.arc(cX, cY, 44, 0, Math.PI * 2); ctx.stroke(); ctx.strokeStyle = activeHotspot ? (activeHotspot.type === 'thermal' ? 'rgba(239, 68, 68, 0.5)' : (activeHotspot.type === 'mixed' ? 'rgba(249, 115, 22, 0.5)' : 'rgba(34, 211, 238, 0.5)')) : 'rgba(56, 189, 248, 0.5)'; ctx.beginPath(); ctx.moveTo(cX - 40, cY); ctx.lineTo(cX - 12, cY); ctx.moveTo(cX + 12, cY); ctx.lineTo(cX + 40, cY); ctx.moveTo(cX, cY - 40); ctx.lineTo(cX, cY - 12); ctx.moveTo(cX, cY + 12); ctx.lineTo(cX, cY + 40); ctx.stroke(); if (activeHotspot) { const b = 10; const r = 26; ctx.strokeStyle = activeHotspot.type === 'thermal' ? '#ef4444' : (activeHotspot.type === 'mixed' ? '#f97316' : '#22d3ee'); ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(cX - r, cY - r + b); ctx.lineTo(cX - r, cY - r); ctx.lineTo(cX - r + b, cY - r); ctx.stroke(); ctx.beginPath(); ctx.moveTo(cX + r, cY - r + b); ctx.lineTo(cX + r, cY - r); ctx.lineTo(cX + r - b, cY - r); ctx.stroke(); ctx.beginPath(); ctx.moveTo(cX - r, cY + r - b); ctx.lineTo(cX - r, cY + r); ctx.lineTo(cX - r + b, cY + r); ctx.stroke(); ctx.beginPath(); ctx.moveTo(cX + r, cY + r - b); ctx.lineTo(cX + r, cY + r); ctx.lineTo(cX + r - b, cY + r); ctx.stroke(); ctx.fillStyle = activeHotspot.type === 'thermal' ? '#ef4444' : (activeHotspot.type === 'mixed' ? '#f97316' : '#22d3ee'); ctx.font = 'bold 9px monospace'; ctx.fillText("LOCK TRG", cX - 22, cY + 42); } } drawMiniCharts(activeHotspot); animationFrameId.current = requestAnimationFrame(render); }; const drawMiniCharts = (activeHs) => { // 1. PRPD mini chart const prpd = miniPrpdRef.current; if (prpd) { const pCtx = prpd.getContext('2d'); pCtx.clearRect(0, 0, prpd.width, prpd.height); const w = prpd.width; const h = prpd.height; const baseline = h / 2; pCtx.strokeStyle = 'rgba(255, 255, 255, 0.08)'; pCtx.lineWidth = 1; pCtx.beginPath(); pCtx.moveTo(0, baseline); pCtx.lineTo(w, baseline); pCtx.stroke(); pCtx.strokeStyle = activeHs && (activeHs.type === 'prpd' || activeHs.type === 'mixed') ? 'rgba(34, 211, 238, 0.7)' : 'rgba(255, 255, 255, 0.15)'; pCtx.lineWidth = 1.2; pCtx.beginPath(); for (let x = 0; x < w; x++) { const phi = (x / w) * Math.PI * 2 * 2; const y = baseline + Math.sin(phi) * (h * 0.35); if (x === 0) pCtx.moveTo(x, y); else pCtx.lineTo(x, y); } pCtx.stroke(); pCtx.fillStyle = '#22d3ee'; if (activeHs && (activeHs.type === 'prpd' || activeHs.type === 'mixed')) { for (let i = 0; i < 30; i++) { const x = Math.random() * w; const phi = (x / w) * Math.PI * 2 * 2; const sineVal = Math.sin(phi); if (Math.abs(sineVal) > 0.45) { const y = baseline + (sineVal * (h * 0.35)) - (Math.random() * h * 0.35) * Math.sign(sineVal); pCtx.fillRect(x, y, 1, 1); } } } else { pCtx.fillStyle = 'rgba(255, 255, 255, 0.2)'; for (let i = 0; i < 4; i++) { pCtx.fillRect(Math.random() * w, h - 2 - Math.random() * 3, 0.8, 0.8); } } } // 2. Temp sparkline chart const temp = miniTempRef.current; if (temp) { const tCtx = temp.getContext('2d'); tCtx.clearRect(0, 0, temp.width, temp.height); const w = temp.width; const h = temp.height; const history = tempHistoryRef.current; const maxVal = 120; const minVal = 10; if (history.length > 1) { tCtx.strokeStyle = activeHs && (activeHs.type === 'thermal' || activeHs.type === 'mixed') ? (activeHs.type === 'mixed' ? '#f97316' : '#ef4444') : '#10b981'; tCtx.lineWidth = 1.5; tCtx.beginPath(); for (let i = 0; i < history.length; i++) { const x = (i / (history.length - 1)) * w; const pct = (history[i] - minVal) / (maxVal - minVal); const y = h - (pct * h * 0.8 + h * 0.1); if (i === 0) tCtx.moveTo(x, y); else tCtx.lineTo(x, y); } tCtx.stroke(); } } }; render(); return () => { if (animationFrameId.current) cancelAnimationFrame(animationFrameId.current); }; }, [scene, mode, hoveredHotspot, addLog, containerSize, bgImage, visibleHotspots]); // Handle slide changes manually const selectScene = (idx) => { setIsAutoPlaying(false); setActiveScene(idx); }; const handleMouseMove = (e) => { const rect = containerRef.current.getBoundingClientRect(); mouseRef.current = { x: e.clientX - rect.left, y: e.clientY - rect.top, active: true }; }; const handleMouseLeave = () => { mouseRef.current.active = false; setHoveredHotspot(null); }; const ambientTemp = 24.1 + Math.sin(Date.now() * 0.001) * 0.4; const displayTemp = hoveredHotspot ? hoveredHotspot.temp : ambientTemp; const displayDb = hoveredHotspot ? hoveredHotspot.db : 10; const displayDist = hoveredHotspot ? hoveredHotspot.dist : null; return (
{/* Background Slides */}
{SCENES.map((s, idx) => (
))}
{/* Crosshair cursor layer (mousemove handled on hero via capture) */}
); } const VALUE_CARDS = [ ['1','hybrid'],['2','cert'],['3','report'],['4','globe'],['5','cloud'],['6','support'], ]; function ValueIcon({ kind }) { const c = { viewBox:"0 0 32 32", width:"22", height:"22", fill:"none", stroke:"currentColor", strokeWidth:"1.4" }; if (kind==='hybrid') return ; if (kind==='cert') return ; if (kind==='report') return ; if (kind==='globe') return ; if (kind==='cloud') return ; return ; } function HomeCoreSiteLinks() { const links = [ { href: 'https://entecpower.net/products/md1000/', label: 'MD-1000', tone: 'md1000' }, { href: 'https://entecpower.net/education/cdp/', labelKey: 'home_core_cdp', tone: 'cdp' }, { href: 'https://entecpower.net/products/HD6G_V/', label: 'HD-6G', tone: 'hd6g' }, ]; return (
{links.map((link) => ( {link.labelKey ? : link.label} ))}
); } function HomeValues() { return (

{VALUE_CARDS.map(([n,k]) => (

))}
); } function HomeProducts() { return (

{/* MD-1000 */}
MD-1000

{/* HD-6G */}
HD-6G

); } const INDUSTRY_LIST = [ ['전력 · 송변전','Power & Transmission','power','부분방전·코로나·절연 결함을 위상-진폭 평면에서 위치 추정.'], ['제조 · 스마트팩토리','Manufacturing','mfg','모터·버스바·MCC를 정지 없이 점검. 95dB 환경에서도 동작.'], ['석유화학 · 가스','Petrochemical & Gas','chem','압축공기·스팀·가스 라인의 누출을 신속 위치 특정.'], ['반도체 · 데이터센터','Semiconductor & DC','data','변압기 질소가스 누설·UPS 진단. 24/7 운용.'], ['철도 · 교통','Rail & Transit','rail','KORAIL 운영. 변전소·차량 베어링 진단.'], ['특수 · 해외 광산','Mining & Special','mining','Rio Tinto 호주, 글로벌 EPC 프로젝트 대응.'], ]; function IndIcon({ k }) { const c = { viewBox:"0 0 32 32", width:"22", height:"22", fill:"none", stroke:"currentColor", strokeWidth:"1.3" }; if (k==='power') return ; if (k==='mfg') return ; if (k==='data') return ; if (k==='chem') return ; if (k==='rail') return ; return ; } function HomeIndustries() { const { lang } = useSite(); return (

{INDUSTRY_LIST.map(([ko,en,k,d],i) => (

{lang === 'ko' ? ko : en}

{d}

))}
); } const PARTNERS = ['kepco','korail','hyundai_electric','posco','sgcc','datang','fujitsu']; function HomePartners() { return (

{PARTNERS.map(p => ( {p}{e.target.style.opacity='1';e.target.style.filter='grayscale(0)';}} onMouseOut={e=>{e.target.style.opacity='.85';e.target.style.filter='grayscale(.5)';}}/> ))}
View partners & global map →
); } const NEWS_PREVIEW = [ ['press','2026-04','엔텍, 글로벌 파트너십 체결','ENTEC signs global partnership','전력·제조 분야 주요 기업과 해외 공급망을 확대했습니다.','Expanded global supply network across power and manufacturing.'], ['update','2026-03','교육·인증 포털 v2 공개','Training portal v2 launched','19개 언어, 진단 시뮬레이터, 이수증 PDF 자동 발급.','19 languages, in-browser diagnostic simulator, auto-issued certificates.'], ['blog','2026-02','현장 진단 품질을 높이는 3가지 팁','3 tips for better field diagnosis','초보 점검자가 90일 안에 실전화하는 체크리스트.','A checklist that gets new inspectors production-ready in 90 days.'], ]; function HomeNews() { const { lang } = useSite(); const ko = lang === 'ko'; return (

{NEWS_PREVIEW.map(([cat,date,kt,et,kd,ed],i) => (
{cat} {date}

{ko ? kt : et}

{ko ? kd : ed}

))}
); } Object.assign(window, { HomeHero, HomeValues, HomeProducts, HomeIndustries, HomePartners, HomeNews });