/** * main.js * 앱 진입점: 전역 상태 초기화, 공통 이벤트 바인딩, 뷰 제어 및 유틸리티 기능 */ import { state, productsData, saveSelection } from './state.js'; import { applyFilters, renderStatusChips, renderCategoryChips, renderTagChips, setupTagToggle } 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'; // ========================================================================== // 1. 전역 설정 및 윈도우 객체 등록 (Global Setup) // ========================================================================== // HTML 내 inline onclick 이벤트를 위한 전역 등록 window.openModal = openModal; window.closeModal = closeModal; window.changePage = changePage; window.scrollToImage = scrollToImage; let lastThumbnailIndex = -1; let currentHoverIndex = -1; let fadeTimers = {}; let touchStartX = 0; let isDragging = false; // 로그용 유니크 ID 생성 const sessionId = Math.random().toString(36).substring(2, 10); console.log(`%c[SESSION]: ${sessionId}`, 'color: #137fec; font-weight: bold; border: 1px solid #137fec; padding: 2px 5px; border-radius: 4px;'); // ========================================================================== // 2. 뷰(모드) 제어 로직 (View Mode) // ========================================================================== /** * 뷰 전환 버튼 상태 업데이트 (Grid/Table) */ function updateViewButtons() { const isGrid = state.viewMode === 'grid'; const gridBtn = document.getElementById('view-grid'); const tableBtn = document.getElementById('view-table'); if (!gridBtn || !tableBtn) return; const active = ['bg-white', 'dark:bg-slate-700', 'shadow-sm', 'text-primary']; const inactive = ['text-slate-400']; if (isGrid) { gridBtn.classList.add(...active); gridBtn.classList.remove(...inactive); tableBtn.classList.add(...inactive); tableBtn.classList.remove(...active); } else { tableBtn.classList.add(...active); tableBtn.classList.remove(...inactive); gridBtn.classList.add(...inactive); gridBtn.classList.remove(...active); } } document.getElementById('view-grid').onclick = () => { state.viewMode = 'grid'; updateViewButtons(); renderProducts(state.currentPage); }; document.getElementById('view-table').onclick = () => { state.viewMode = 'table'; updateViewButtons(); renderProducts(state.currentPage); }; // ========================================================================== // 3. 선택 및 요약 로직 (Selection & Summary) // ========================================================================== /** * 선택 요약 바 업데이트 (개수 및 총 가격) */ export function updateSummary() { const summaryBar = document.getElementById('selection-summary'); const countEl = document.getElementById('selected-count'); const priceEl = document.getElementById('selected-total-price'); if (state.selectedIds.size > 0) { summaryBar.classList.remove('hidden'); summaryBar.classList.add('flex'); countEl.textContent = state.selectedIds.size; const total = Array.from(state.selectedIds).reduce((sum, id) => { const p = productsData.find((item) => item.id === id); return sum + (p ? p.price : 0); }, 0); priceEl.textContent = `₩${total.toLocaleString()}`; } else { summaryBar.classList.add('hidden'); summaryBar.classList.remove('flex'); } } /** * 개별 아이템 선택 토글 */ 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.activeStatuses.has(product.status) && state.selectedIds.add(id); saveSelection(); updateSummary(); }; /** * 현재 페이지 전체 선택/해제 */ 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) { isChecked ? state.selectedIds.add(p.id) : state.selectedIds.delete(p.id); } }); saveSelection(); updateSummary(); renderProducts(state.currentPage); }; /** * 선택 초기화 모달 및 실행 */ window.resetSelection = () => { const modal = document.getElementById('selection-reset-modal'); modal.classList.replace('hidden', 'flex'); }; window.closeSelectionResetModal = () => { const modal = document.getElementById('selection-reset-modal'); modal.classList.replace('flex', 'hidden'); }; window.confirmSelectionReset = () => { state.selectedIds.clear(); saveSelection(); updateSummary(); renderProducts(state.currentPage); window.closeSelectionResetModal(); }; // ========================================================================== // 4. 테마 및 검색 이벤트 (Theme & Search) // ========================================================================== /** * 다크/라이트 테마 초기화 및 버튼 바인딩 */ export function initTheme() { const themeToggleBtn = document.getElementById('theme-toggle'); if (!themeToggleBtn) return; const isDark = localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches); document.documentElement.classList.toggle('dark', isDark); updateThemeIcons(); themeToggleBtn.onclick = () => { const willBeDark = !document.documentElement.classList.contains('dark'); document.documentElement.classList.toggle('dark', willBeDark); localStorage.setItem('color-theme', willBeDark ? 'dark' : 'light'); updateThemeIcons(); }; } function updateThemeIcons() { const darkIcon = document.getElementById('theme-toggle-dark-icon'); const lightIcon = document.getElementById('theme-toggle-light-icon'); if (!darkIcon || !lightIcon) return; const isDark = document.documentElement.classList.contains('dark'); lightIcon.classList.toggle('hidden', !isDark); darkIcon.classList.toggle('hidden', isDark); } // 검색어 입력 실시간 반영 document.getElementById('search-input')?.addEventListener('input', (e) => { state.searchKeyword = e.target.value.trim().toLowerCase(); applyFilters(); }); // ESC 키로 모달 닫기 document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeModal(); }); // ========================================================================== // 5. 썸네일 애니메이션 및 인터랙션 (Thumbnail Logic) // ========================================================================== /** * 썸네일 이미지/인디케이터 즉시 업데이트 (마우스 호버용) */ 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) return; if (fadeTimers[productId]) { clearTimeout(fadeTimers[productId]); delete fadeTimers[productId]; } if (fadeThumb) { fadeThumb.style.transition = 'none'; fadeThumb.style.opacity = '0'; } mainThumb.style.backgroundImage = `url("${newImageUrl}")`; if (indicator) updateIndicatorUI(indicator, index); } /** * 썸네일 상태 복구 */ function resetThumbnail(productId) { 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) { const firstImgUrl = `url("${product.images[0]}")`; mainThumb.style.backgroundImage = firstImgUrl; if (fadeThumb) { fadeThumb.style.transition = 'none'; fadeThumb.style.opacity = '0'; fadeThumb.style.backgroundImage = firstImgUrl; } } if (indicator) updateIndicatorUI(indicator, 0); } 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'; }); } // 데스크탑 호버 이벤트 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); } }; window.handleThumbnailLeave = (productId) => { currentHoverIndex = -1; resetThumbnail(productId); }; // 모바일 터치 스와이프 이벤트 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 cardWidth = e.currentTarget.offsetWidth; const diffX = touchStartX - e.touches[0].clientX; 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)); if (lastThumbnailIndex === index) return; lastThumbnailIndex = index; const mainThumb = document.getElementById(`thumb-${productId}`); const fadeThumb = document.getElementById(`thumb-fade-${productId}`); if (mainThumb) { mainThumb.style.backgroundImage = `url("${product.images[index]}")`; if (fadeThumb) fadeThumb.style.opacity = '0'; updateIndicatorUI(document.getElementById(`indicator-${productId}`), index); } } }; window.handleTouchEnd = (e, productId) => { if (!isDragging) { window.openModal(productId); } else { resetThumbnail(productId); } lastThumbnailIndex = -1; isDragging = false; }; // ========================================================================== // 6. 데이터 내보내기 (Export) // ========================================================================== /** * 선택된 항목 CSV 다운로드 */ window.exportToExcel = () => { if (state.selectedIds.size === 0) { alert('내보낼 상품을 선택해 주세요.'); return; } let totalCount = 0; let totalPrice = 0; const rows = Array.from(state.selectedIds) .map((id) => { const p = productsData.find((item) => item.id === id); if (!p) return null; totalCount += 1; totalPrice += p.price; return [p.id, `"${p.title.replace(/"/g, '""')}"`, p.category, p.price, p.status, `"${p.description.replace(/"/g, '""')}"`]; }) .filter(Boolean); const headers = ['상품 ID', '상품명', '카테고리', '가격', '상태', '상세설명']; const footerTotal = ['TOTAL', `"총 ${totalCount}건"`, '', totalPrice, '', `"발행: ${new Date().toLocaleString()}"`]; const csvContent = '\uFEFF' + [headers.join(','), ...rows.map((row) => row.join(',')), ['', '', '', '', '', ''].join(','), footerTotal.join(',')].join('\n'); const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `inventory_receipt_${new Date().toISOString().split('T')[0]}.csv`; link.click(); }; // ========================================================================== // 7. 초기화 (App Initialization) // ========================================================================== document.addEventListener('DOMContentLoaded', () => { console.log('App initialized. Products:', productsData.length); initTheme(); renderCategoryChips(productsData); renderStatusChips(); renderTagChips(); setupTagToggle(); applyFilters(); updateViewButtons(); updateSummary(); });