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 = `
-
- `;
+ 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 = `
@@ -62,11 +68,13 @@ function initLightbox() {
`;
-
+
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 @@