장바구니 루멘 스튜디오
예약금 시뮬레이터 비활성화됨
0%
권장 비율 30% • 범위 10%~60%
예약금 예상액₩0
잔금(결제 시)₩0
주문자 정보 정확한 연락처를 입력해주세요
'; } } async function loadCatalog(){ try{ const res = await fetch('./catalog.json',{cache:'no-store'}); state.catalog = await res.json(); }catch(e){ state.catalog = []; } } function loadCart(){ try{ const raw = localStorage.getItem(state.cartKey); if(!raw){ state.cart=[]; return } const arr = JSON.parse(raw); state.cart = Array.isArray(arr)?arr:[]; }catch(e){ state.cart=[] } } function saveCart(){ localStorage.setItem(state.cartKey, JSON.stringify(state.cart)); } function catalogMap(){ const map = new Map(); for(const it of state.catalog){ if(it && (it.id!==undefined && it.id!==null)) map.set(String(it.id), it); } return map; } function resolveItems(){ const map = catalogMap(); return state.cart.map(ci=>{ const ref = map.get(String(ci.id)) || {}; const price = Number(ref.price || ci.price || 0); const title = ref.title || ci.title || '알 수 없는 패키지'; const image = ref.image || './images/maximum_ditailes_of_this_image.black_modern_camera_studio_light_softbox_high_detail_8k_photorealistic.jpg'; return { id:String(ci.id), qty:clamp(Number(ci.qty||1),1,999), price, title, image } }); } function calcTotals(items){ const sum = items.reduce((acc,it)=>acc + it.price*it.qty, 0); const promoPct = state.promoActive?0.05:0; const discount = Math.round(sum * promoPct); const total = sum - discount; const deposit = state.reserveEnabled ? Math.round(total * (state.reservePct/100)) : 0; const rest = total - deposit; return {sum, discount, total, deposit, rest} } function renderCart(){ const items = resolveItems(); const box = mountEl('cartList'); if(items.length===0){ box.innerHTML = '
장바구니가 비어있습니다. 상단 메뉴에서 상품을 담아보세요.
'; updateSummary({sum:0,discount:0,total:0,deposit:0,rest:0}); return; } const rows = items.map((it,idx)=>{ const rowId=`row_${it.id}_${idx}`; return `
${it.title}
${it.title}
단가: ${formatKRW(it.price)}
스튜디오 전담 매니저 배정
${formatKRW(it.price*it.qty)}
`; }).join(''); box.innerHTML = rows; box.querySelectorAll('[data-act="inc"]').forEach(btn=>{ btn.addEventListener('click', onIncDec); }); box.querySelectorAll('[data-act="dec"]').forEach(btn=>{ btn.addEventListener('click', onIncDec); }); box.querySelectorAll('[data-role="qty"]').forEach(inp=>{ inp.addEventListener('input', onQtyInput); inp.addEventListener('blur', onQtyBlur); }); box.querySelectorAll('[data-act="rm"]').forEach(btn=>{ btn.addEventListener('click', onRemove); }); const totals = calcTotals(items); updateSummary(totals); } function updateSummary(t){ mountEl('sumPrice').textContent = formatKRW(t.sum); mountEl('discountPrice').textContent = '-'+formatKRW(t.discount); mountEl('grandTotal').textContent = formatKRW(t.total); mountEl('summaryDeposit').textContent = formatKRW(t.deposit); mountEl('reserveAmount').textContent = formatKRW(t.deposit); mountEl('restAmount').textContent = formatKRW(t.rest); } function onIncDec(e){ const btn = e.currentTarget; const row = btn.closest('.y7v4k'); const id = row.getAttribute('data-id'); const act = btn.getAttribute('data-act'); const item = state.cart.find(i=>String(i.id)===String(id)); if(!item) return; if(act==='inc') item.qty = clamp(Number(item.qty||1)+1,1,999); else item.qty = clamp(Number(item.qty||1)-1,1,999); saveCart(); renderCart(); } function onQtyInput(e){ const inp = e.currentTarget; inp.value = inp.value.replace(/[^\d]/g,'').slice(0,3); } function onQtyBlur(e){ const inp = e.currentTarget; const row = inp.closest('.y7v4k'); const id = row.getAttribute('data-id'); const item = state.cart.find(i=>String(i.id)===String(id)); if(!item) return; const v = clamp(parseInt(inp.value||'1',10)||1,1,999); item.qty = v; saveCart(); renderCart(); } function onRemove(e){ const row = e.currentTarget.closest('.y7v4k'); const id = row.getAttribute('data-id'); state.cart = state.cart.filter(i=>String(i.id)!==String(id)); saveCart(); renderCart(); } function initPromo(){ const bar = mountEl('promoBar'); const timerEl = mountEl('promoTimer'); const startRaw = localStorage.getItem(state.promoKey); let start = startRaw?Number(startRaw):NaN; if(isNaN(start)){ start = Date.now(); localStorage.setItem(state.promoKey,String(start)); } const end = start + state.promoMinutes*60*1000; function tick(){ const now = Date.now(); const remain = Math.max(0, end-now); const mm = String(Math.floor(remain/60000)).padStart(2,'0'); const ss = String(Math.floor((remain%60000)/1000)).padStart(2,'0'); timerEl.textContent = `${mm}:${ss}`; state.promoActive = remain>0; bar.style.display = 'flex'; renderCart(); if(remain<=0) clearInterval(intv); } tick(); const intv = setInterval(tick, 1000); } function initReserve(){ const toggle = mountEl('reserveToggle'); const range = mountEl('reserveRange'); const status = mountEl('reserveStatus'); const pct = mountEl('reservePercent'); toggle.checked = false; range.disabled = !toggle.checked; status.textContent = toggle.checked?'활성화됨':'비활성화됨'; pct.textContent = toggle.checked?`${range.value}%`:'0%'; state.reserveEnabled = toggle.checked; state.reservePct = parseInt(range.value,10)||30; toggle.addEventListener('change',()=>{ range.disabled = !toggle.checked; state.reserveEnabled = toggle.checked; status.textContent = toggle.checked?'활성화됨':'비활성화됨'; pct.textContent = toggle.checked?`${range.value}%`:'0%'; renderCart(); }); range.addEventListener('input',()=>{ state.reservePct = clamp(parseInt(range.value,10)||30,10,60); pct.textContent = `${state.reservePct}%`; renderCart(); }); } function initTheme(){ const btn = mountEl('themeToggle'); btn.addEventListener('click',()=>{ const isDark = document.documentElement.classList.toggle('dark'); localStorage.setItem('theme', isDark?'dark':'light'); }); } function initForm(){ const form = mountEl('checkoutForm'); const err = mountEl('formErrors'); const phone = mountEl('phone'); phone.addEventListener('input',()=>{ phone.value = phoneNormalize(phone.value) }); form.addEventListener('submit',(e)=>{ e.preventDefault(); err.textContent=''; const items = resolveItems(); if(items.length===0){ err.textContent='장바구니가 비어 있습니다.'; return } const name = String(mountEl('name').value||'').trim(); const email = String(mountEl('email').value||'').trim(); const phoneV = String(mountEl('phone').value||'').trim(); const agree = mountEl('agree').checked; const errs = []; if(name.length<2) errs.push('이름을 2자 이상 입력하세요.'); const phoneRe = /^01[016789]-?\d{3,4}-?\d{4}$/; if(!phoneRe.test(phoneV)) errs.push('연락처 형식이 올바르지 않습니다.'); const emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if(!emailRe.test(email)) errs.push('이메일 형식이 올바르지 않습니다.'); if(!agree) errs.push('개인정보 처리방침에 동의해주세요.'); if(errs.length){ err.innerHTML = errs.join('
'); return } const totals = calcTotals(items); const orderId = genOrderId(); mountEl('orderId').textContent = orderId; mountEl('orderName').textContent = name; mountEl('orderPhone').textContent = phoneV; mountEl('orderEmail').textContent = email; mountEl('orderPay').textContent = formatKRW(totals.total); mountEl('orderDeposit').textContent = formatKRW(totals.deposit); mountEl('orderModal').classList.add('active'); // simulate successful place: clear cart and promo state.cart = []; saveCart(); renderCart(); localStorage.removeItem(state.promoKey); }); mountEl('orderClose').addEventListener('click',()=>mountEl('orderModal').classList.remove('active')); mountEl('orderGoClose').addEventListener('click',()=>mountEl('orderModal').classList.remove('active')); mountEl('clearCart').addEventListener('click',()=>{ if(confirm('장바구니를 비우시겠습니까?')){ state.cart=[]; saveCart(); renderCart(); } }); } function initPromoModal(){ mountEl('promoInfo').addEventListener('click',()=>mountEl('promoModal').classList.add('active')); mountEl('promoClose').addEventListener('click',()=>mountEl('promoModal').classList.remove('active')); mountEl('promoOk').addEventListener('click',()=>mountEl('promoModal').classList.remove('active')); } function initCookie(){ const bn = mountEl('cookieBanner'); const consent = localStorage.getItem('cookieConsent'); if(!consent){ setTimeout(()=>bn.classList.add('show'), 900) } mountEl('cookieAccept').addEventListener('click',()=>{ localStorage.setItem('cookieConsent','granted'); bn.classList.remove('show'); }); mountEl('cookieDecline').addEventListener('click',()=>{ localStorage.setItem('cookieConsent','denied'); bn.classList.remove('show'); }); } function bindGlobals(){ document.addEventListener('keydown',(e)=>{ if(e.key==='Escape'){ mountEl('promoModal').classList.remove('active'); mountEl('orderModal').classList.remove('active'); } }); } async function start(){ els.cartList = mountEl('cartList'); await mountComponents(); await loadCatalog(); loadCart(); initPromo(); initReserve(); initTheme(); initForm(); initPromoModal(); initCookie(); bindGlobals(); renderCart(); } start();