Files
zenn.inventory/scripts/productList.js
2026-02-11 02:35:33 +09:00

208 lines
10 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** 상품 그리드·페이지네이션 렌더링 */
import { state, saveSelection } from './state.js';
import { ITEMS_PER_PAGE, STATUS_META, STATUS_COLOR, PRODUCT_CONDITIONS } from './config.js';
import { updateSummary } from './main.js';
import { openModal } from './modal.js';
// 1. 체크박스 전역 핸들러 등록
window.toggleSelectItem = function(id) {
if (state.selectedIds.has(id)) {
state.selectedIds.delete(id);
} else {
state.selectedIds.add(id);
}
saveSelection();
renderProducts(state.currentPage);
updateSummary();
};
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 || !tableWrapper) return;
// 1. 결과가 0개인 경우 (그리드/테이블 공통 안내)
if (state.visibleProducts.length === 0) {
grid.classList.remove('grid', 'hidden');
grid.innerHTML = `
<div class="flex flex-col items-center justify-center py-20 w-full text-center">
<span class="material-symbols-outlined text-6xl text-slate-300 dark:text-slate-700 mb-4">search_off</span>
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-2">검색 결과가 없습니다</h3>
<p class="text-slate-500 dark:text-slate-400 text-sm">입력하신 검색어나 선택한 필터를 확인해 주세요.</p>
</div>
`;
tableWrapper.classList.add('hidden');
if (paginationContainer) paginationContainer.innerHTML = '';
return;
}
// 2. 뷰 모드에 따른 컨테이너 노출 설정
if (state.viewMode === 'grid') {
// 그리드 활성화
grid.classList.remove('hidden');
grid.classList.add('grid');
// 테이블 및 요약바 비활성화
tableWrapper.classList.add('hidden');
if (summaryBar) {
summaryBar.classList.remove('flex'); // flex 제거
summaryBar.classList.add('hidden');
}
} else {
// 그리드 비활성화
grid.classList.remove('grid');
grid.classList.add('hidden');
// 테이블 활성화
tableWrapper.classList.remove('hidden');
// 요약바 노출 여부는 데이터 상태에 따라 updateSummary에서 결정
updateSummary();
}
// 3. 현재 페이지 데이터 슬라이싱
const startIndex = (page - 1) * ITEMS_PER_PAGE;
const pagedProducts = state.visibleProducts.slice(startIndex, startIndex + ITEMS_PER_PAGE);
if (state.viewMode === 'grid') {
grid.innerHTML = '';
pagedProducts.forEach((product) => {
const isSold = STATUS_META[product.status]?.soldOut === true;
grid.insertAdjacentHTML('beforeend', `
<div class="group flex flex-col gap-4 cursor-pointer" onclick="openModal('${product.id}')">
<div class="relative w-full aspect-card bg-slate-50 dark:bg-slate-800 rounded-xl overflow-hidden shadow-sm transition-shadow">
<div class="w-full h-full bg-center bg-no-repeat bg-cover transform ${isSold ? 'grayscale opacity-80' : 'group-hover:scale-105'} transition-transform duration-500"
style="background-image: url('${product.images[0]}')"></div>
<div class="absolute top-3 left-3">
<span class="px-2 py-1 text-[10px] uppercase tracking-wider font-bold rounded ${STATUS_COLOR[product.status]} backdrop-blur-md border">
${product.status}
</span>
</div>
</div>
<div class="flex flex-col gap-1">
<div class="flex flex-col sm:flex-row justify-between items-start gap-1">
<h3 class="text-slate-900 dark:text-white text-base font-semibold leading-tight break-keep ${isSold ? 'line-through text-slate-400' : ''}">${product.title}</h3>
<p class="text-slate-900 dark:text-white text-base font-bold text-nowrap">${product.currency}${product.price.toLocaleString()}</p>
</div>
<p class="text-slate-500 dark:text-slate-400 text-sm font-normal line-clamp-1">${product.description}</p>
</div>
</div>
`);
});
} else {
// [테이블 렌더링: 스타일 & 체크박스 제어 추가]
tableBody.innerHTML = pagedProducts
.map((product) => {
const meta = STATUS_META[product.status];
const isSold = meta?.soldOut === true;
const isSelectable = meta?.selectable !== false;
// 1. 상태(Condition) 설정 로직
const conditionKey = product.specs.condition;
const conditionConfig = PRODUCT_CONDITIONS[conditionKey];
let conditionDisplay = '';
let conditionClass = 'text-slate-500';
if (conditionConfig) {
conditionDisplay = conditionConfig.label;
conditionClass = conditionConfig.color;
} else if (conditionKey && conditionKey.trim() !== '') {
conditionDisplay = conditionKey;
} else {
conditionDisplay = '상세 설명 참고 ';
conditionClass = 'text-indigo-500 italic';
}
// 2. 행 전체 스타일 및 제목 스타일
const rowClass = isSold ? 'opacity-50 grayscale-[0.5]' : '';
const titleClass = isSold ? 'line-through text-slate-400' : 'text-slate-900 dark:text-white';
return `
<tr class="hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors ${isSold ? 'opacity-50 grayscale cursor-not-allowed' : 'cursor-pointer'}"
onclick="if(event.target.type !== 'checkbox') {
${isSold ? "alert('판매 완료된 상품은 상세 정보를 볼 수 없습니다.');" : `openModal('${product.id}')`}
}">
<td class="py-4 px-4 text-center" onclick="event.stopPropagation()">
<input type="checkbox"
class="product-check rounded border-slate-300 w-4 h-4 ${isSelectable ? 'cursor-pointer text-primary' : 'opacity-20 cursor-not-allowed'}"
${state.selectedIds.has(product.id) ? 'checked' : ''}
${isSelectable ? '' : 'disabled'}
onchange="window.toggleSelectItem('${product.id}')">
</td>
<td class="py-4 px-4 font-semibold ${isSold ? 'line-through text-slate-400' : 'text-slate-900 dark:text-white'}">${product.title}</td>
<td class="py-4 px-4 text-xs break-keep ${conditionClass}">${conditionDisplay}</td>
<td class="py-4 px-4 text-right font-bold text-slate-900 dark:text-white">
${product.price.toLocaleString()}
</td>
<td class="hidden lg:table-cell py-4 px-4 text-center">
<span class="px-2 py-0.5 rounded text-[10px] font-bold border ${STATUS_COLOR[product.status]}">${product.status}</span>
</td>
</tr>`;
})
.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();
}
export function renderPagination() {
const container = document.getElementById('pagination');
if (!container) return;
const totalPages = Math.ceil(state.visibleProducts.length / ITEMS_PER_PAGE);
const { currentPage } = state;
let html = `<button onclick="changePage(${currentPage - 1})" class="size-10 flex items-center justify-center ${currentPage === 1 ? 'invisible' : ''}">
<svg viewBox="0 0 24 24" fill="none" stroke="#64748B" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="w-5 h-5">
<path d="M15 18l-6-6 6-6" />
</svg>
</button>`;
for (let i = 1; i <= totalPages; i++) {
html += `<button onclick="changePage(${i})" class="size-10 font-bold rounded-lg ${i === currentPage ? 'bg-primary text-white' : 'text-slate-500'}">${i}</button>`;
}
html += `<button onclick="changePage(${currentPage + 1})" class="size-10 flex items-center justify-center ${currentPage === totalPages ? 'invisible' : ''}">
<svg viewBox="0 0 24 24" fill="none" stroke="#64748B" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="w-5 h-5">
<path d="M9 18l6-6-6-6" />
</svg>
</button>`;
container.innerHTML = html;
}
export function changePage(page) {
state.currentPage = page;
renderProducts(page);
window.scrollTo({ top: 0, behavior: 'smooth' });
}
/** 모든 필터를 초기 상태로 되돌리는 함수 */
function resetAllFilters() {
state.searchKeyword = '';
const searchInput = document.getElementById('search-input');
if (searchInput) searchInput.value = '';
state.activeCategories.clear();
state.activeCategories.add('All');
// 상태 필터 초기화 (config에서 defaultActive인 것만)
import('./config.js').then(({ STATUS_FILTERS }) => {
state.activeStatuses.clear();
STATUS_FILTERS.filter((f) => f.defaultActive).forEach((f) => state.activeStatuses.add(f.key));
// UI 전체 갱신
applyFilters();
renderStatusChips();
renderCategoryChips(productsData);
});
}