208 lines
10 KiB
JavaScript
208 lines
10 KiB
JavaScript
/** 상품 그리드·페이지네이션 렌더링 */
|
||
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);
|
||
});
|
||
}
|