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> </div>
<div class="footer-bottom"> <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>
</div> </div>
</footer> </footer>

View File

@ -27,7 +27,7 @@
</div> </div>
</div> </div>
<div class="footer-bottom"> <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>
</div> </div>
</footer> </footer>

View File

@ -226,7 +226,7 @@
</div> </div>
</div> </div>
<div class="footer-bottom"> <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>
</div> </div>
</footer> </footer>

View File

@ -326,10 +326,6 @@
/* ======================================== /* ========================================
다크모드 다크모드
======================================== */ ======================================== */
[data-theme="dark"] .page-header {
background: var(--dark-header-gradient);
}
[data-theme="dark"] .info-grid { [data-theme="dark"] .info-grid {
background: var(--dark-surface); background: var(--dark-surface);
border: 1px solid var(--glass-border); border: 1px solid var(--glass-border);

View File

@ -412,13 +412,6 @@ body {
gap: var(--spacing-md); 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 { .site-footer {
background: var(--text-primary); background: var(--text-primary);
@ -1501,7 +1494,7 @@ body {
.streamer-card { .streamer-card {
background: var(--bg-white); background: var(--bg-white);
border-radius: var(--radius-xl); border-radius: var(--border-radius-xl);
box-shadow: var(--shadow-md); box-shadow: var(--shadow-md);
padding: var(--spacing-2xl); padding: var(--spacing-2xl);
text-align: center; text-align: center;
@ -1577,7 +1570,7 @@ body {
align-items: center; align-items: center;
gap: 6px; gap: 6px;
padding: 8px 16px; padding: 8px 16px;
border-radius: var(--radius-lg); border-radius: var(--border-radius-lg);
font-size: var(--font-sm); font-size: var(--font-sm);
font-weight: 500; font-weight: 500;
text-decoration: none; text-decoration: none;
@ -1686,7 +1679,7 @@ body {
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border-radius: var(--radius-lg); border-radius: var(--border-radius-lg);
overflow: hidden; overflow: hidden;
background: var(--bg-light); background: var(--bg-light);
box-shadow: var(--shadow-sm); 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 { [data-theme="dark"] .contact-card {
background: rgba(255, 255, 255, 0.04); background: rgba(255, 255, 255, 0.04);
backdrop-filter: blur(20px); 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 { [data-theme="dark"] .gallery-grid {
background: transparent; background: transparent;
} }

View File

@ -817,10 +817,6 @@
/* ======================================== /* ========================================
다크모드 다크모드
======================================== */ ======================================== */
[data-theme="dark"] .page-header {
background: var(--dark-header-gradient);
}
[data-theme="dark"] .tab-btn { [data-theme="dark"] .tab-btn {
color: var(--dark-text-secondary); color: var(--dark-text-secondary);
border-color: var(--glass-border); 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 { [data-theme="dark"] .faq-search {
background: var(--dark-surface); background: var(--dark-surface);
border: 1px solid var(--glass-border); 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 { [data-theme="dark"] .service-package {
background-color: var(--dark-surface); background-color: var(--dark-surface);
border: 1px solid var(--glass-border); border: 1px solid var(--glass-border);

View File

@ -215,7 +215,7 @@
</div> </div>
</div> </div>
<div class="footer-bottom"> <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>
</div> </div>
</footer> </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">
<div class="signs-marquee-track"> <div class="signs-marquee-track">
<!-- 원본 10개 --> <!-- 원본 12개 -->
<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 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 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 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 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 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 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 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 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 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>
<!-- 복제 1 --> <!-- 복제 1 -->
<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 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 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 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 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 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 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 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 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 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>
<!-- 복제 2 --> <!-- 복제 2 -->
<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 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 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 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 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 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 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 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 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 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> </div>
</div> </div>
</div> </div>
@ -735,13 +741,13 @@
<!-- 클라이언트 로고 마키 --> <!-- 클라이언트 로고 마키 -->
<div class="partners-marquee"> <div class="partners-marquee">
<div class="partners-marquee-track"> <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 loading="lazy" 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/(주)세광종합기술단.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 loading="lazy" 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/(주)세광종합기술단.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 loading="lazy" 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/(주)세광종합기술단.png" alt="(주)세광종합기술단" class="partner-logo"></div>
</div> </div>
</div> </div>
</div> </div>
@ -839,7 +845,7 @@
</div> </div>
</div> </div>
<div class="footer-bottom"> <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>
</div> </div>
</footer> </footer>

View File

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

View File

@ -35,12 +35,14 @@ async function loadComponents() {
if (headerPlaceholder) { if (headerPlaceholder) {
try { try {
const response = await fetch('components/header.html'); const response = await fetch('components/header.html');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const html = await response.text(); const html = await response.text();
headerPlaceholder.innerHTML = html; headerPlaceholder.innerHTML = html;
initializeNavigation(); // 헤더 로드 후 네비게이션 초기화 initializeNavigation(); // 헤더 로드 후 네비게이션 초기화
initThemeToggle(); // 테마 토글 초기화 initThemeToggle(); // 테마 토글 초기화
} catch (error) { } catch (error) {
console.error('Error loading header:', 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) { if (footerPlaceholder) {
try { try {
const response = await fetch('components/footer.html'); const response = await fetch('components/footer.html');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const html = await response.text(); const html = await response.text();
footerPlaceholder.innerHTML = html; 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) { if (backupFooter) {
backupFooter.style.display = 'none'; backupFooter.style.display = 'none';
} }
@ -67,11 +76,15 @@ async function loadComponents() {
// ======================================== // ========================================
// 네비게이션 기능 // 네비게이션 기능
// ======================================== // ========================================
let _navInitialized = false;
let _scrollListenerAdded = false;
function initializeNavigation() { function initializeNavigation() {
const hamburger = document.querySelector('.hamburger'); const hamburger = document.querySelector('.hamburger');
const navMenu = document.querySelector('.nav-menu'); const navMenu = document.querySelector('.nav-menu');
if (hamburger && navMenu) { if (hamburger && navMenu && !hamburger._listenerAdded) {
hamburger._listenerAdded = true;
hamburger.addEventListener('click', function() { hamburger.addEventListener('click', function() {
const isActive = hamburger.classList.toggle('active'); const isActive = hamburger.classList.toggle('active');
navMenu.classList.toggle('active'); navMenu.classList.toggle('active');
@ -90,20 +103,23 @@ function initializeNavigation() {
}); });
} }
// 스크롤 시 네비게이션 바 스타일 변경 (RAF 최적화) // 스크롤 시 네비게이션 바 스타일 변경 (RAF 최적화) - 한 번만 등록
let scrollTicking = false; if (!_scrollListenerAdded) {
const navbar = document.querySelector('.navbar'); _scrollListenerAdded = true;
window.addEventListener('scroll', () => { let scrollTicking = false;
if (!scrollTicking) { window.addEventListener('scroll', () => {
requestAnimationFrame(() => { if (!scrollTicking) {
if (navbar) { requestAnimationFrame(() => {
navbar.classList.toggle('scrolled', window.pageYOffset > 20); const navbar = document.querySelector('.navbar');
} if (navbar) {
scrollTicking = false; navbar.classList.toggle('scrolled', window.pageYOffset > 20);
}); }
scrollTicking = true; scrollTicking = false;
} });
}); scrollTicking = true;
}
}, { passive: true });
}
} }
// ======================================== // ========================================
@ -292,9 +308,6 @@ function initLazyLoading() {
} }
} }
// ========================================
// Export 함수들 (다른 스크립트에서 사용 가능)
// ========================================
// ======================================== // ========================================
// 로딩 상태 관리 // 로딩 상태 관리
// ======================================== // ========================================
@ -327,12 +340,17 @@ function hidePageLoading() {
} }
function showComponentLoading(element, text = '로딩 중...') { function showComponentLoading(element, text = '로딩 중...') {
element.innerHTML = ` const wrapper = document.createElement('div');
<div class="component-loading"> wrapper.className = 'component-loading';
<div class="loading-spinner"></div> const spinner = document.createElement('div');
<div class="loading-text">${text}</div> spinner.className = 'loading-spinner';
</div> 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) { function hideComponentLoading(element, content) {

View File

@ -79,16 +79,18 @@ async function handleFormSubmit(e) {
// 서버 전송 (mailto 기반 폴백) // 서버 전송 (mailto 기반 폴백)
async function submitContactForm(data) { 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( const body = encodeURIComponent(
`이름: ${data.name || ''}\n` + `이름: ${sanitize(data.name)}\n` +
`이메일: ${data.email || ''}\n` + `이메일: ${sanitize(data.email)}\n` +
`전화번호: ${data.phone || ''}\n` + `전화번호: ${sanitize(data.phone)}\n` +
`문의 유형: ${data.service || ''}\n` + `문의 유형: ${sanitize(data.service)}\n` +
`\n문의 내용:\n${data.message || ''}` `\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: '이메일 클라이언트가 열렸습니다.' }; return { success: true, message: '이메일 클라이언트가 열렸습니다.' };
} }
@ -206,7 +208,12 @@ function isValidPhone(phone) {
// 전화번호 자동 포맷팅 // 전화번호 자동 포맷팅
function formatPhoneNumber(e) { function formatPhoneNumber(e) {
let value = e.target.value.replace(/[^0-9]/g, ''); let value = e.target.value.replace(/[^0-9]/g, '');
// 최대 11자리 제한 (한국 휴대폰 번호)
if (value.length > 11) {
value = value.slice(0, 11);
}
if (value.length >= 11) { if (value.length >= 11) {
// 01X-XXXX-XXXX 형태 // 01X-XXXX-XXXX 형태
value = value.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3'); value = value.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3');
@ -217,7 +224,7 @@ function formatPhoneNumber(e) {
// 01X-XXX 형태 // 01X-XXX 형태
value = value.replace(/(\d{3})(\d{0,4})/, '$1-$2'); value = value.replace(/(\d{3})(\d{0,4})/, '$1-$2');
} }
e.target.value = value; e.target.value = value;
} }

View File

@ -12,44 +12,50 @@ document.addEventListener('DOMContentLoaded', function() {
// 갤러리 초기화 // 갤러리 초기화
function initGallery() { function initGallery() {
const galleryItems = document.querySelectorAll('.gallery-item'); 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) => { galleryItems.forEach((item, index) => {
const img = item.querySelector('.gallery-img'); const img = item.querySelector('.gallery-img');
if (!img) return;
// 이미지 클릭 시 라이트박스 열기 // 이미지 클릭 시 라이트박스 열기
img.addEventListener('click', () => openLightbox(index)); img.addEventListener('click', () => openLightbox(index));
// 이미지 로딩 에러 처리 // 이미지 로딩 에러 처리
img.addEventListener('error', function() { img.addEventListener('error', function() {
this.src = 'images/placeholder.jpg'; this.src = 'images/placeholder.jpg';
this.alt = '이미지를 불러올 수 없습니다'; this.alt = '이미지를 불러올 수 없습니다';
}); });
// 레이지 로딩 구현 // 레이지 로딩 구현
if ('IntersectionObserver' in window) { if (imageObserver && img.dataset.src) {
const imageObserver = new IntersectionObserver((entries) => { imageObserver.observe(img);
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);
}
} }
}); });
} }
// 라이트박스 기능 // 라이트박스 기능
let currentImageIndex = 0; let currentImageIndex = 0;
const galleryImages = document.querySelectorAll('.gallery-img'); let galleryImages = [];
function initLightbox() { function initLightbox() {
// DOM 준비 후 갤러리 이미지 수집
galleryImages = document.querySelectorAll('.gallery-img');
// 라이트박스 HTML 생성 // 라이트박스 HTML 생성
const lightboxHTML = ` const lightboxHTML = `
<div id="lightbox" class="lightbox" role="dialog" aria-label="이미지 뷰어"> <div id="lightbox" class="lightbox" role="dialog" aria-label="이미지 뷰어">
@ -62,11 +68,13 @@ function initLightbox() {
</div> </div>
</div> </div>
`; `;
document.body.insertAdjacentHTML('beforeend', lightboxHTML); document.body.insertAdjacentHTML('beforeend', lightboxHTML);
// ESC 키로 라이트박스 닫기 // ESC 키로 라이트박스 닫기 (라이트박스가 열려 있을 때만)
document.addEventListener('keydown', function(e) { 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 === 'Escape') closeLightbox();
if (e.key === 'ArrowLeft') previousImage(); if (e.key === 'ArrowLeft') previousImage();
if (e.key === 'ArrowRight') nextImage(); 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) { function setupLightboxTouchListeners() {
touchStartX = e.changedTouches[0].screenX; if (lightboxTouchListenersAdded) return;
} const lightbox = document.getElementById('lightbox');
if (!lightbox) return;
lightboxTouchListenersAdded = true;
function handleLightboxTouchEnd(e) { lightbox.addEventListener('touchstart', function(e) {
touchEndX = e.changedTouches[0].screenX; if (!lightbox.classList.contains('active')) return;
const swipeThreshold = 50; lightboxTouchStartX = e.changedTouches[0].screenX;
const diff = touchStartX - touchEndX; });
if (Math.abs(diff) > swipeThreshold) { lightbox.addEventListener('touchend', function(e) {
if (diff > 0) { if (!lightbox.classList.contains('active')) return;
nextImage(); const touchEndX = e.changedTouches[0].screenX;
} else { const swipeThreshold = 50;
previousImage(); const diff = lightboxTouchStartX - touchEndX;
if (Math.abs(diff) > swipeThreshold) {
if (diff > 0) {
nextImage();
} else {
previousImage();
}
} }
} });
} }
// openLightbox/closeLightbox에서 터치 리스너 관리를 위해 원본 함수 래핑 // openLightbox 래핑: 터치 리스너 초기화 보장
const _originalOpenLightbox = openLightbox; const _originalOpenLightbox = openLightbox;
openLightbox = function(index) { openLightbox = function(index) {
_originalOpenLightbox(index); _originalOpenLightbox(index);
const lightbox = document.getElementById('lightbox'); setupLightboxTouchListeners();
if (lightbox) {
lightbox.addEventListener('touchstart', handleLightboxTouchStart);
lightbox.addEventListener('touchend', handleLightboxTouchEnd);
}
}; };
const _originalCloseLightbox = closeLightbox;
closeLightbox = function() {
const lightbox = document.getElementById('lightbox');
if (lightbox) {
lightbox.removeEventListener('touchstart', handleLightboxTouchStart);
lightbox.removeEventListener('touchend', handleLightboxTouchEnd);
}
_originalCloseLightbox();
};
// ======================================== // ========================================
// 간단한 360도 파노라마 뷰어 - 좌우 스크롤 방식 // 간단한 360도 파노라마 뷰어 - 좌우 스크롤 방식
@ -378,8 +288,7 @@ class Easy360Viewer {
user-select: none; user-select: none;
pointer-events: none; pointer-events: none;
display: block; display: block;
image-rendering: -webkit-optimize-contrast; image-rendering: auto;
image-rendering: crisp-edges;
filter: contrast(1.05) saturate(1.1); filter: contrast(1.05) saturate(1.1);
`; `;
this.image.draggable = false; this.image.draggable = false;
@ -408,18 +317,23 @@ class Easy360Viewer {
// 컨테이너 크기 가져오기 // 컨테이너 크기 가져오기
const containerHeight = this.container.clientHeight; const containerHeight = this.container.clientHeight;
const containerWidth = this.container.clientWidth; 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; let imageWidth = imageHeight * imageAspectRatio;
// 360도 이미지는 매우 가로가 길므로 최소 크기 보장 // 최소 너비 보장: 컨테이너 1.5배 (3배까지 늘리면 흐릿해짐)
const minWidth = Math.max(containerWidth * 3, imageWidth); // 단, 원본 너비를 초과하지 않도록 제한
imageWidth = minWidth; const minWidth = Math.min(containerWidth * 1.5, naturalW);
imageHeight = imageWidth / imageAspectRatio; if (imageWidth < minWidth) {
imageWidth = minWidth;
imageHeight = imageWidth / imageAspectRatio;
}
// 줌 레벨에 따른 크기 조정 (모바일 확대 시 경계 문제 해결) // 줌 레벨에 따른 크기 조정
if (this.zoom > 1) { if (this.zoom > 1) {
imageHeight = Math.max(containerHeight, imageHeight); imageHeight = Math.max(containerHeight, imageHeight);
imageWidth = imageHeight * imageAspectRatio; imageWidth = imageHeight * imageAspectRatio;

View File

@ -81,7 +81,7 @@ function initParallaxImages() {
}); });
ticking = true; ticking = true;
} }
}); }, { passive: true });
} }
// ======================================== // ========================================
@ -107,7 +107,7 @@ function initHeroScrollFade() {
}); });
ticking = true; ticking = true;
} }
}); }, { passive: true });
} }
// ======================================== // ========================================
@ -315,8 +315,9 @@ function initPortfolioTabs() {
activePanel.classList.add('active'); activePanel.classList.add('active');
// 탭 전환 시 lazy loading 트리거 // 탭 전환 시 lazy loading 트리거
activePanel.querySelectorAll('iframe[data-src]').forEach(iframe => { activePanel.querySelectorAll('iframe[data-src]').forEach(iframe => {
if (!iframe.src) { if (iframe.dataset.src) {
iframe.src = iframe.dataset.src; iframe.src = iframe.dataset.src;
iframe.removeAttribute('data-src');
} }
}); });
} }

View File

@ -80,7 +80,8 @@ document.addEventListener('DOMContentLoaded', function() {
// ESC 키로 닫기 // ESC 키로 닫기
document.addEventListener('keydown', function(e) { document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') { const popup = document.getElementById('mainPopup');
if (e.key === 'Escape' && popup && popup.classList.contains('active')) {
closePopup(); closePopup();
} }
}); });

View File

@ -322,7 +322,9 @@ function initYouTubePlayers() {
players.push(player); players.push(player);
} }
}); });
// pauseOtherVideos에서 사용할 수 있도록 전역에 저장
window.youtubePlayers = players;
return players; return players;
} }
@ -335,7 +337,8 @@ function extractVideoId(url) {
// 플레이어 준비 완료 // 플레이어 준비 완료
function onPlayerReady(event) { function onPlayerReady(event) {
// 플레이어 컨테이너에 로딩 완료 클래스 추가 // 플레이어 컨테이너에 로딩 완료 클래스 추가
const playerElement = event.target.getIframe(); const playerElement = event.target.getIframe?.();
if (!playerElement) return;
const wrapper = playerElement.closest('.video-wrapper'); const wrapper = playerElement.closest('.video-wrapper');
if (wrapper) { if (wrapper) {
wrapper.classList.add('loaded'); wrapper.classList.add('loaded');
@ -348,7 +351,8 @@ function onPlayerReady(event) {
// 플레이어 상태 변경 // 플레이어 상태 변경
function onPlayerStateChange(event) { function onPlayerStateChange(event) {
const playerElement = event.target.getIframe(); const playerElement = event.target.getIframe?.();
if (!playerElement) return;
const videoCard = playerElement.closest('.video-card'); const videoCard = playerElement.closest('.video-card');
if (event.data === YT.PlayerState.PLAYING) { if (event.data === YT.PlayerState.PLAYING) {
@ -374,13 +378,19 @@ function onPlayerStateChange(event) {
// 플레이어 오류 처리 // 플레이어 오류 처리
function onPlayerError(event) { function onPlayerError(event) {
console.error('YouTube player error:', event.data); 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'); const wrapper = playerElement.closest('.video-wrapper');
if (wrapper) { if (wrapper) {
const errorDiv = document.createElement('div'); const errorDiv = document.createElement('div');
errorDiv.className = 'video-error'; 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 = ` errorDiv.style.cssText = `
position: absolute; position: absolute;
top: 50%; top: 50%;

View File

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

View File

@ -143,14 +143,16 @@ function handleSearch(query) {
return; return;
} }
const searchRegex = new RegExp(query, 'gi'); // 검색어 이스케이프 및 g 플래그 없이 생성 (.test()의 lastIndex 문제 방지)
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const searchRegex = new RegExp(escapedQuery, 'i');
faqItems.forEach(item => { faqItems.forEach(item => {
const question = item.querySelector('.faq-question h3'); const question = item.querySelector('.faq-question h3');
const answer = item.querySelector('.faq-answer'); const answer = item.querySelector('.faq-answer');
const questionText = question.textContent; const questionText = question.textContent;
const answerText = answer.textContent; const answerText = answer.textContent;
// 검색어 매칭 확인 // 검색어 매칭 확인
const questionMatch = searchRegex.test(questionText); const questionMatch = searchRegex.test(questionText);
const answerMatch = searchRegex.test(answerText); const answerMatch = searchRegex.test(answerText);
@ -162,8 +164,8 @@ function handleSearch(query) {
// 검색어 하이라이트 // 검색어 하이라이트
highlightSearchTerm(item, query); highlightSearchTerm(item, query);
// 답변에 매칭되는 경우 자동으로 열기 // 답변에 매칭되는 경우 자동으로 열기 (이미 열려 있으면 무시)
if (answerMatch && !questionMatch) { if (answerMatch && !questionMatch && !item.classList.contains('active')) {
toggleFAQ(item); toggleFAQ(item);
} }
} else { } else {
@ -303,7 +305,6 @@ function hideSuggestions() {
// 검색 결과 없음 표시 // 검색 결과 없음 표시
function showNoResults(query) { function showNoResults(query) {
const safeQuery = query.replace(/[<>&"]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'})[c]);
let noResults = document.querySelector('.no-results'); let noResults = document.querySelector('.no-results');
if (!noResults) { if (!noResults) {
noResults = document.createElement('div'); noResults = document.createElement('div');
@ -311,13 +312,13 @@ function showNoResults(query) {
noResults.innerHTML = ` noResults.innerHTML = `
<div class="no-results-icon">🔍</div> <div class="no-results-icon">🔍</div>
<h3>검색 결과가 없습니다</h3> <h3>검색 결과가 없습니다</h3>
<p><strong>"${safeQuery}"</strong> .</p> <p><strong></strong> .</p>
<p>다른 키워드로 검색해보시거나 <a href="contact.html">직접 문의</a> .</p> <p>다른 키워드로 검색해보시거나 <a href="contact.html">직접 문의</a> .</p>
`; `;
document.querySelector('.faq-list').appendChild(noResults); 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'); noResults.classList.add('active');
} }

View File

@ -659,7 +659,7 @@
</div> </div>
</div> </div>
<div class="footer-bottom"> <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>
</div> </div>
</footer> </footer>

View File

@ -446,7 +446,7 @@
</div> </div>
</div> </div>
<div class="footer-bottom"> <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>
</div> </div>
</footer> </footer>

View File

@ -760,7 +760,7 @@
</div> </div>
</div> </div>
<div class="footer-bottom"> <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>
</div> </div>
</footer> </footer>