Files
sst_site/public/js/main.js
2025-10-26 14:44:10 +09:00

640 lines
22 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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;
}