Files
zenn.inventory/scripts/productList.js
zenn b268a97e0f [260211] 상품 컨디션 정의
- 가격 누락 복구
2026-02-11 00:27:42 +09:00

186 lines
9.2 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 } from './state.js';
import { ITEMS_PER_PAGE, STATUS_META, STATUS_COLOR, PRODUCT_CONDITIONS } 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 || !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 group-hover:shadow-md 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 isSelectable = STATUS_META[product.status]?.selectable !== false;
const conditionKey = product.specs.condition;
const conditionConfig = PRODUCT_CONDITIONS[conditionKey];
let conditionDisplay = '';
let conditionClass = 'text-slate-500';
if (conditionConfig) {
// 1. 정의된 Key인 경우 (추천 방식)
conditionDisplay = conditionConfig.label;
conditionClass = conditionConfig.color;
} else if (conditionKey && conditionKey.trim() !== '') {
// 2. 정의되지 않았지만 텍스트가 있는 경우 (기존 수기 입력 데이터)
conditionDisplay = conditionKey;
} else {
// 3. 데이터가 비어있는 경우
conditionDisplay = '상세 설명 참고 ';
conditionClass = 'text-indigo-500 italic';
}
return `
<tr class="hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors cursor-pointer" onclick="if(event.target.type !== 'checkbox') openModal('${product.id}')">
<td class="py-4 px-4 text-center" onclick="event.stopPropagation()">
<input type="checkbox" ...>
</td>
<td class="py-4 px-4 font-semibold 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:block 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);
});
}