// Main JavaScript for SmartSolTech Website document.addEventListener('DOMContentLoaded', function() { // Initialize AOS (Animate On Scroll) if (typeof AOS !== 'undefined') { AOS.init({ duration: 800, easing: 'ease-in-out', once: true, offset: 100 }); } // Theme Management const themeToggle = document.getElementById('theme-toggle'); const html = document.documentElement; const slider = document.querySelector('.theme-toggle-slider'); const sunIcon = document.querySelector('.theme-sun-icon'); const moonIcon = document.querySelector('.theme-moon-icon'); // Get current theme from server or localStorage const serverTheme = html.classList.contains('dark') ? 'dark' : 'light'; const localTheme = localStorage.getItem('theme'); const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; // Priority: localStorage > server > system preference const currentTheme = localTheme || serverTheme || (prefersDark ? 'dark' : 'light'); console.log('Theme initialization:', { serverTheme, localTheme, prefersDark, currentTheme }); // Apply theme and update toggle with smooth animation function applyTheme(isDark, animate = false) { console.log('Applying theme:', isDark ? 'dark' : 'light', 'animate:', animate); if (isDark) { html.classList.add('dark'); if (themeToggle) themeToggle.checked = true; if (slider) { // Расчет: ширина контейнера (56px) - ширина ползунка (20px) - отступы (8px) = 28px движения slider.style.transform = 'translateX(28px)'; } if (sunIcon && moonIcon && animate) { // Animated transition to dark sunIcon.style.transform = 'rotate(180deg) scale(0)'; sunIcon.style.opacity = '0'; moonIcon.style.transform = 'rotate(0deg) scale(1)'; moonIcon.style.opacity = '1'; } else if (sunIcon && moonIcon) { // Instant set for initial load sunIcon.style.transform = 'rotate(180deg) scale(0)'; sunIcon.style.opacity = '0'; moonIcon.style.transform = 'rotate(0deg) scale(1)'; moonIcon.style.opacity = '1'; } } else { html.classList.remove('dark'); if (themeToggle) themeToggle.checked = false; if (slider) { slider.style.transform = 'translateX(0)'; } if (sunIcon && moonIcon && animate) { // Animated transition to light moonIcon.style.transform = 'rotate(-180deg) scale(0)'; moonIcon.style.opacity = '0'; sunIcon.style.transform = 'rotate(0deg) scale(1)'; sunIcon.style.opacity = '1'; } else if (sunIcon && moonIcon) { // Instant set for initial load moonIcon.style.transform = 'rotate(-180deg) scale(0)'; moonIcon.style.opacity = '0'; sunIcon.style.transform = 'rotate(0deg) scale(1)'; sunIcon.style.opacity = '1'; } } } // Initial theme application (без анимации при загрузке) applyTheme(currentTheme === 'dark', false); // Theme toggle handler function toggleTheme() { const isDark = html.classList.contains('dark'); const newTheme = isDark ? 'light' : 'dark'; console.log('Toggling theme from', isDark ? 'dark' : 'light', 'to', newTheme); applyTheme(!isDark, true); // С анимацией при клике localStorage.setItem('theme', newTheme); // Send to server fetch(`/theme/${newTheme}`, { method: 'GET', headers: { 'Accept': 'application/json' } }).then(response => response.json()) .then(data => console.log('Theme synced to server:', data)) .catch(error => { console.warn('Theme sync failed:', error); }); } if (themeToggle) { themeToggle.addEventListener('change', toggleTheme); console.log('Theme toggle listener attached'); } else { console.warn('Theme toggle element not found'); } // Mobile Navigation Toggle const mobileMenuButton = document.querySelector('.mobile-menu-button'); const mobileMenu = document.querySelector('.mobile-menu'); if (mobileMenuButton && mobileMenu) { mobileMenuButton.addEventListener('click', function() { mobileMenu.classList.toggle('show'); const isOpen = mobileMenu.classList.contains('show'); // Toggle button icon const icon = mobileMenuButton.querySelector('svg'); if (icon) { icon.innerHTML = isOpen ? '' : ''; } // Accessibility mobileMenuButton.setAttribute('aria-expanded', isOpen); }); // Close mobile menu when clicking outside document.addEventListener('click', function(e) { if (!mobileMenuButton.contains(e.target) && !mobileMenu.contains(e.target)) { mobileMenu.classList.remove('show'); mobileMenuButton.setAttribute('aria-expanded', 'false'); } }); } // Navbar Scroll Effect const navbar = document.querySelector('nav'); let lastScrollY = window.scrollY; window.addEventListener('scroll', function() { const currentScrollY = window.scrollY; if (navbar) { if (currentScrollY > 100) { navbar.classList.add('navbar-scrolled'); } else { navbar.classList.remove('navbar-scrolled'); } // Hide/show navbar on scroll if (currentScrollY > lastScrollY && currentScrollY > 200) { navbar.style.transform = 'translateY(-100%)'; } else { navbar.style.transform = 'translateY(0)'; } } lastScrollY = currentScrollY; }); // Smooth Scrolling for Anchor Links document.querySelectorAll('a[href^="#"]').forEach(anchor => { anchor.addEventListener('click', function(e) { e.preventDefault(); const target = document.querySelector(this.getAttribute('href')); if (target) { const offsetTop = target.offsetTop - (navbar ? navbar.offsetHeight : 0); window.scrollTo({ top: offsetTop, behavior: 'smooth' }); } }); }); // Contact Form Handler const quickContactForm = document.getElementById('quick-contact-form'); if (quickContactForm) { quickContactForm.addEventListener('submit', handleContactSubmit); } const mainContactForm = document.getElementById('contact-form'); if (mainContactForm) { mainContactForm.addEventListener('submit', handleContactSubmit); } async function handleContactSubmit(e) { e.preventDefault(); const form = e.target; const submitButton = form.querySelector('button[type="submit"]'); const originalText = submitButton.textContent; // Show loading state submitButton.disabled = true; submitButton.classList.add('btn-loading'); submitButton.textContent = '전송 중...'; try { const formData = new FormData(form); const data = Object.fromEntries(formData.entries()); const response = await fetch('/api/contact/submit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); const result = await response.json(); if (result.success) { showNotification('메시지가 성공적으로 전송되었습니다! 곧 연락드리겠습니다.', 'success'); form.reset(); } else { showNotification('메시지 전송 중 오류가 발생했습니다. 다시 시도해주세요.', 'error'); } } catch (error) { console.error('Contact form error:', error); showNotification('메시지 전송 중 오류가 발생했습니다. 다시 시도해주세요.', 'error'); } finally { // Reset button state submitButton.disabled = false; submitButton.classList.remove('btn-loading'); submitButton.textContent = originalText; } } // Notification System function showNotification(message, type = 'info') { const notification = document.createElement('div'); notification.className = `notification notification-${type}`; notification.innerHTML = `
${message}
`; // Styles notification.style.cssText = ` position: fixed; top: 20px; right: 20px; z-index: 9999; max-width: 400px; padding: 1rem; border-radius: 0.5rem; color: white; font-weight: 500; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); transform: translateX(100%); transition: transform 0.3s ease; ${type === 'success' ? 'background: #10b981;' : ''} ${type === 'error' ? 'background: #ef4444;' : ''} ${type === 'info' ? 'background: #3b82f6;' : ''} `; document.body.appendChild(notification); // Animate in setTimeout(() => { notification.style.transform = 'translateX(0)'; }, 100); // Close button handler const closeButton = notification.querySelector('.notification-close'); closeButton.addEventListener('click', () => { closeNotification(notification); }); // Auto close after 5 seconds setTimeout(() => { closeNotification(notification); }, 5000); } function closeNotification(notification) { notification.style.transform = 'translateX(100%)'; setTimeout(() => { if (notification.parentNode) { notification.parentNode.removeChild(notification); } }, 300); } // Portfolio Filter (if on portfolio page) const portfolioFilters = document.querySelectorAll('.portfolio-filter'); const portfolioItems = document.querySelectorAll('.portfolio-item'); portfolioFilters.forEach(filter => { filter.addEventListener('click', function() { const category = this.dataset.category; // Update active filter portfolioFilters.forEach(f => f.classList.remove('active')); this.classList.add('active'); // Filter items portfolioItems.forEach(item => { if (category === 'all' || item.dataset.category === category) { item.style.display = 'block'; item.style.animation = 'fadeIn 0.5s ease'; } else { item.style.display = 'none'; } }); }); }); // Image Lazy Loading const images = document.querySelectorAll('img[data-src]'); const imageObserver = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; img.src = img.dataset.src; img.classList.remove('lazy'); imageObserver.unobserve(img); } }); }); images.forEach(img => imageObserver.observe(img)); // Service Worker Registration for PWA if ('serviceWorker' in navigator && 'PushManager' in window) { navigator.serviceWorker.register('/sw.js') .then(registration => { console.log('Service Worker registered successfully:', registration); }) .catch(error => { console.log('Service Worker registration failed:', error); }); } // Performance Monitoring if ('performance' in window) { window.addEventListener('load', () => { setTimeout(() => { const perfData = performance.getEntriesByType('navigation')[0]; const loadTime = perfData.loadEventEnd - perfData.loadEventStart; if (loadTime > 3000) { console.warn('Page load time is slow:', loadTime + 'ms'); } }, 1000); }); } // Cookie Consent (if needed) function initCookieConsent() { const consent = localStorage.getItem('cookieConsent'); if (!consent) { showCookieConsent(); } } function showCookieConsent() { const banner = document.createElement('div'); banner.className = 'cookie-consent'; banner.innerHTML = ` `; banner.style.cssText = ` position: fixed; bottom: 0; left: 0; right: 0; background: rgba(0, 0, 0, 0.9); color: white; padding: 1rem; z-index: 9999; transform: translateY(100%); transition: transform 0.3s ease; `; document.body.appendChild(banner); setTimeout(() => { banner.style.transform = 'translateY(0)'; }, 100); document.getElementById('accept-cookies').addEventListener('click', () => { localStorage.setItem('cookieConsent', 'accepted'); banner.style.transform = 'translateY(100%)'; setTimeout(() => banner.remove(), 300); }); document.getElementById('decline-cookies').addEventListener('click', () => { localStorage.setItem('cookieConsent', 'declined'); banner.style.transform = 'translateY(100%)'; setTimeout(() => banner.remove(), 300); }); } // Initialize cookie consent // initCookieConsent(); // Parallax Effect const parallaxElements = document.querySelectorAll('.parallax'); function updateParallax() { const scrollY = window.pageYOffset; parallaxElements.forEach(element => { const speed = element.dataset.speed || 0.5; const yPos = -(scrollY * speed); element.style.transform = `translateY(${yPos}px)`; }); } if (parallaxElements.length > 0) { window.addEventListener('scroll', updateParallax); } // Counter Animation const counters = document.querySelectorAll('.counter'); const counterObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { animateCounter(entry.target); counterObserver.unobserve(entry.target); } }); }); counters.forEach(counter => counterObserver.observe(counter)); function animateCounter(element) { const target = parseInt(element.dataset.count); const duration = 2000; const start = performance.now(); function updateCounter(currentTime) { const elapsed = currentTime - start; const progress = Math.min(elapsed / duration, 1); const current = Math.floor(progress * target); element.textContent = current.toLocaleString(); if (progress < 1) { requestAnimationFrame(updateCounter); } } requestAnimationFrame(updateCounter); } // Typing Effect for Hero Text const typingElements = document.querySelectorAll('.typing-effect'); typingElements.forEach(element => { const text = element.textContent; element.textContent = ''; element.style.borderRight = '2px solid'; let i = 0; const timer = setInterval(() => { element.textContent += text[i]; i++; if (i >= text.length) { clearInterval(timer); element.style.borderRight = 'none'; } }, 100); }); // Handle form validation const forms = document.querySelectorAll('form[data-validate]'); forms.forEach(form => { form.addEventListener('submit', function(e) { if (!validateForm(this)) { e.preventDefault(); } }); // Real-time validation const inputs = form.querySelectorAll('input, textarea'); inputs.forEach(input => { input.addEventListener('blur', () => validateField(input)); input.addEventListener('input', () => clearFieldError(input)); }); }); function validateForm(form) { let isValid = true; const inputs = form.querySelectorAll('input[required], textarea[required]'); inputs.forEach(input => { if (!validateField(input)) { isValid = false; } }); return isValid; } function validateField(field) { const value = field.value.trim(); const type = field.type; let isValid = true; let message = ''; // Required field check if (field.hasAttribute('required') && !value) { isValid = false; message = '이 필드는 필수입니다.'; } // Email validation if (type === 'email' && value) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(value)) { isValid = false; message = '올바른 이메일 형식을 입력해주세요.'; } } // Phone validation if (type === 'tel' && value) { const phoneRegex = /^[0-9-+\s()]+$/; if (!phoneRegex.test(value)) { isValid = false; message = '올바른 전화번호 형식을 입력해주세요.'; } } // Show/hide error if (!isValid) { showFieldError(field, message); } else { clearFieldError(field); } return isValid; } function showFieldError(field, message) { clearFieldError(field); field.classList.add('error'); const errorDiv = document.createElement('div'); errorDiv.className = 'field-error'; errorDiv.textContent = message; errorDiv.style.cssText = 'color: #ef4444; font-size: 0.875rem; margin-top: 0.25rem;'; field.parentNode.appendChild(errorDiv); } function clearFieldError(field) { field.classList.remove('error'); const errorDiv = field.parentNode.querySelector('.field-error'); if (errorDiv) { errorDiv.remove(); } } }); // Utility Functions const utils = { // Debounce function debounce: function(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; }, // Throttle function throttle: function(func, limit) { let inThrottle; return function() { const args = arguments; const context = this; if (!inThrottle) { func.apply(context, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; }, // Format currency formatCurrency: function(amount, currency = 'KRW') { return new Intl.NumberFormat('ko-KR', { style: 'currency', currency: currency, minimumFractionDigits: 0 }).format(amount); }, // Format date formatDate: function(date, options = {}) { const defaultOptions = { year: 'numeric', month: 'long', day: 'numeric' }; return new Intl.DateTimeFormat('ko-KR', { ...defaultOptions, ...options }).format(new Date(date)); } }; // Global error handler window.addEventListener('error', function(e) { console.error('Global error:', e.error); // Could send error to analytics service }); // Unhandled promise rejection handler window.addEventListener('unhandledrejection', function(e) { console.error('Unhandled promise rejection:', e.reason); // Could send error to analytics service }); // Export for use in other modules if (typeof module !== 'undefined' && module.exports) { module.exports = utils; }