Refactor: JS 버그 수정 23건 + 이미지 최적화 + 크리에이터 사인 추가

- JS 논리 오류 수정: gallery.js lightbox 초기화 타이밍, 터치 리스너 누적, IntersectionObserver 통합
- XSS 방지: qna.js showNoResults innerHTML → textContent, 정규식 이스케이프 추가
- 안전성 개선: popup.js ESC 가드, portfolio.js getIframe optional chaining, backgrounds/props null 가드
- 이미지 최적화: 스튜디오 12장 WebP 압축 (4.0MB → 2.2MB, 46% 감소)
- 360 이미지: git 히스토리에서 원본 복구 후 4096×2048 리사이즈 (해상도 4.6배 향상)
- 360 뷰어: image-rendering auto 전환, naturalWidth/Height 기반 렌더링으로 품질 개선
- 크리에이터 사인 추가: 얌하 (3.3KB), 구슬요 (5.9KB) WebP 변환 및 마키 삽입
- 불필요 코드 제거: gallery.js 미사용 함수 6개 삭제

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
68893236+KINDNICK@users.noreply.github.com 2026-02-24 00:56:48 +09:00
parent 7579dcc735
commit b5008b2f5d
40 changed files with 223 additions and 292 deletions

View File

@ -298,7 +298,7 @@
</div>
</div>
<div class="footer-bottom">
<p>&copy; 2025 밍글 스튜디오. All rights reserved.</p>
<p>&copy; <script>document.write(new Date().getFullYear())</script> 밍글 스튜디오. All rights reserved.</p>
</div>
</div>
</footer>

View File

@ -27,7 +27,7 @@
</div>
</div>
<div class="footer-bottom">
<p>&copy; 2025 밍글 스튜디오. All rights reserved.</p>
<p>&copy; <script>document.write(new Date().getFullYear())</script> 밍글 스튜디오. All rights reserved.</p>
</div>
</div>
</footer>

View File

@ -226,7 +226,7 @@
</div>
</div>
<div class="footer-bottom">
<p>&copy; 2025 밍글 스튜디오. All rights reserved.</p>
<p>&copy; <script>document.write(new Date().getFullYear())</script> 밍글 스튜디오. All rights reserved.</p>
</div>
</div>
</footer>

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -929,10 +929,6 @@
/* ========================================
다크모드
======================================== */
[data-theme="dark"] .page-header {
background: var(--dark-header-gradient);
}
[data-theme="dark"] .gallery-grid {
background: transparent;
}

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -215,7 +215,7 @@
</div>
</div>
<div class="footer-bottom">
<p>&copy; 2025 밍글 스튜디오. All rights reserved.</p>
<p>&copy; <script>document.write(new Date().getFullYear())</script> 밍글 스튜디오. All rights reserved.</p>
</div>
</div>
</footer>

BIN
images/sign/구슬요.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
images/sign/얌하.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 248 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 476 KiB

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 384 KiB

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 382 KiB

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 459 KiB

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

After

Width:  |  Height:  |  Size: 889 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

After

Width:  |  Height:  |  Size: 862 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 503 KiB

After

Width:  |  Height:  |  Size: 238 KiB

View File

