Compare commits

..

No commits in common. "claude/vigorous-mestorf" and "main" have entirely different histories.

40 changed files with 296 additions and 227 deletions

View File

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

View File

@ -326,6 +326,10 @@
/* ======================================== /* ========================================
다크모드 다크모드
======================================== */ ======================================== */
[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,6 +412,13 @@ 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);
@ -1494,7 +1501,7 @@ body {
.streamer-card { .streamer-card {
background: var(--bg-white); background: var(--bg-white);
border-radius: var(--border-radius-xl); border-radius: var(--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;
@ -1570,7 +1577,7 @@ body {
align-items: center; align-items: center;
gap: 6px; gap: 6px;
padding: 8px 16px; padding: 8px 16px;
border-radius: var(--border-radius-lg); border-radius: var(--radius-lg);
font-size: var(--font-sm); font-size: var(--font-sm);
font-weight: 500; font-weight: 500;
text-decoration: none; text-decoration: none;
@ -1679,7 +1686,7 @@ body {
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border-radius: var(--border-radius-lg); border-radius: var(--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,6 +507,10 @@
/* ======================================== /* ========================================
다크모드 다크모드
======================================== */ ======================================== */
[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,6 +929,10 @@
/* ======================================== /* ========================================
다크모드 다크모드
======================================== */ ======================================== */
[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,6 +817,10 @@
/* ======================================== /* ========================================
다크모드 다크모드
======================================== */ ======================================== */
[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,6 +419,10 @@
/* ======================================== /* ========================================
다크모드 다크모드
======================================== */ ======================================== */
[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,6 +2127,10 @@
/* ======================================== /* ========================================
다크모드 다크모드
======================================== */ ======================================== */
[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; <script>document.write(new Date().getFullYear())</script> 밍글 스튜디오. All rights reserved.</p> <p>&copy; 2025 밍글 스튜디오. All rights reserved.</p>
</div> </div>
</div> </div>
</footer> </footer>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 KiB

After

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

After

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

After

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 KiB

After

Width:  |  Height:  |  Size: 459 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 889 KiB

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 KiB

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 KiB

After

Width:  |  Height:  |  Size: 503 KiB

View File

@ -686,45 +686,39 @@
<div class="signs-marquee"> <div class="signs-marquee">
<div class="signs-marquee-track"> <div class="signs-marquee-track">
<!-- 원본 12개 --> <!-- 원본 10개 -->
<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 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 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 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 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 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>
@ -741,13 +735,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 loading="lazy" src="/images/partners/메가메타.jpg" 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/(주)세광종합기술단.png" 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 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/(주)세광종합기술단.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 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/(주)세광종합기술단.png" alt="(주)세광종합기술단" class="partner-logo"></div>
</div> </div>
</div> </div>
</div> </div>
@ -845,7 +839,7 @@
</div> </div>
</div> </div>
<div class="footer-bottom"> <div class="footer-bottom">
<p>&copy; <script>document.write(new Date().getFullYear())</script> 밍글 스튜디오. All rights reserved.</p> <p>&copy; 2025 밍글 스튜디오. All rights reserved.</p>
</div> </div>
</div> </div>
</footer> </footer>

View File

@ -107,8 +107,6 @@
* 이벤트 리스너 설정 * 이벤트 리스너 설정
*/ */
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,14 +35,12 @@ 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>';
} }
} }
@ -51,18 +49,11 @@ 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#backupFooter'); const backupFooter = document.querySelector('footer.site-footer');
if (backupFooter) { if (backupFooter) {
backupFooter.style.display = 'none'; backupFooter.style.display = 'none';
} }
@ -76,15 +67,11 @@ 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 && !hamburger._listenerAdded) { if (hamburger && navMenu) {
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');
@ -103,23 +90,20 @@ function initializeNavigation() {
}); });
} }
// 스크롤 시 네비게이션 바 스타일 변경 (RAF 최적화) - 한 번만 등록 // 스크롤 시 네비게이션 바 스타일 변경 (RAF 최적화)
if (!_scrollListenerAdded) { let scrollTicking = false;
_scrollListenerAdded = true; const navbar = document.querySelector('.navbar');
let scrollTicking = false; window.addEventListener('scroll', () => {
window.addEventListener('scroll', () => { if (!scrollTicking) {
if (!scrollTicking) { requestAnimationFrame(() => {
requestAnimationFrame(() => { if (navbar) {
const navbar = document.querySelector('.navbar'); navbar.classList.toggle('scrolled', window.pageYOffset > 20);
if (navbar) { }
navbar.classList.toggle('scrolled', window.pageYOffset > 20); scrollTicking = false;
} });
scrollTicking = false; scrollTicking = true;
}); }
scrollTicking = true; });
}
}, { passive: true });
}
} }
// ======================================== // ========================================
@ -308,6 +292,9 @@ function initLazyLoading() {
} }
} }
// ========================================
// Export 함수들 (다른 스크립트에서 사용 가능)
// ========================================
// ======================================== // ========================================
// 로딩 상태 관리 // 로딩 상태 관리
// ======================================== // ========================================
@ -340,17 +327,12 @@ function hidePageLoading() {
} }
function showComponentLoading(element, text = '로딩 중...') { function showComponentLoading(element, text = '로딩 중...') {
const wrapper = document.createElement('div'); element.innerHTML = `
wrapper.className = 'component-loading'; <div class="component-loading">
const spinner = document.createElement('div'); <div class="loading-spinner"></div>
spinner.className = 'loading-spinner'; <div class="loading-text">${text}</div>
const label = document.createElement('div'); </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,18 +79,16 @@ async function handleFormSubmit(e) {
// 서버 전송 (mailto 기반 폴백) // 서버 전송 (mailto 기반 폴백)
async function submitContactForm(data) { async function submitContactForm(data) {
// 입력값에서 줄바꿈/캐리지리턴 제거 (이메일 헤더 인젝션 방지) // mailto 링크로 이메일 클라이언트 열기
const sanitize = (str) => (str || '').replace(/[\r\n]/g, ' ').trim(); const subject = encodeURIComponent(`[밍글 스튜디오 문의] ${data.name || '웹사이트 문의'}`);
const subject = encodeURIComponent(`[밍글 스튜디오 문의] ${sanitize(data.name) || '웹사이트 문의'}`);
const body = encodeURIComponent( const body = encodeURIComponent(
`이름: ${sanitize(data.name)}\n` + `이름: ${data.name || ''}\n` +
`이메일: ${sanitize(data.email)}\n` + `이메일: ${data.email || ''}\n` +
`전화번호: ${sanitize(data.phone)}\n` + `전화번호: ${data.phone || ''}\n` +
`문의 유형: ${sanitize(data.service)}\n` + `문의 유형: ${data.service || ''}\n` +
`\n문의 내용:\n${(data.message || '').trim()}` `\n문의 내용:\n${data.message || ''}`
); );
window.location.href = `mailto:help@minglestudio.co.kr?subject=${subject}&body=${body}`; window.location.href = `mailto:mingle_studio@naver.com?subject=${subject}&body=${body}`;
return { success: true, message: '이메일 클라이언트가 열렸습니다.' }; return { success: true, message: '이메일 클라이언트가 열렸습니다.' };
} }
@ -209,11 +207,6 @@ 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');

View File

@ -13,24 +13,8 @@ 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));
@ -42,20 +26,30 @@ function initGallery() {
}); });
// 레이지 로딩 구현 // 레이지 로딩 구현
if (imageObserver && img.dataset.src) { if ('IntersectionObserver' in window) {
imageObserver.observe(img); 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);
}
} }
}); });
} }
// 라이트박스 기능 // 라이트박스 기능
let currentImageIndex = 0; let currentImageIndex = 0;
let galleryImages = []; const galleryImages = document.querySelectorAll('.gallery-img');
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="이미지 뷰어">
@ -71,10 +65,8 @@ function initLightbox() {
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();
@ -166,45 +158,143 @@ function initGalleryAnimations() {
}); });
} }
// 터치 이벤트 지원 (모바일) - 라이트박스에만 스코프 제한 // 갤러리 필터 기능 (향후 확장용)
// 리스너는 한 번만 등록하고, 라이트박스 활성 상태에서만 동작 function initGalleryFilters() {
let lightboxTouchStartX = 0; const filterButtons = document.querySelectorAll('.filter-btn');
let lightboxTouchListenersAdded = false; const galleryItems = document.querySelectorAll('.gallery-item');
function setupLightboxTouchListeners() { filterButtons.forEach(btn => {
if (lightboxTouchListenersAdded) return; btn.addEventListener('click', function() {
const lightbox = document.getElementById('lightbox'); // 활성 버튼 업데이트
if (!lightbox) return; filterButtons.forEach(b => b.classList.remove('active'));
lightboxTouchListenersAdded = true; this.classList.add('active');
lightbox.addEventListener('touchstart', function(e) { const filter = this.dataset.filter;
if (!lightbox.classList.contains('active')) return;
lightboxTouchStartX = e.changedTouches[0].screenX;
});
lightbox.addEventListener('touchend', function(e) { galleryItems.forEach(item => {
if (!lightbox.classList.contains('active')) return; if (filter === 'all' || item.dataset.category === filter) {
const touchEndX = e.changedTouches[0].screenX; item.style.display = 'block';
const swipeThreshold = 50; setTimeout(() => {
const diff = lightboxTouchStartX - touchEndX; item.style.opacity = '1';
item.style.transform = 'scale(1)';
if (Math.abs(diff) > swipeThreshold) { }, 10);
if (diff > 0) { } else {
nextImage(); item.style.opacity = '0';
} else { item.style.transform = 'scale(0.8)';
previousImage(); setTimeout(() => {
} item.style.display = 'none';
} }, 300);
}
});
});
}); });
} }
// openLightbox 래핑: 터치 리스너 초기화 보장 // 이미지 프리로딩
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;
function handleLightboxTouchStart(e) {
touchStartX = e.changedTouches[0].screenX;
}
function handleLightboxTouchEnd(e) {
touchEndX = e.changedTouches[0].screenX;
const swipeThreshold = 50;
const diff = touchStartX - touchEndX;
if (Math.abs(diff) > swipeThreshold) {
if (diff > 0) {
nextImage();
} else {
previousImage();
}
}
}
// openLightbox/closeLightbox에서 터치 리스너 관리를 위해 원본 함수 래핑
const _originalOpenLightbox = openLightbox; const _originalOpenLightbox = openLightbox;
openLightbox = function(index) { openLightbox = function(index) {
_originalOpenLightbox(index); _originalOpenLightbox(index);
setupLightboxTouchListeners(); const lightbox = document.getElementById('lightbox');
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도 파노라마 뷰어 - 좌우 스크롤 방식
@ -288,7 +378,8 @@ class Easy360Viewer {
user-select: none; user-select: none;
pointer-events: none; pointer-events: none;
display: block; display: block;
image-rendering: auto; image-rendering: -webkit-optimize-contrast;
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;
@ -317,23 +408,18 @@ class Easy360Viewer {
// 컨테이너 크기 가져오기 // 컨테이너 크기 가져오기
const containerHeight = this.container.clientHeight; const containerHeight = this.container.clientHeight;
const containerWidth = this.container.clientWidth; const containerWidth = this.container.clientWidth;
const naturalW = this.image.naturalWidth; const imageAspectRatio = this.image.naturalWidth / this.image.naturalHeight;
const naturalH = this.image.naturalHeight;
const imageAspectRatio = naturalW / naturalH;
// 컨테이너 높이에 맞추되, 원본 해상도를 초과하지 않도록 제한 // 모바일에서 줌 시 경계 문제 해결을 위한 더 정확한 크기 계산
let imageHeight = Math.min(containerHeight * this.zoom, naturalH); let imageHeight = containerHeight * this.zoom;
let imageWidth = imageHeight * imageAspectRatio; let imageWidth = imageHeight * imageAspectRatio;
// 최소 너비 보장: 컨테이너 1.5배 (3배까지 늘리면 흐릿해짐) // 360도 이미지는 매우 가로가 길므로 최소 크기 보장
// 단, 원본 너비를 초과하지 않도록 제한 const minWidth = Math.max(containerWidth * 3, imageWidth);
const minWidth = Math.min(containerWidth * 1.5, naturalW); imageWidth = minWidth;
if (imageWidth < minWidth) { imageHeight = imageWidth / imageAspectRatio;
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,9 +315,8 @@ 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.dataset.src) { if (!iframe.src) {
iframe.src = iframe.dataset.src; iframe.src = iframe.dataset.src;
iframe.removeAttribute('data-src');
} }
}); });
} }

View File

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

View File

@ -323,8 +323,6 @@ function initYouTubePlayers() {
} }
}); });
// pauseOtherVideos에서 사용할 수 있도록 전역에 저장
window.youtubePlayers = players;
return players; return players;
} }
@ -337,8 +335,7 @@ 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');
@ -351,8 +348,7 @@ 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) {
@ -378,19 +374,13 @@ 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';
const mainText = document.createElement('div'); errorDiv.innerHTML = '비디오를 로드할 수 없습니다<br><small>네트워크 연결을 확인해주세요</small>';
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,8 +74,6 @@
* 이벤트 리스너 설정 * 이벤트 리스너 설정
*/ */
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,9 +143,7 @@ function handleSearch(query) {
return; return;
} }
// 검색어 이스케이프 및 g 플래그 없이 생성 (.test()의 lastIndex 문제 방지) const searchRegex = new RegExp(query, 'gi');
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');
@ -164,8 +162,8 @@ function handleSearch(query) {
// 검색어 하이라이트 // 검색어 하이라이트
highlightSearchTerm(item, query); highlightSearchTerm(item, query);
// 답변에 매칭되는 경우 자동으로 열기 (이미 열려 있으면 무시) // 답변에 매칭되는 경우 자동으로 열기
if (answerMatch && !questionMatch && !item.classList.contains('active')) { if (answerMatch && !questionMatch) {
toggleFAQ(item); toggleFAQ(item);
} }
} else { } else {
@ -305,6 +303,7 @@ 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');
@ -312,13 +311,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></strong> .</p> <p><strong>"${safeQuery}"</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; <script>document.write(new Date().getFullYear())</script> 밍글 스튜디오. All rights reserved.</p> <p>&copy; 2025 밍글 스튜디오. 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; <script>document.write(new Date().getFullYear())</script> 밍글 스튜디오. All rights reserved.</p> <p>&copy; 2025 밍글 스튜디오. 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; <script>document.write(new Date().getFullYear())</script> 밍글 스튜디오. All rights reserved.</p> <p>&copy; 2025 밍글 스튜디오. All rights reserved.</p>
</div> </div>
</div> </div>
</footer> </footer>