/** * YS Project - 추천 스타일 상세 페이지 (작품/배우 공용) * Mobile-first, vanilla JS (no framework dependency) * * 담당 범위 * - 작품코드/배우코드 복사 버튼 * - 외부 이미지 로드 실패 시 플레이스홀더 대체 * * 샘플 영상 embed 여부는 서버(styleVideoEmbedUrl)가 화이트리스트로 판정한다. * JS 는 iframe src 를 만들지도 바꾸지도 않는다. */ (function () { 'use strict'; // ============================================================ // Toast Notification (store.js / attendance_board.js 와 동일 패턴) // ============================================================ var toastTimeout = null; function showToast(msg) { var toast = document.querySelector('.toast'); if (!toast) { toast = document.createElement('div'); toast.className = 'toast'; document.body.appendChild(toast); } toast.textContent = msg; toast.classList.add('toast--visible'); if (toastTimeout) clearTimeout(toastTimeout); toastTimeout = setTimeout(function () { toast.classList.remove('toast--visible'); }, 2500); } // ============================================================ // Copy to Clipboard // ============================================================ /** * navigator.clipboard 는 HTTPS(또는 localhost)에서만 동작한다. * 평문 HTTP 접속을 대비해 execCommand 폴백을 둔다. */ function copyText(text) { if (navigator.clipboard && window.isSecureContext) { return navigator.clipboard.writeText(text); } return new Promise(function (resolve, reject) { var ta = document.createElement('textarea'); ta.value = text; ta.setAttribute('readonly', ''); ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); var ok = false; try { ok = document.execCommand('copy'); } catch (err) { ok = false; } document.body.removeChild(ta); ok ? resolve() : reject(new Error('copy failed')); }); } function bindCopyButtons() { var buttons = document.querySelectorAll('[data-styles-copy]'); Array.prototype.forEach.call(buttons, function (btn) { btn.addEventListener('click', function () { var value = btn.getAttribute('data-styles-copy') || ''; if (!value) return; copyText(value).then(function () { showToast('코드를 복사했습니다: ' + value); }).catch(function () { showToast('복사에 실패했습니다. 코드를 직접 선택해 주세요.'); }); }); }); } // ============================================================ // Image Fallback // ============================================================ /** * 커버/프로필 이미지는 외부 URL인 경우가 많아 깨질 수 있다. * 실패한 를 같은 크기의 텍스트 플레이스홀더로 교체한다. * (textContent 사용 — 사용자 데이터를 innerHTML 로 넣지 않는다) */ function replaceWithPlaceholder(img) { var label = img.getAttribute('data-styles-fallback') || '이미지 없음'; var box = document.createElement('div'); // 크기·색은 assets/css/style.css 의 .styles-detail__card-placeholder 가 담당한다 box.className = 'styles-detail__card-placeholder'; box.textContent = label; if (img.parentNode) { img.parentNode.replaceChild(box, img); } } function bindImageFallback() { var images = document.querySelectorAll('img[data-styles-fallback]'); Array.prototype.forEach.call(images, function (img) { img.addEventListener('error', function () { replaceWithPlaceholder(img); }); // 리스너 등록 전에 이미 로드가 실패했을 수 있다 if (img.complete && img.naturalWidth === 0) { replaceWithPlaceholder(img); } }); } // ============================================================ // Initialize // ============================================================ function init() { bindCopyButtons(); bindImageFallback(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();