@ -686,39 +686,45 @@
<div class="signs-marquee">
<div class="signs-marquee-track">
<!-- 원본 10개 -->
<div class="sign-item"><img src="/images/sign/김마늘.jpg" alt="김마늘 사인"><span class="sign-name">김마늘</span></div>
<div class="sign-item"><img src="/images/sign/만타.jpg" alt="만타 사인"><span class="sign-name">만타</span></div>
<div class="sign-item"><img src="/images/sign/문모모.jpg" alt="문모모 사인"><span class="sign-name">문모모</span></div>
<div class="sign-item"><img src="/images/sign/베니.jpg" alt="베니 사인"><span class="sign-name">베니</span></div>
<div class="sign-item"><img src="/images/sign/시에.jpg" alt="시에 사인"><span class="sign-name">시에</span></div>
<div class="sign-item"><img src="/images/sign/요나카.jpg" alt="요나카 사인"><span class="sign-name">요나카</span></div>
<div class="sign-item"><img src="/images/sign/이무지.jpg" alt="이무지 사인"><span class="sign-name">이무지</span></div>
<div class="sign-item"><img src="/images/sign/지한이또.jpg" alt="지한이또 사인"><span class="sign-name">지한이또</span></div>
<div class="sign-item"><img src="/images/sign/최또.jpg" alt="최또 사인"><span class="sign-name">최또</span></div>
<div class="sign-item"><img src="/images/sign/치요.jpg" alt="치요 사인"><span class="sign-name">치요</span></div>
<!-- 원본 12개 -->
<div class="sign-item"><img loading="lazy" src="/images/sign/김마늘.jpg" alt="김마늘 사인"><span class="sign-name">김마늘</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/만타.jpg" alt="만타 사인"><span class="sign-name">만타</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/문모모.jpg" alt="문모모 사인"><span class="sign-name">문모모</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/베니.jpg" alt="베니 사인"><span class="sign-name">베니</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/시에.jpg" alt="시에 사인"><span class="sign-name">시에</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/얌하.webp" alt="얌하 사인"><span class="sign-name">얌하</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/요나카.jpg" alt="요나카 사인"><span class="sign-name">요나카</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/이무지.jpg" alt="이무지 사인"><span class="sign-name">이무지</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/구슬요.webp" alt="구슬요 사인"><span class="sign-name">구슬요</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/지한이또.jpg" alt="지한이또 사인"><span class="sign-name">지한이또</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/최또.jpg" alt="최또 사인"><span class="sign-name">최또</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/치요.jpg" alt="치요 사인"><span class="sign-name">치요</span></div>
<!-- 복제 1 -->
<div class="sign-item"><img src="/images/sign/김마늘.jpg" alt="김마늘 사인"><span class="sign-name">김마늘</span></div>
<div class="sign-item"><img src="/images/sign/만타.jpg" alt="만타 사인"><span class="sign-name">만타</span></div>
<div class="sign-item"><img src="/images/sign/문모모.jpg" alt="문모모 사인"><span class="sign-name">문모모</span></div>
<div class="sign-item"><img src="/images/sign/베니.jpg" alt="베니 사인"><span class="sign-name">베니</span></div>
<div class="sign-item"><img src="/images/sign/시에.jpg" alt="시에 사인"><span class="sign-name">시에</span></div>
<div class="sign-item"><img src="/images/sign/요나카.jpg" alt="요나카 사인"><span class="sign-name">요나카</span></div>
<div class="sign-item"><img src="/images/sign/이무지.jpg" alt="이무지 사인"><span class="sign-name">이무지</span></div>
<div class="sign-item"><img src="/images/sign/지한이또.jpg" alt="지한이또 사인"><span class="sign-name">지한이또</span></div>
<div class="sign-item"><img src="/images/sign/최또.jpg" alt="최또 사인"><span class="sign-name">최또</span></div>
<div class="sign-item"><img src="/images/sign/치요.jpg" alt="치요 사인"><span class="sign-name">치요</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/김마늘.jpg" alt="김마늘 사인"><span class="sign-name">김마늘</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/만타.jpg" alt="만타 사인"><span class="sign-name">만타</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/문모모.jpg" alt="문모모 사인"><span class="sign-name">문모모</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/베니.jpg" alt="베니 사인"><span class="sign-name">베니</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/시에.jpg" alt="시에 사인"><span class="sign-name">시에</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/얌하.webp" alt="얌하 사인"><span class="sign-name">얌하</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/요나카.jpg" alt="요나카 사인"><span class="sign-name">요나카</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/이무지.jpg" alt="이무지 사인"><span class="sign-name">이무지</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/구슬요.webp" alt="구슬요 사인"><span class="sign-name">구슬요</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/지한이또.jpg" alt="지한이또 사인"><span class="sign-name">지한이또</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/최또.jpg" alt="최또 사인"><span class="sign-name">최또</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/치요.jpg" alt="치요 사인"><span class="sign-name">치요</span></div>
<!-- 복제 2 -->
<div class="sign-item"><img src="/images/sign/김마늘.jpg" alt="김마늘 사인"><span class="sign-name">김마늘</span></div>
<div class="sign-item"><img src="/images/sign/만타.jpg" alt="만타 사인"><span class="sign-name">만타</span></div>
<div class="sign-item"><img src="/images/sign/문모모.jpg" alt="문모모 사인"><span class="sign-name">문모모</span></div>
<div class="sign-item"><img src="/images/sign/베니.jpg" alt="베니 사인"><span class="sign-name">베니</span></div>
<div class="sign-item"><img src="/images/sign/시에.jpg" alt="시에 사인"><span class="sign-name">시에</span></div>
<div class="sign-item"><img src="/images/sign/요나카.jpg" alt="요나카 사인"><span class="sign-name">요나카</span></div>
<div class="sign-item"><img src="/images/sign/이무지.jpg" alt="이무지 사인"><span class="sign-name">이무지</span></div>
<div class="sign-item"><img src="/images/sign/지한이또.jpg" alt="지한이또 사인"><span class="sign-name">지한이또</span></div>
<div class="sign-item"><img src="/images/sign/최또.jpg" alt="최또 사인"><span class="sign-name">최또</span></div>
<div class="sign-item"><img src="/images/sign/치요.jpg" alt="치요 사인"><span class="sign-name">치요</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/김마늘.jpg" alt="김마늘 사인"><span class="sign-name">김마늘</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/만타.jpg" alt="만타 사인"><span class="sign-name">만타</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/문모모.jpg" alt="문모모 사인"><span class="sign-name">문모모</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/베니.jpg" alt="베니 사인"><span class="sign-name">베니</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/시에.jpg" alt="시에 사인"><span class="sign-name">시에</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/얌하.webp" alt="얌하 사인"><span class="sign-name">얌하</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/요나카.jpg" alt="요나카 사인"><span class="sign-name">요나카</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/이무지.jpg" alt="이무지 사인"><span class="sign-name">이무지</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/구슬요.webp" alt="구슬요 사인"><span class="sign-name">구슬요</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/지한이또.jpg" alt="지한이또 사인"><span class="sign-name">지한이또</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/최또.jpg" alt="최또 사인"><span class="sign-name">최또</span></div>
<div class="sign-item"><img loading="lazy" src="/images/sign/치요.jpg" alt="치요 사인"><span class="sign-name">치요</span></div>
</div>
</div>
</div>
@ -735,13 +741,13 @@
<!-- 클라이언트 로고 마키 -->
<div class="partners-marquee">
<div class="partners-marquee-track">
<div class="partner-logo-item"><img src="/images/partners/메가메타.jpg" alt="메가메타 - 밍글 스튜디오 클라이언트" class="partner-logo"></div>
<div class="partner-logo-item"><img src="/images/partners/(주)세광종합기술단.png" alt="(주)세광종합기술단 - 밍글 스튜디오 클라이언트" class="partner-logo"></div>
<div class="partner-logo-item"><img loading="lazy" src="/images/partners/메가메타.jpg" alt="메가메타 - 밍글 스튜디오 클라이언트" class="partner-logo"></div>
<div class="partner-logo-item"><img loading="lazy" src="/images/partners/(주)세광종합기술단.png" alt="(주)세광종합기술단 - 밍글 스튜디오 클라이언트" class="partner-logo"></div>
<!-- 무한 루프용 복제 -->
<div class="partner-logo-item"><img src="/images/partners/메가메타.jpg" alt="메가메타" class="partner-logo"></div>
<div class="partner-logo-item"><img src="/images/partners/(주)세광종합기술단.png" alt="(주)세광종합기술단" class="partner-logo"></div>
<div class="partner-logo-item"><img src="/images/partners/메가메타.jpg" alt="메가메타" class="partner-logo"></div>
<div class="partner-logo-item"><img src="/images/partners/(주)세광종합기술단.png" alt="(주)세광종합기술단" class="partner-logo"></div>
<div class="partner-logo-item"><img loading="lazy" src="/images/partners/메가메타.jpg" alt="메가메타" class="partner-logo"></div>
<div class="partner-logo-item"><img loading="lazy" src="/images/partners/(주)세광종합기술단.png" alt="(주)세광종합기술단" class="partner-logo"></div>
<div class="partner-logo-item"><img loading="lazy" src="/images/partners/메가메타.jpg" alt="메가메타" class="partner-logo"></div>
<div class="partner-logo-item"><img loading="lazy" src="/images/partners/(주)세광종합기술단.png" alt="(주)세광종합기술단" class="partner-logo"></div>
</div>
</div>
</div>
@ -839,7 +845,7 @@
</div>
</div>
<div class="footer-bottom">
<p>&copy; 2025 밍글 스튜디오. All rights reserved.</p>
<p>&copy; <script>document.write(new Date().getFullYear())</script> 밍글 스튜디오. All rights reserved.</p>
</div>
</div>
</footer>

