Files
zenn.inventory/scripts/main.js
zenn 526d310ac9 - 코드 정리
- 경고 클래스 수정
- 카드 비율 수정 3:4 > 4:5
2026-02-10 11:30:45 +09:00

244 lines
8.6 KiB
JavaScript

/** 진입점: 이벤트 바인딩·초기 렌더·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;
// 뷰 전환 이벤트
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);
};
/** 선택 리셋 */
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);
};