640 lines
22 KiB
JavaScript
640 lines
22 KiB
JavaScript
// 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
|
||
? '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />'
|
||
: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />';
|
||
}
|
||
|
||
// 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 = `
|
||
<div class="notification-content">
|
||
<span class="notification-message">${message}</span>
|
||
<button class="notification-close" aria-label="Close notification">
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
`;
|
||
|
||
// 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 = `
|
||
<div class="cookie-content">
|
||
<p>이 웹사이트는 더 나은 서비스 제공을 위해 쿠키를 사용합니다.</p>
|
||
<div class="cookie-buttons">
|
||
<button id="accept-cookies" class="btn btn-primary">동의</button>
|
||
<button id="decline-cookies" class="btn btn-secondary">거부</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
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;
|
||
} |