diff --git a/index.html b/index.html index 53106e5..193203d 100644 --- a/index.html +++ b/index.html @@ -137,13 +137,58 @@ Total Results 0 + +
+ + +
+ +
+
@@ -225,18 +270,18 @@
- + - - chat_bubble - 오픈카톡으로 문의하기 - + + chat_bubble + 오픈카톡으로 문의하기 + -

링크를 복사해 문의 시 전달해주세요.

-
+

링크를 복사해 문의 시 전달해주세요.

+ diff --git a/scripts/main.js b/scripts/main.js index f039706..577f8d6 100644 --- a/scripts/main.js +++ b/scripts/main.js @@ -1,11 +1,12 @@ /** 진입점: 이벤트 바인딩·초기 렌더·URL 모달 처리 */ -import { state, productsData } from './state.js'; +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'; @@ -18,6 +19,58 @@ window.closeModal = closeModal; window.changePage = changePage; window.scrollToImage = scrollToImage; +// 뷰 전환 이벤트 +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 (state.viewMode === 'table' && state.selectedIds.size > 0) { + 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 { + summary.classList.add('hidden'); + summary.classList.remove('flex'); + } +} + // 검색 입력 const searchInput = document.getElementById('search-input'); if (searchInput) { @@ -98,3 +151,104 @@ document.addEventListener('DOMContentLoaded', () => { 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); +}; + +/** 선택 리셋 */ +window.resetSelection = () => { + if (!confirm("선택된 내역을 모두 초기화할까요?")) return; + state.selectedIds.clear(); + saveSelection(); // 스토리지 동기화 + updateSummary(); + renderProducts(state.currentPage); +}; + +/** 선택 토글 시 스토리지 저장 추가 */ +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); +}; \ No newline at end of file diff --git a/scripts/productList.js b/scripts/productList.js index 1560968..778d6a1 100644 --- a/scripts/productList.js +++ b/scripts/productList.js @@ -1,63 +1,100 @@ /** 상품 그리드·페이지네이션 렌더링 */ import { state } from './state.js'; -import { ITEMS_PER_PAGE, STATUS_META } from './config.js'; +import { ITEMS_PER_PAGE, STATUS_META, STATUS_COLOR } from './config.js'; +import { updateSummary } from './main.js'; export function renderProducts(page = 1) { const grid = document.getElementById('product-grid'); + const tableWrapper = document.getElementById('product-table-wrapper'); + const tableBody = document.getElementById('product-table-body'); + const summaryBar = document.getElementById('selection-summary'); const paginationContainer = document.getElementById('pagination'); - if (!grid) return; - // 1. 결과가 0개인 경우 (안내 텍스트만 출력) + if (!grid || !tableWrapper) return; + + // 1. 결과가 0개인 경우 (그리드/테이블 공통 안내) if (state.visibleProducts.length === 0) { - grid.classList.remove('grid'); // 중앙 정렬을 위해 그리드 해제 + grid.classList.remove('grid', 'hidden'); grid.innerHTML = `
- - search_off - + search_off

검색 결과가 없습니다

-

- 입력하신 검색어나 선택한 필터를 확인해 주세요. -

+

입력하신 검색어나 선택한 필터를 확인해 주세요.

`; - + tableWrapper.classList.add('hidden'); if (paginationContainer) paginationContainer.innerHTML = ''; return; } - // 2. 결과가 있을 경우 (그리드 복구 및 초기화) - grid.classList.add('grid'); - grid.innerHTML = ''; + // 2. 뷰 모드에 따른 컨테이너 노출 설정 + if (state.viewMode === 'grid') { + grid.classList.remove('hidden'); + grid.classList.add('grid'); + tableWrapper.classList.add('hidden'); + summaryBar.classList.add('hidden'); + } else { + grid.classList.add('hidden'); + tableWrapper.classList.remove('hidden'); + updateSummary(); + } + // 3. 현재 페이지 데이터 슬라이싱 const startIndex = (page - 1) * ITEMS_PER_PAGE; const pagedProducts = state.visibleProducts.slice(startIndex, startIndex + ITEMS_PER_PAGE); - // 상품 카드 생성 로직 - pagedProducts.forEach((product) => { - const isSold = STATUS_META[product.status]?.soldOut === true; - const cardHtml = ` -
-
-
-
- - ${product.status} - + if (state.viewMode === 'grid') { + grid.innerHTML = ''; + pagedProducts.forEach((product) => { + const isSold = STATUS_META[product.status]?.soldOut === true; + grid.insertAdjacentHTML('beforeend', ` +
+
+
+
+ + ${product.status} + +
-
-
-
+

${product.title}

-

${product.currency}${product.price.toLocaleString()}

+

${product.description}

-

${product.description}

-
- `; - grid.insertAdjacentHTML('beforeend', cardHtml); - }); + `); + }); + } else { + // 테이블 렌더링 (이전과 동일, 가격 포함) + tableBody.innerHTML = pagedProducts.map(product => { + const isSelectable = STATUS_META[product.status]?.selectable !== false; + return ` + + + + + ${product.title} + ${product.category} + ₩${product.price.toLocaleString()} + + ${product.status} + + `; + }).join(''); + } + + // [중요] 전체 선택 체크박스 상태 동기화 + const selectAllCheck = document.getElementById('select-all-current'); + if (selectAllCheck) { + const startIndex = (page - 1) * ITEMS_PER_PAGE; + const currentSelectableItems = state.visibleProducts.slice(startIndex, startIndex + ITEMS_PER_PAGE).filter((p) => STATUS_META[p.status]?.selectable !== false); + + selectAllCheck.checked = currentSelectableItems.length > 0 && currentSelectableItems.every((p) => state.selectedIds.has(p.id)); + } renderPagination(); } @@ -106,11 +143,11 @@ function resetAllFilters() { // 상태 필터 초기화 (config에서 defaultActive인 것만) import('./config.js').then(({ STATUS_FILTERS }) => { state.activeStatuses.clear(); - STATUS_FILTERS.filter(f => f.defaultActive).forEach(f => state.activeStatuses.add(f.key)); - + STATUS_FILTERS.filter((f) => f.defaultActive).forEach((f) => state.activeStatuses.add(f.key)); + // UI 전체 갱신 applyFilters(); renderStatusChips(); renderCategoryChips(productsData); }); -} \ No newline at end of file +} diff --git a/scripts/state.js b/scripts/state.js index 8a36004..a1ca608 100644 --- a/scripts/state.js +++ b/scripts/state.js @@ -4,14 +4,24 @@ import { STATUS_META } from './config.js'; export const productsData = products; +// 초기 로드 시 세션 스토리지에서 선택 내역 불러오기 +const savedIds = JSON.parse(sessionStorage.getItem('selectedProductIds') || '[]'); + export const state = { currentPage: 1, activeCategories: new Set(['All']), visibleProducts: [...products], searchKeyword: '', + viewMode: 'grid', // 기본값 + selectedIds: new Set(savedIds), activeStatuses: new Set( Object.entries(STATUS_META) .filter(([_, meta]) => meta.defaultVisible) .map(([status]) => status), ), }; + +// 선택 내역이 변경될 때마다 세션 스토리지에 저장하는 헬퍼 함수 +export function saveSelection() { + sessionStorage.setItem('selectedProductIds', JSON.stringify(Array.from(state.selectedIds))); +} \ No newline at end of file