diff --git a/about.html b/about.html index fbb0e1d..9c01d48 100644 --- a/about.html +++ b/about.html @@ -298,7 +298,7 @@ diff --git a/components/footer.html b/components/footer.html index 71a383b..55435a8 100644 --- a/components/footer.html +++ b/components/footer.html @@ -27,7 +27,7 @@ \ No newline at end of file diff --git a/contact.html b/contact.html index c470064..930eb9e 100644 --- a/contact.html +++ b/contact.html @@ -226,7 +226,7 @@ diff --git a/css/about.css b/css/about.css index 3fe9085..0ffcb0d 100644 --- a/css/about.css +++ b/css/about.css @@ -326,10 +326,6 @@ /* ======================================== 다크모드 ======================================== */ -[data-theme="dark"] .page-header { - background: var(--dark-header-gradient); -} - [data-theme="dark"] .info-grid { background: var(--dark-surface); border: 1px solid var(--glass-border); diff --git a/css/common.css b/css/common.css index ef411db..4639137 100644 --- a/css/common.css +++ b/css/common.css @@ -412,13 +412,6 @@ body { gap: var(--spacing-md); } -.footer-bottom { - text-align: center; - padding-top: var(--spacing-xl); - border-top: 1px solid rgba(255, 255, 255, 0.1); - color: rgba(255, 255, 255, 0.6); -} - /* 백업/기본 푸터 */ .site-footer { background: var(--text-primary); @@ -1501,7 +1494,7 @@ body { .streamer-card { background: var(--bg-white); - border-radius: var(--radius-xl); + border-radius: var(--border-radius-xl); box-shadow: var(--shadow-md); padding: var(--spacing-2xl); text-align: center; @@ -1577,7 +1570,7 @@ body { align-items: center; gap: 6px; padding: 8px 16px; - border-radius: var(--radius-lg); + border-radius: var(--border-radius-lg); font-size: var(--font-sm); font-weight: 500; text-decoration: none; @@ -1686,7 +1679,7 @@ body { flex-shrink: 0; display: flex; flex-direction: column; - border-radius: var(--radius-lg); + border-radius: var(--border-radius-lg); overflow: hidden; background: var(--bg-light); box-shadow: var(--shadow-sm); diff --git a/css/contact.css b/css/contact.css index dd2cb43..0419daa 100644 --- a/css/contact.css +++ b/css/contact.css @@ -507,10 +507,6 @@ /* ======================================== 다크모드 ======================================== */ -[data-theme="dark"] .page-header { - background: var(--dark-header-gradient); -} - [data-theme="dark"] .contact-card { background: rgba(255, 255, 255, 0.04); backdrop-filter: blur(20px); diff --git a/css/gallery.css b/css/gallery.css index f553892..cb91777 100644 --- a/css/gallery.css +++ b/css/gallery.css @@ -929,10 +929,6 @@ /* ======================================== 다크모드 ======================================== */ -[data-theme="dark"] .page-header { - background: var(--dark-header-gradient); -} - [data-theme="dark"] .gallery-grid { background: transparent; } diff --git a/css/portfolio.css b/css/portfolio.css index 661236b..1d4ce82 100644 --- a/css/portfolio.css +++ b/css/portfolio.css @@ -817,10 +817,6 @@ /* ======================================== 다크모드 ======================================== */ -[data-theme="dark"] .page-header { - background: var(--dark-header-gradient); -} - [data-theme="dark"] .tab-btn { color: var(--dark-text-secondary); border-color: var(--glass-border); diff --git a/css/qna.css b/css/qna.css index fd2e2fd..66b55ff 100644 --- a/css/qna.css +++ b/css/qna.css @@ -419,10 +419,6 @@ /* ======================================== 다크모드 ======================================== */ -[data-theme="dark"] .page-header { - background: var(--dark-header-gradient); -} - [data-theme="dark"] .faq-search { background: var(--dark-surface); border: 1px solid var(--glass-border); diff --git a/css/services.css b/css/services.css index 8bb45c4..11b8c5e 100644 --- a/css/services.css +++ b/css/services.css @@ -2127,10 +2127,6 @@ /* ======================================== 다크모드 ======================================== */ -[data-theme="dark"] .page-header { - background: var(--dark-header-gradient); -} - [data-theme="dark"] .service-package { background-color: var(--dark-surface); border: 1px solid var(--glass-border); diff --git a/gallery.html b/gallery.html index 53722e9..c939d1a 100644 --- a/gallery.html +++ b/gallery.html @@ -215,7 +215,7 @@ diff --git a/images/sign/구슬요.webp b/images/sign/구슬요.webp new file mode 100644 index 0000000..b2269d3 Binary files /dev/null and b/images/sign/구슬요.webp differ diff --git a/images/sign/얌하.webp b/images/sign/얌하.webp new file mode 100644 index 0000000..fd5c674 Binary files /dev/null and b/images/sign/얌하.webp differ diff --git a/images/studio/고품질 녹음을 위한 음향 시스템.webp b/images/studio/고품질 녹음을 위한 음향 시스템.webp index 061a1c8..58792de 100644 Binary files a/images/studio/고품질 녹음을 위한 음향 시스템.webp and b/images/studio/고품질 녹음을 위한 음향 시스템.webp differ diff --git a/images/studio/넓은 모션 캡쳐 공간 002.webp b/images/studio/넓은 모션 캡쳐 공간 002.webp index 5a0b877..4c52363 100644 Binary files a/images/studio/넓은 모션 캡쳐 공간 002.webp and b/images/studio/넓은 모션 캡쳐 공간 002.webp differ diff --git a/images/studio/모션캡쳐 공간 001.webp b/images/studio/모션캡쳐 공간 001.webp index 83cc782..e2237a6 100644 Binary files a/images/studio/모션캡쳐 공간 001.webp and b/images/studio/모션캡쳐 공간 001.webp differ diff --git a/images/studio/모션캡쳐 공간 002.webp b/images/studio/모션캡쳐 공간 002.webp index 5b3f671..4290250 100644 Binary files a/images/studio/모션캡쳐 공간 002.webp and b/images/studio/모션캡쳐 공간 002.webp differ diff --git a/images/studio/모션캡쳐 공간 003.webp b/images/studio/모션캡쳐 공간 003.webp index db64624..d37b55b 100644 Binary files a/images/studio/모션캡쳐 공간 003.webp and b/images/studio/모션캡쳐 공간 003.webp differ diff --git a/images/studio/모션캡쳐 공간 004.webp b/images/studio/모션캡쳐 공간 004.webp index ba3475b..74ac780 100644 Binary files a/images/studio/모션캡쳐 공간 004.webp and b/images/studio/모션캡쳐 공간 004.webp differ diff --git a/images/studio/스튜디오와 연결된 파우더룸.webp b/images/studio/스튜디오와 연결된 파우더룸.webp index 1235c0a..37affb0 100644 Binary files a/images/studio/스튜디오와 연결된 파우더룸.webp and b/images/studio/스튜디오와 연결된 파우더룸.webp differ diff --git a/images/studio/오퍼레이팅 공간 강조.webp b/images/studio/오퍼레이팅 공간 강조.webp index de5330b..da9ba0c 100644 Binary files a/images/studio/오퍼레이팅 공간 강조.webp and b/images/studio/오퍼레이팅 공간 강조.webp differ diff --git a/images/studio/커튼 걷은 360 이미지.webp b/images/studio/커튼 걷은 360 이미지.webp index 28fcf9e..9942b23 100644 Binary files a/images/studio/커튼 걷은 360 이미지.webp and b/images/studio/커튼 걷은 360 이미지.webp differ diff --git a/images/studio/커튼을 친 외부 전경.webp b/images/studio/커튼을 친 외부 전경.webp index 602b7f7..f219ebf 100644 Binary files a/images/studio/커튼을 친 외부 전경.webp and b/images/studio/커튼을 친 외부 전경.webp differ diff --git a/images/studio/커튼친 360 이미지.webp b/images/studio/커튼친 360 이미지.webp index 23086b4..01ba3c3 100644 Binary files a/images/studio/커튼친 360 이미지.webp and b/images/studio/커튼친 360 이미지.webp differ diff --git a/images/studio/탈의실 내부 공간.webp b/images/studio/탈의실 내부 공간.webp index 2b384b1..3a98d65 100644 Binary files a/images/studio/탈의실 내부 공간.webp and b/images/studio/탈의실 내부 공간.webp differ diff --git a/images/studio/탈의실 외부 공간.webp b/images/studio/탈의실 외부 공간.webp index 1a56f36..5fab3eb 100644 Binary files a/images/studio/탈의실 외부 공간.webp and b/images/studio/탈의실 외부 공간.webp differ diff --git a/images/studio/프라이핏한 모션 캡쳐 공간.webp b/images/studio/프라이핏한 모션 캡쳐 공간.webp index 1d5a0f3..f3714f7 100644 Binary files a/images/studio/프라이핏한 모션 캡쳐 공간.webp and b/images/studio/프라이핏한 모션 캡쳐 공간.webp differ diff --git a/index.html b/index.html index f9ca801..5e74df5 100644 --- a/index.html +++ b/index.html @@ -686,39 +686,45 @@
- -
김마늘 사인김마늘
-
만타 사인만타
-
문모모 사인문모모
-
베니 사인베니
-
시에 사인시에
-
요나카 사인요나카
-
이무지 사인이무지
-
지한이또 사인지한이또
-
최또 사인최또
-
치요 사인치요
+ +
김마늘 사인김마늘
+
만타 사인만타
+
문모모 사인문모모
+
베니 사인베니
+
시에 사인시에
+
얌하 사인얌하
+
요나카 사인요나카
+
이무지 사인이무지
+
구슬요 사인구슬요
+
지한이또 사인지한이또
+
최또 사인최또
+
치요 사인치요
-
김마늘 사인김마늘
-
만타 사인만타
-
문모모 사인문모모
-
베니 사인베니
-
시에 사인시에
-
요나카 사인요나카
-
이무지 사인이무지
-
지한이또 사인지한이또
-
최또 사인최또
-
치요 사인치요
+
김마늘 사인김마늘
+
만타 사인만타
+
문모모 사인문모모
+
베니 사인베니
+
시에 사인시에
+
얌하 사인얌하
+
요나카 사인요나카
+
이무지 사인이무지
+
구슬요 사인구슬요
+
지한이또 사인지한이또
+
최또 사인최또
+
치요 사인치요
-
김마늘 사인김마늘
-
만타 사인만타
-
문모모 사인문모모
-
베니 사인베니
-
시에 사인시에
-
요나카 사인요나카
-
이무지 사인이무지
-
지한이또 사인지한이또
-
최또 사인최또
-
치요 사인치요
+
김마늘 사인김마늘
+
만타 사인만타
+
문모모 사인문모모
+
베니 사인베니
+
시에 사인시에
+
얌하 사인얌하
+
요나카 사인요나카
+
이무지 사인이무지
+
구슬요 사인구슬요
+
지한이또 사인지한이또
+
최또 사인최또
+
치요 사인치요
@@ -735,13 +741,13 @@
-
-
+
+
-
-
-
-
+
+
+
+
@@ -839,7 +845,7 @@ diff --git a/js/backgrounds.js b/js/backgrounds.js index a14b8e7..c8b450c 100644 --- a/js/backgrounds.js +++ b/js/backgrounds.js @@ -107,6 +107,8 @@ * 이벤트 리스너 설정 */ function setupEventListeners() { + if (!elements.searchInput || !elements.imageModal || !elements.filterTags) return; + // 검색 elements.searchInput.addEventListener('input', debounce((e) => { searchQuery = e.target.value.toLowerCase(); diff --git a/js/common.js b/js/common.js index f226096..dd20ca3 100644 --- a/js/common.js +++ b/js/common.js @@ -35,12 +35,14 @@ async function loadComponents() { if (headerPlaceholder) { try { const response = await fetch('components/header.html'); + if (!response.ok) throw new Error(`HTTP ${response.status}`); const html = await response.text(); headerPlaceholder.innerHTML = html; initializeNavigation(); // 헤더 로드 후 네비게이션 초기화 initThemeToggle(); // 테마 토글 초기화 } catch (error) { console.error('Error loading header:', error); + headerPlaceholder.innerHTML = ''; } } @@ -49,11 +51,18 @@ async function loadComponents() { if (footerPlaceholder) { try { const response = await fetch('components/footer.html'); + if (!response.ok) throw new Error(`HTTP ${response.status}`); const html = await response.text(); footerPlaceholder.innerHTML = html; - + + // 동적 푸터의 저작권 연도 자동 업데이트 + const footerYear = footerPlaceholder.querySelector('.footer-bottom p'); + if (footerYear) { + footerYear.innerHTML = `© ${new Date().getFullYear()} 밍글 스튜디오. All rights reserved.`; + } + // 동적 푸터 로드 성공 시 백업 푸터 숨기기 - const backupFooter = document.querySelector('footer.site-footer'); + const backupFooter = document.querySelector('footer.site-footer#backupFooter'); if (backupFooter) { backupFooter.style.display = 'none'; } @@ -67,11 +76,15 @@ async function loadComponents() { // ======================================== // 네비게이션 기능 // ======================================== +let _navInitialized = false; +let _scrollListenerAdded = false; + function initializeNavigation() { const hamburger = document.querySelector('.hamburger'); const navMenu = document.querySelector('.nav-menu'); - - if (hamburger && navMenu) { + + if (hamburger && navMenu && !hamburger._listenerAdded) { + hamburger._listenerAdded = true; hamburger.addEventListener('click', function() { const isActive = hamburger.classList.toggle('active'); navMenu.classList.toggle('active'); @@ -90,20 +103,23 @@ function initializeNavigation() { }); } - // 스크롤 시 네비게이션 바 스타일 변경 (RAF 최적화) - let scrollTicking = false; - const navbar = document.querySelector('.navbar'); - window.addEventListener('scroll', () => { - if (!scrollTicking) { - requestAnimationFrame(() => { - if (navbar) { - navbar.classList.toggle('scrolled', window.pageYOffset > 20); - } - scrollTicking = false; - }); - scrollTicking = true; - } - }); + // 스크롤 시 네비게이션 바 스타일 변경 (RAF 최적화) - 한 번만 등록 + if (!_scrollListenerAdded) { + _scrollListenerAdded = true; + let scrollTicking = false; + window.addEventListener('scroll', () => { + if (!scrollTicking) { + requestAnimationFrame(() => { + const navbar = document.querySelector('.navbar'); + if (navbar) { + navbar.classList.toggle('scrolled', window.pageYOffset > 20); + } + scrollTicking = false; + }); + scrollTicking = true; + } + }, { passive: true }); + } } // ======================================== @@ -292,9 +308,6 @@ function initLazyLoading() { } } -// ======================================== -// Export 함수들 (다른 스크립트에서 사용 가능) -// ======================================== // ======================================== // 로딩 상태 관리 // ======================================== @@ -327,12 +340,17 @@ function hidePageLoading() { } function showComponentLoading(element, text = '로딩 중...') { - element.innerHTML = ` -
-
-
${text}
-
- `; + const wrapper = document.createElement('div'); + wrapper.className = 'component-loading'; + const spinner = document.createElement('div'); + spinner.className = 'loading-spinner'; + const label = document.createElement('div'); + label.className = 'loading-text'; + label.textContent = text; + wrapper.appendChild(spinner); + wrapper.appendChild(label); + element.innerHTML = ''; + element.appendChild(wrapper); } function hideComponentLoading(element, content) { diff --git a/js/contact.js b/js/contact.js index cb99a5e..1bda6d4 100644 --- a/js/contact.js +++ b/js/contact.js @@ -79,16 +79,18 @@ async function handleFormSubmit(e) { // 서버 전송 (mailto 기반 폴백) async function submitContactForm(data) { - // mailto 링크로 이메일 클라이언트 열기 - const subject = encodeURIComponent(`[밍글 스튜디오 문의] ${data.name || '웹사이트 문의'}`); + // 입력값에서 줄바꿈/캐리지리턴 제거 (이메일 헤더 인젝션 방지) + const sanitize = (str) => (str || '').replace(/[\r\n]/g, ' ').trim(); + + const subject = encodeURIComponent(`[밍글 스튜디오 문의] ${sanitize(data.name) || '웹사이트 문의'}`); const body = encodeURIComponent( - `이름: ${data.name || ''}\n` + - `이메일: ${data.email || ''}\n` + - `전화번호: ${data.phone || ''}\n` + - `문의 유형: ${data.service || ''}\n` + - `\n문의 내용:\n${data.message || ''}` + `이름: ${sanitize(data.name)}\n` + + `이메일: ${sanitize(data.email)}\n` + + `전화번호: ${sanitize(data.phone)}\n` + + `문의 유형: ${sanitize(data.service)}\n` + + `\n문의 내용:\n${(data.message || '').trim()}` ); - window.location.href = `mailto:mingle_studio@naver.com?subject=${subject}&body=${body}`; + window.location.href = `mailto:help@minglestudio.co.kr?subject=${subject}&body=${body}`; return { success: true, message: '이메일 클라이언트가 열렸습니다.' }; } @@ -206,7 +208,12 @@ function isValidPhone(phone) { // 전화번호 자동 포맷팅 function formatPhoneNumber(e) { let value = e.target.value.replace(/[^0-9]/g, ''); - + + // 최대 11자리 제한 (한국 휴대폰 번호) + if (value.length > 11) { + value = value.slice(0, 11); + } + if (value.length >= 11) { // 01X-XXXX-XXXX 형태 value = value.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3'); @@ -217,7 +224,7 @@ function formatPhoneNumber(e) { // 01X-XXX 형태 value = value.replace(/(\d{3})(\d{0,4})/, '$1-$2'); } - + e.target.value = value; } diff --git a/js/gallery.js b/js/gallery.js index d0df107..8d12e7e 100644 --- a/js/gallery.js +++ b/js/gallery.js @@ -12,44 +12,50 @@ document.addEventListener('DOMContentLoaded', function() { // 갤러리 초기화 function initGallery() { const galleryItems = document.querySelectorAll('.gallery-item'); - + + // 레이지 로딩용 옵저버를 하나만 생성 (이미지마다 생성하지 않음) + let imageObserver = null; + if ('IntersectionObserver' in window) { + imageObserver = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + const img = entry.target; + img.src = img.dataset.src || img.src; + img.classList.remove('lazy'); + imageObserver.unobserve(img); + } + }); + }); + } + galleryItems.forEach((item, index) => { const img = item.querySelector('.gallery-img'); - + if (!img) return; + // 이미지 클릭 시 라이트박스 열기 img.addEventListener('click', () => openLightbox(index)); - + // 이미지 로딩 에러 처리 img.addEventListener('error', function() { this.src = 'images/placeholder.jpg'; this.alt = '이미지를 불러올 수 없습니다'; }); - + // 레이지 로딩 구현 - if ('IntersectionObserver' in window) { - const imageObserver = new IntersectionObserver((entries) => { - entries.forEach(entry => { - if (entry.isIntersecting) { - const img = entry.target; - img.src = img.dataset.src || img.src; - img.classList.remove('lazy'); - imageObserver.unobserve(img); - } - }); - }); - - if (img.dataset.src) { - imageObserver.observe(img); - } + if (imageObserver && img.dataset.src) { + imageObserver.observe(img); } }); } // 라이트박스 기능 let currentImageIndex = 0; -const galleryImages = document.querySelectorAll('.gallery-img'); +let galleryImages = []; function initLightbox() { + // DOM 준비 후 갤러리 이미지 수집 + galleryImages = document.querySelectorAll('.gallery-img'); + // 라이트박스 HTML 생성 const lightboxHTML = ` `; - + document.body.insertAdjacentHTML('beforeend', lightboxHTML); - - // ESC 키로 라이트박스 닫기 + + // ESC 키로 라이트박스 닫기 (라이트박스가 열려 있을 때만) document.addEventListener('keydown', function(e) { + const lightbox = document.getElementById('lightbox'); + if (!lightbox || !lightbox.classList.contains('active')) return; if (e.key === 'Escape') closeLightbox(); if (e.key === 'ArrowLeft') previousImage(); if (e.key === 'ArrowRight') nextImage(); @@ -158,143 +166,45 @@ function initGalleryAnimations() { }); } -// 갤러리 필터 기능 (향후 확장용) -function initGalleryFilters() { - const filterButtons = document.querySelectorAll('.filter-btn'); - const galleryItems = document.querySelectorAll('.gallery-item'); - - filterButtons.forEach(btn => { - btn.addEventListener('click', function() { - // 활성 버튼 업데이트 - filterButtons.forEach(b => b.classList.remove('active')); - this.classList.add('active'); - - const filter = this.dataset.filter; - - galleryItems.forEach(item => { - if (filter === 'all' || item.dataset.category === filter) { - item.style.display = 'block'; - setTimeout(() => { - item.style.opacity = '1'; - item.style.transform = 'scale(1)'; - }, 10); - } else { - item.style.opacity = '0'; - item.style.transform = 'scale(0.8)'; - setTimeout(() => { - item.style.display = 'none'; - }, 300); - } - }); - }); - }); -} - -// 이미지 프리로딩 -function preloadImages() { - galleryImages.forEach(img => { - const imagePreload = new Image(); - imagePreload.src = img.src; - }); -} - -// 갤러리 검색 기능 (향후 확장용) -function initGallerySearch() { - const searchInput = document.getElementById('gallery-search'); - const galleryItems = document.querySelectorAll('.gallery-item'); - - if (searchInput) { - searchInput.addEventListener('input', function() { - const searchTerm = this.value.toLowerCase(); - - galleryItems.forEach(item => { - const caption = item.querySelector('.gallery-caption'); - const alt = item.querySelector('.gallery-img').alt; - const text = (caption ? caption.textContent : '') + ' ' + alt; - - if (text.toLowerCase().includes(searchTerm)) { - item.style.display = 'block'; - } else { - item.style.display = 'none'; - } - }); - }); - } -} - -// 이미지 로딩 상태 표시 -function showGalleryLoading() { - const loading = document.querySelector('.gallery-loading'); - if (loading) { - loading.style.display = 'block'; - } -} - -function hideGalleryLoading() { - const loading = document.querySelector('.gallery-loading'); - if (loading) { - loading.style.display = 'none'; - } -} - -// 갤러리 그리드 리사이즈 최적화 -let resizeTimeout; -window.addEventListener('resize', function() { - clearTimeout(resizeTimeout); - resizeTimeout = setTimeout(function() { - // 갤러리 그리드 재조정 로직 - const galleryGrid = document.querySelector('.gallery-grid'); - if (galleryGrid) { - galleryGrid.style.opacity = '0.8'; - setTimeout(() => { - galleryGrid.style.opacity = '1'; - }, 100); - } - }, 250); -}); - // 터치 이벤트 지원 (모바일) - 라이트박스에만 스코프 제한 -let touchStartX = 0; -let touchEndX = 0; +// 리스너는 한 번만 등록하고, 라이트박스 활성 상태에서만 동작 +let lightboxTouchStartX = 0; +let lightboxTouchListenersAdded = false; -function handleLightboxTouchStart(e) { - touchStartX = e.changedTouches[0].screenX; -} +function setupLightboxTouchListeners() { + if (lightboxTouchListenersAdded) return; + const lightbox = document.getElementById('lightbox'); + if (!lightbox) return; + lightboxTouchListenersAdded = true; -function handleLightboxTouchEnd(e) { - touchEndX = e.changedTouches[0].screenX; - const swipeThreshold = 50; - const diff = touchStartX - touchEndX; + lightbox.addEventListener('touchstart', function(e) { + if (!lightbox.classList.contains('active')) return; + lightboxTouchStartX = e.changedTouches[0].screenX; + }); - if (Math.abs(diff) > swipeThreshold) { - if (diff > 0) { - nextImage(); - } else { - previousImage(); + lightbox.addEventListener('touchend', function(e) { + if (!lightbox.classList.contains('active')) return; + const touchEndX = e.changedTouches[0].screenX; + const swipeThreshold = 50; + const diff = lightboxTouchStartX - touchEndX; + + if (Math.abs(diff) > swipeThreshold) { + if (diff > 0) { + nextImage(); + } else { + previousImage(); + } } - } + }); } -// openLightbox/closeLightbox에서 터치 리스너 관리를 위해 원본 함수 래핑 +// openLightbox 래핑: 터치 리스너 초기화 보장 const _originalOpenLightbox = openLightbox; openLightbox = function(index) { _originalOpenLightbox(index); - const lightbox = document.getElementById('lightbox'); - if (lightbox) { - lightbox.addEventListener('touchstart', handleLightboxTouchStart); - lightbox.addEventListener('touchend', handleLightboxTouchEnd); - } + setupLightboxTouchListeners(); }; -const _originalCloseLightbox = closeLightbox; -closeLightbox = function() { - const lightbox = document.getElementById('lightbox'); - if (lightbox) { - lightbox.removeEventListener('touchstart', handleLightboxTouchStart); - lightbox.removeEventListener('touchend', handleLightboxTouchEnd); - } - _originalCloseLightbox(); -}; // ======================================== // 간단한 360도 파노라마 뷰어 - 좌우 스크롤 방식 @@ -378,8 +288,7 @@ class Easy360Viewer { user-select: none; pointer-events: none; display: block; - image-rendering: -webkit-optimize-contrast; - image-rendering: crisp-edges; + image-rendering: auto; filter: contrast(1.05) saturate(1.1); `; this.image.draggable = false; @@ -408,18 +317,23 @@ class Easy360Viewer { // 컨테이너 크기 가져오기 const containerHeight = this.container.clientHeight; const containerWidth = this.container.clientWidth; - const imageAspectRatio = this.image.naturalWidth / this.image.naturalHeight; + const naturalW = this.image.naturalWidth; + const naturalH = this.image.naturalHeight; + const imageAspectRatio = naturalW / naturalH; - // 모바일에서 줌 시 경계 문제 해결을 위한 더 정확한 크기 계산 - let imageHeight = containerHeight * this.zoom; + // 컨테이너 높이에 맞추되, 원본 해상도를 초과하지 않도록 제한 + let imageHeight = Math.min(containerHeight * this.zoom, naturalH); let imageWidth = imageHeight * imageAspectRatio; - // 360도 이미지는 매우 가로가 길므로 최소 크기 보장 - const minWidth = Math.max(containerWidth * 3, imageWidth); - imageWidth = minWidth; - imageHeight = imageWidth / imageAspectRatio; + // 최소 너비 보장: 컨테이너 1.5배 (3배까지 늘리면 흐릿해짐) + // 단, 원본 너비를 초과하지 않도록 제한 + const minWidth = Math.min(containerWidth * 1.5, naturalW); + if (imageWidth < minWidth) { + imageWidth = minWidth; + imageHeight = imageWidth / imageAspectRatio; + } - // 줌 레벨에 따른 크기 조정 (모바일 확대 시 경계 문제 해결) + // 줌 레벨에 따른 크기 조정 if (this.zoom > 1) { imageHeight = Math.max(containerHeight, imageHeight); imageWidth = imageHeight * imageAspectRatio; diff --git a/js/main.js b/js/main.js index d308e93..0f53096 100644 --- a/js/main.js +++ b/js/main.js @@ -81,7 +81,7 @@ function initParallaxImages() { }); ticking = true; } - }); + }, { passive: true }); } // ======================================== @@ -107,7 +107,7 @@ function initHeroScrollFade() { }); ticking = true; } - }); + }, { passive: true }); } // ======================================== @@ -315,8 +315,9 @@ function initPortfolioTabs() { activePanel.classList.add('active'); // 탭 전환 시 lazy loading 트리거 activePanel.querySelectorAll('iframe[data-src]').forEach(iframe => { - if (!iframe.src) { + if (iframe.dataset.src) { iframe.src = iframe.dataset.src; + iframe.removeAttribute('data-src'); } }); } diff --git a/js/popup.js b/js/popup.js index 824cf0e..3372dbf 100644 --- a/js/popup.js +++ b/js/popup.js @@ -80,7 +80,8 @@ document.addEventListener('DOMContentLoaded', function() { // ESC 키로 닫기 document.addEventListener('keydown', function(e) { - if (e.key === 'Escape') { + const popup = document.getElementById('mainPopup'); + if (e.key === 'Escape' && popup && popup.classList.contains('active')) { closePopup(); } }); diff --git a/js/portfolio.js b/js/portfolio.js index 6c7c611..477720f 100644 --- a/js/portfolio.js +++ b/js/portfolio.js @@ -322,7 +322,9 @@ function initYouTubePlayers() { players.push(player); } }); - + + // pauseOtherVideos에서 사용할 수 있도록 전역에 저장 + window.youtubePlayers = players; return players; } @@ -335,7 +337,8 @@ function extractVideoId(url) { // 플레이어 준비 완료 function onPlayerReady(event) { // 플레이어 컨테이너에 로딩 완료 클래스 추가 - const playerElement = event.target.getIframe(); + const playerElement = event.target.getIframe?.(); + if (!playerElement) return; const wrapper = playerElement.closest('.video-wrapper'); if (wrapper) { wrapper.classList.add('loaded'); @@ -348,7 +351,8 @@ function onPlayerReady(event) { // 플레이어 상태 변경 function onPlayerStateChange(event) { - const playerElement = event.target.getIframe(); + const playerElement = event.target.getIframe?.(); + if (!playerElement) return; const videoCard = playerElement.closest('.video-card'); if (event.data === YT.PlayerState.PLAYING) { @@ -374,13 +378,19 @@ function onPlayerStateChange(event) { // 플레이어 오류 처리 function onPlayerError(event) { console.error('YouTube player error:', event.data); - const playerElement = event.target.getIframe(); + const playerElement = event.target.getIframe?.(); + if (!playerElement) return; const wrapper = playerElement.closest('.video-wrapper'); if (wrapper) { const errorDiv = document.createElement('div'); errorDiv.className = 'video-error'; - errorDiv.innerHTML = '비디오를 로드할 수 없습니다
네트워크 연결을 확인해주세요'; + const mainText = document.createElement('div'); + mainText.textContent = '비디오를 로드할 수 없습니다'; + const subText = document.createElement('small'); + subText.textContent = '네트워크 연결을 확인해주세요'; + errorDiv.appendChild(mainText); + errorDiv.appendChild(subText); errorDiv.style.cssText = ` position: absolute; top: 50%; diff --git a/js/props.js b/js/props.js index 9617e92..7b93d49 100644 --- a/js/props.js +++ b/js/props.js @@ -74,6 +74,8 @@ * 이벤트 리스너 설정 */ function setupEventListeners() { + if (!elements.searchInput || !elements.imageModal) return; + // 검색 elements.searchInput.addEventListener('input', debounce((e) => { searchQuery = e.target.value.toLowerCase(); diff --git a/js/qna.js b/js/qna.js index 5aac285..ef92105 100644 --- a/js/qna.js +++ b/js/qna.js @@ -143,14 +143,16 @@ function handleSearch(query) { return; } - const searchRegex = new RegExp(query, 'gi'); - + // 검색어 이스케이프 및 g 플래그 없이 생성 (.test()의 lastIndex 문제 방지) + const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const searchRegex = new RegExp(escapedQuery, 'i'); + faqItems.forEach(item => { const question = item.querySelector('.faq-question h3'); const answer = item.querySelector('.faq-answer'); const questionText = question.textContent; const answerText = answer.textContent; - + // 검색어 매칭 확인 const questionMatch = searchRegex.test(questionText); const answerMatch = searchRegex.test(answerText); @@ -162,8 +164,8 @@ function handleSearch(query) { // 검색어 하이라이트 highlightSearchTerm(item, query); - // 답변에 매칭되는 경우 자동으로 열기 - if (answerMatch && !questionMatch) { + // 답변에 매칭되는 경우 자동으로 열기 (이미 열려 있으면 무시) + if (answerMatch && !questionMatch && !item.classList.contains('active')) { toggleFAQ(item); } } else { @@ -303,7 +305,6 @@ function hideSuggestions() { // 검색 결과 없음 표시 function showNoResults(query) { - const safeQuery = query.replace(/[<>&"]/g, c => ({'<':'<','>':'>','&':'&','"':'"'})[c]); let noResults = document.querySelector('.no-results'); if (!noResults) { noResults = document.createElement('div'); @@ -311,13 +312,13 @@ function showNoResults(query) { noResults.innerHTML = `
🔍

검색 결과가 없습니다

-

"${safeQuery}"와 관련된 질문을 찾을 수 없습니다.

+

와 관련된 질문을 찾을 수 없습니다.

다른 키워드로 검색해보시거나 직접 문의해 주세요.

`; document.querySelector('.faq-list').appendChild(noResults); - } else { - noResults.querySelector('p strong').textContent = `"${query}"`; } + // textContent로 안전하게 삽입 (XSS 방지) + noResults.querySelector('p strong').textContent = `"${query}"`; noResults.classList.add('active'); } diff --git a/portfolio.html b/portfolio.html index f2e5682..1ab0e81 100644 --- a/portfolio.html +++ b/portfolio.html @@ -659,7 +659,7 @@ diff --git a/qna.html b/qna.html index 1b5e031..75d39ce 100644 --- a/qna.html +++ b/qna.html @@ -446,7 +446,7 @@ diff --git a/services.html b/services.html index a1dbad9..bcb1a04 100644 --- a/services.html +++ b/services.html @@ -760,7 +760,7 @@