View File

@ -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();

View File

@ -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 = '<nav class="navbar"><div class="nav-container"><a href="/" class="nav-logo"><span>밍글 스튜디오</span></a></div></nav>';
}
}
@ -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 = `&copy; ${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 = `
<div class="component-loading">
<div class="loading-spinner"></div>
<div class="loading-text">${text}</div>
</div>
`;
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) {

View File

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

View File

@ -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 = `
<div id="lightbox" class="lightbox" role="dialog" aria-label="이미지 뷰어">
@ -62,11 +68,13 @@ function initLightbox() {
</div>
</div>
`;
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;

View File

@ -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');
}
});
}

View File

@ -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();
}
});

View File

@ -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 = '비디오를 로드할 수 없습니다<br><small>네트워크 연결을 확인해주세요</small>';
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%;

View File

@ -74,6 +74,8 @@
* 이벤트 리스너 설정
*/
function setupEventListeners() {
if (!elements.searchInput || !elements.imageModal) return;
// 검색
elements.searchInput.addEventListener('input', debounce((e) => {
searchQuery = e.target.value.toLowerCase();

View File

@ -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 => ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'})[c]);
let noResults = document.querySelector('.no-results');
if (!noResults) {
noResults = document.createElement('div');
@ -311,13 +312,13 @@ function showNoResults(query) {
noResults.innerHTML = `
<div class="no-results-icon">🔍</div>
<h3>검색 결과가 없습니다</h3>
<p><strong>"${safeQuery}"</strong> .</p>
<p><strong></strong> .</p>
<p>다른 키워드로 검색해보시거나 <a href="contact.html">직접 문의</a> .</p>
`;
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');
}

View File

@ -659,7 +659,7 @@
</div>
</div>
<div class="footer-bottom">
<p>&copy; 2025 밍글 스튜디오. All rights reserved.</p>
<p>&copy; <script>document.write(new Date().getFullYear())</script> 밍글 스튜디오. All rights reserved.</p>
</div>
</div>
</footer>

View File

@ -446,7 +446,7 @@
</div>
</div>
<div class="footer-bottom">
<p>&copy; 2025 밍글 스튜디오. All rights reserved.</p>
<p>&copy; <script>document.write(new Date().getFullYear())</script> 밍글 스튜디오. All rights reserved.</p>
</div>
</div>
</footer>

View File

@ -760,7 +760,7 @@
</div>
</div>
<div class="footer-bottom">
<p>&copy; 2025 밍글 스튜디오. All rights reserved.</p>
<p>&copy; <script>document.write(new Date().getFullYear())</script> 밍글 스튜디오. All rights reserved.</p>
</div>
</div>
</footer>