/** 진입점: 이벤트 바인딩·초기 렌더·URL 모달 처리 */ import { state, productsData, saveSelection } from './state.js'; import { applyFilters, renderStatusChips, renderCategoryChips, bindCategoryFilter } from './filters.js'; import { ITEMS_PER_PAGE, STATUS_META } from './config.js'; import { renderProducts, changePage } from './productList.js'; import { openModal, closeModal } from './modal.js'; import { scrollToImage } from './carousel.js'; console.log('Total products loaded:', productsData.length); // HTML onclick에서 사용 window.openModal = openModal; window.closeModal = closeModal; window.changePage = changePage; window.scrollToImage = scrollToImage; let fadeTimers = {}; // 뷰 전환 이벤트 document.getElementById('view-grid').onclick = () => { state.viewMode = 'grid'; updateViewButtons(); renderProducts(state.currentPage); }; document.getElementById('view-table').onclick = () => { state.viewMode = 'table'; updateViewButtons(); renderProducts(state.currentPage); }; function updateViewButtons() { const isGrid = state.viewMode === 'grid'; document.getElementById('view-grid').classList.toggle('bg-white', isGrid); document.getElementById('view-grid').classList.toggle('text-primary', isGrid); document.getElementById('view-table').classList.toggle('bg-white', !isGrid); document.getElementById('view-table').classList.toggle('text-primary', !isGrid); } // [전역 함수] 체크박스 토글 및 합계 업데이트 window.toggleSelectItem = (id) => { if (state.selectedIds.has(id)) state.selectedIds.delete(id); else state.selectedIds.add(id); updateSummary(); }; export function updateSummary() { const summary = document.getElementById('selection-summary'); const countEl = document.getElementById('selected-count'); const priceEl = document.getElementById('selected-total-price'); if (!summary) return; // 테이블 모드이면서 선택된 항목이 있을 때만 노출 if (state.viewMode === 'table' && state.selectedIds.size > 0) { // [수정] flex를 추가할 때 hidden은 확실히 제거 summary.classList.remove('hidden'); summary.classList.add('flex'); const total = Array.from(state.selectedIds).reduce((sum, id) => { const p = productsData.find((item) => item.id === id); return sum + (p ? p.price : 0); }, 0); countEl.textContent = state.selectedIds.size; priceEl.textContent = `₩${total.toLocaleString()}`; } else { // [수정] hidden을 추가할 때 flex는 확실히 제거 summary.classList.remove('flex'); summary.classList.add('hidden'); } } // 검색 입력 const searchInput = document.getElementById('search-input'); if (searchInput) { searchInput.addEventListener('input', (e) => { state.searchKeyword = e.target.value.trim().toLowerCase(); applyFilters(); }); } // Escape로 모달 닫기 document.addEventListener('keydown', (e) => { if (e.key !== 'Escape') return; const modal = document.getElementById('product-modal'); if (!modal || modal.classList.contains('hidden')) return; closeModal(); }); function checkUrlAndOpenModal() { const params = new URLSearchParams(window.location.search); const productId = params.get('id'); if (productId) { const product = productsData.find((p) => String(p.id) === productId); if (product) { setTimeout(() => openModal(product.id), 100); } } } // 초기 테마 설정 확인 const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon'); const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon'); const themeToggleBtn = document.getElementById('theme-toggle'); // 1. 현재 테마 상태에 따라 아이콘 표시/숨김 처리 function updateIcons() { if (document.documentElement.classList.contains('dark')) { themeToggleLightIcon.classList.remove('hidden'); themeToggleDarkIcon.classList.add('hidden'); } else { themeToggleDarkIcon.classList.remove('hidden'); themeToggleLightIcon.classList.add('hidden'); } } // 2. 초기 로드 시 설정 (localStorage 또는 시스템 설정 확인) if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) { document.documentElement.classList.add('dark'); } else { document.documentElement.classList.remove('dark'); } updateIcons(); // 3. 버튼 클릭 이벤트 themeToggleBtn.addEventListener('click', function () { // 테마 토글 if (document.documentElement.classList.contains('dark')) { document.documentElement.classList.remove('dark'); localStorage.setItem('color-theme', 'light'); } else { document.documentElement.classList.add('dark'); localStorage.setItem('color-theme', 'dark'); } updateIcons(); }); // 초기 렌더 renderCategoryChips(productsData); bindCategoryFilter(productsData); renderStatusChips(); applyFilters(); document.addEventListener('DOMContentLoaded', () => { renderProducts(state.currentPage); checkUrlAndOpenModal(); }); // 데이터용 새 ID 생성기 const newId = Math.random().toString(36).substring(2, 10); console.log(`%c[NUMBER]: ${newId}`, 'color: #137fec; font-weight: bold; border: 1px solid #137fec; padding: 2px 5px; border-radius: 4px;'); /** 현재 페이지의 '선택 가능한' 항목들만 선택/해제 */ window.toggleSelectAll = (isChecked) => { const startIndex = (state.currentPage - 1) * ITEMS_PER_PAGE; const currentPageProducts = state.visibleProducts.slice(startIndex, startIndex + ITEMS_PER_PAGE); currentPageProducts.forEach((p) => { const isSelectable = STATUS_META[p.status]?.selectable !== false; if (isSelectable) { if (isChecked) state.selectedIds.add(p.id); else state.selectedIds.delete(p.id); } }); saveSelection(); // 변경사항 저장 updateSummary(); renderProducts(state.currentPage); }; /** 선택 리셋 */ // [수정] 기존 resetSelection을 모달 오픈으로 변경 window.resetSelection = () => { const modal = document.getElementById('selection-reset-modal'); modal.classList.remove('hidden'); modal.classList.add('flex'); }; // [추가] 모달 닫기 window.closeSelectionResetModal = () => { const modal = document.getElementById('selection-reset-modal'); modal.classList.add('hidden'); modal.classList.remove('flex'); }; // [추가] 실제 초기화 실행 (기존 로직 그대로) window.confirmSelectionReset = () => { state.selectedIds.clear(); saveSelection(); // 스토리지 동기화 updateSummary(); renderProducts(state.currentPage); window.closeSelectionResetModal(); }; /** 선택 토글 시 스토리지 저장 추가 */ window.toggleSelectItem = (id) => { const product = productsData.find((p) => p.id === id); if (!product || STATUS_META[product.status]?.selectable === false) return; if (state.selectedIds.has(id)) state.selectedIds.delete(id); else state.selectedIds.add(id); saveSelection(); // 변경사항 저장 updateSummary(); }; /** 선택된 항목들을 CSV 파일로 내보내기 */ window.exportToExcel = () => { if (state.selectedIds.size === 0) { alert('내보낼 상품을 선택해 주세요.'); return; } // 1. 선택된 데이터 추출 및 계산 let totalCount = 0; let totalPrice = 0; const rows = Array.from(state.selectedIds) .map((id) => { const p = productsData.find((item) => item.id === id); if (p) { totalCount += 1; totalPrice += p.price; return [p.id, `"${p.title.replace(/"/g, '""')}"`, p.category, p.price, p.status, `"${p.description.replace(/"/g, '""')}"`]; } return null; }) .filter((row) => row !== null); // 2. 헤더 및 푸터(합계) 설정 const headers = ['상품 ID', '상품명', '카테고리', '가격', '상태', '상세설명']; // 영수증 느낌을 위한 하단 합계 줄 const footerEmpty = ['', '', '', '', '', '']; // 빈 줄 const footerTotal = ['TOTAL', `"총 ${totalCount}건의 항목"`, '', totalPrice, '', `"발행일: ${new Date().toLocaleString()}"`]; // 3. CSV 포맷 생성 (한글 깨짐 방지 BOM 추가) const csvContent = '\uFEFF' + [ headers.join(','), ...rows.map((row) => row.join(',')), footerEmpty.join(','), // 간격 조절용 빈 줄 footerTotal.join(','), // 합계 라인 ].join('\n'); // 4. 다운로드 실행 const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); const timestamp = new Date().toISOString().split('T')[0]; link.setAttribute('href', url); link.setAttribute('download', `inventory_receipt_${timestamp}.csv`); document.body.appendChild(link); link.click(); document.body.removeChild(link); }; let currentHoverIndex = -1; window.handleThumbnailHover = (e, productId) => { if (window.isDragging) return; const product = productsData.find((p) => p.id === productId); if (!product || !product.images || product.images.length <= 1) return; const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX - rect.left; let index = Math.floor((x / rect.width) * product.images.length); index = Math.max(0, Math.min(product.images.length - 1, index)); if (currentHoverIndex !== index) { currentHoverIndex = index; updateThumbnailWithFade(productId, product.images[index], index); } }; // 페이드 업데이트 함수 function updateThumbnailWithFade(productId, newImageUrl, index) { const mainThumb = document.getElementById(`thumb-${productId}`); const fadeThumb = document.getElementById(`thumb-fade-${productId}`); const indicator = document.getElementById(`indicator-${productId}`); if (!mainThumb || !fadeThumb) return; // 기존에 해당 카드에서 돌아가던 타이머가 있다면 즉시 제거 if (fadeTimers[productId]) { clearTimeout(fadeTimers[productId]); } // 페이드 레이어 세팅 fadeThumb.style.transition = 'opacity 0.3s ease-in-out'; fadeThumb.style.backgroundImage = `url("${newImageUrl}")`; fadeThumb.style.opacity = '1'; // 타이머 시작 fadeTimers[productId] = setTimeout(() => { mainThumb.style.backgroundImage = `url("${newImageUrl}")`; fadeThumb.style.opacity = '0'; delete fadeTimers[productId]; // 작업 완료 후 타이머 삭제 }, 300); if (indicator) updateIndicatorUI(indicator, index); } window.handleThumbnailLeave = (productId) => { currentHoverIndex = -1; // 인덱스 초기화 resetThumbnail(productId); }; function resetThumbnail(productId) { // 1. 진행 중인 모든 페이드 타이머 즉시 파괴 if (fadeTimers[productId]) { clearTimeout(fadeTimers[productId]); delete fadeTimers[productId]; } const product = productsData.find((p) => p.id === productId); if (!product) return; const mainThumb = document.getElementById(`thumb-${productId}`); const fadeThumb = document.getElementById(`thumb-fade-${productId}`); const indicator = document.getElementById(`indicator-${productId}`); if (mainThumb && fadeThumb) { const firstImg = `url("${product.images[0]}")`; // 2. 페이드 레이어를 즉시 숨김 (transition 방해 금지) fadeThumb.style.transition = 'none'; fadeThumb.style.opacity = '0'; // 3. 두 레이어 모두 첫 번째 이미지로 강제 일치 mainThumb.style.backgroundImage = firstImg; fadeThumb.style.backgroundImage = firstImg; // 4. 다음 호버를 위해 트랜지션 복구 setTimeout(() => { fadeThumb.style.transition = 'opacity 0.3s ease-in-out'; }, 50); } if (indicator) updateIndicatorUI(indicator, 0); } // 중복 코드를 방지하기 위한 UI 업데이트 헬퍼 function updateIndicatorUI(indicator, activeIndex) { Array.from(indicator.children).forEach((dot, i) => { const isActive = i === activeIndex; dot.style.backgroundColor = isActive ? 'white' : 'rgba(255, 255, 255, 0.4)'; dot.style.transform = isActive ? 'scale(1.2)' : 'scale(1)'; dot.style.opacity = isActive ? '1' : '0.7'; }); } // 터치 상태 관리를 위한 변수 let touchStartX = 0; let isDragging = false; window.handleTouchStart = (e) => { touchStartX = e.touches[0].clientX; isDragging = false; }; window.handleTouchMove = (e, productId) => { const product = productsData.find((p) => p.id === productId); if (!product || !product.images || product.images.length <= 1) return; const cardElement = e.currentTarget; const cardWidth = cardElement.offsetWidth; const touchCurrentX = e.touches[0].clientX; const diffX = touchStartX - touchCurrentX; if (Math.abs(diffX) > 10) { isDragging = true; if (e.cancelable) e.preventDefault(); } if (isDragging) { const step = cardWidth / product.images.length; let index = Math.floor(Math.abs(diffX) / step); index = Math.max(0, Math.min(product.images.length - 1, index)); const mainThumb = document.getElementById(`thumb-${productId}`); const fadeThumb = document.getElementById(`thumb-fade-${productId}`); if (mainThumb && fadeThumb) { // 드래그 중에는 페이드 없이 즉시 교체 (반응성 우선) mainThumb.style.backgroundImage = `url("${product.images[index]}")`; fadeThumb.style.opacity = '0'; // 페이드 레이어 숨김 updateIndicator(productId, index); } } }; window.handleTouchEnd = (e, productId) => { if (e.cancelable) e.preventDefault(); if (!isDragging) { window.openModal(productId); } else { resetThumbnail(productId); // 드래그 종료 시 확실한 리셋 } isDragging = false; }; // 인디케이터 업데이트 헬퍼 함수 function updateIndicator(productId, index) { const indicator = document.getElementById(`indicator-${productId}`); if (indicator) { Array.from(indicator.children).forEach((dot, i) => { dot.style.backgroundColor = i === index ? 'white' : 'rgba(255, 255, 255, 0.4)'; dot.style.opacity = i === index ? '1' : '0.7'; }); } }