/**
* productList.js
* 상품 그리드/테이블 렌더링, 지연 로딩 및 페이지네이션 제어
*/
import { state, saveSelection } from './state.js';
import { ITEMS_PER_PAGE, STATUS_META, STATUS_COLOR, PRODUCT_CONDITIONS } from './config.js';
import { updateSummary } from './main.js';
// ==========================================================================
// 1. 터치 및 호버 인터랙션 (Interaction)
// ==========================================================================
window.isDragging = false;
let touchStartX = 0;
let touchStartY = 0;
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
/** 터치 시작: 드래그 여부 판단 초기화 */
window.handleTouchStart = function (e) {
window.isDragging = false;
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
};
/** 터치 이동: 일정 거리 이상 움직이면 클릭이 아닌 드래그로 간주 */
window.handleTouchMove = function (e) {
const touchX = e.touches[0].clientX;
const touchY = e.touches[0].clientY;
if (Math.abs(touchX - touchStartX) > 10 || Math.abs(touchY - touchStartY) > 10) {
window.isDragging = true;
}
};
window.handleTouchEnd = function (e) {};
/** 썸네일 호버: 마우스 위치에 따른 이미지 교체 및 인디케이터 업데이트 */
window.handleThumbnailHover = function (e, id) {
if (isTouchDevice) return;
const product = state.visibleProducts.find((p) => p.id === id);
if (!product || !product.images || product.images.length <= 1) return;
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const sectionWidth = rect.width / product.images.length;
const index = Math.floor(x / sectionWidth);
const thumb = document.getElementById(`thumb-${id}`);
const indicators = document.querySelector(`#indicator-${id}`)?.children;
if (thumb && product.images[index]) {
thumb.style.backgroundImage = `url("${product.images[index]}")`;
}
if (indicators) {
Array.from(indicators).forEach((dot, i) => {
dot.classList.toggle('bg-white', i === index);
dot.classList.toggle('scale-125', i === index);
dot.classList.toggle('bg-white/40', i !== index);
});
}
};
/** 호버 해제: 첫 번째 이미지로 복구 */
window.handleThumbnailLeave = function (id) {
if (isTouchDevice) return;
const product = state.visibleProducts.find((p) => p.id === id);
const thumb = document.getElementById(`thumb-${id}`);
const indicators = document.querySelector(`#indicator-${id}`)?.children;
if (thumb && product) thumb.style.backgroundImage = `url("${product.images[0]}")`;
if (indicators) {
Array.from(indicators).forEach((dot, i) => {
dot.classList.toggle('bg-white', i === 0);
dot.classList.toggle('scale-125', i === 0);
dot.classList.toggle('bg-white/40', i !== 0);
});
}
};
/** 개별 체크박스 토글 */
window.toggleSelectItem = function (id) {
if (state.selectedIds.has(id)) state.selectedIds.delete(id);
else state.selectedIds.add(id);
saveSelection();
renderProducts(state.currentPage);
updateSummary();
};
// ==========================================================================
// 2. 메인 렌더링 컨트롤러 (Main Rendering)
// ==========================================================================
/**
* 상태에 따른 상품 목록 렌더링 실행
* @param {number} page - 현재 페이지 번호
*/
export function renderProducts(page = 1) {
const grid = document.getElementById('product-grid');
const tableWrapper = document.getElementById('product-table-wrapper');
const paginationContainer = document.getElementById('pagination');
if (!grid || !tableWrapper) return;
// [1] 결과 없음 처리
if (state.visibleProducts.length === 0) {
renderEmpty(grid, tableWrapper, paginationContainer);
return;
}
// [2] 페이지 데이터 슬라이싱
const startIndex = (page - 1) * ITEMS_PER_PAGE;
const pagedProducts = state.visibleProducts.slice(startIndex, startIndex + ITEMS_PER_PAGE);
// [3] 뷰 모드에 따른 렌더링
if (state.viewMode === 'grid') {
renderGridView(grid, tableWrapper, pagedProducts);
} else {
renderTableView(grid, tableWrapper, pagedProducts);
}
updateSelectAllCheckbox(page);
renderPagination();
}
/** 검색 결과가 없을 때의 UI */
function renderEmpty(grid, tableWrapper, paginationContainer) {
grid.classList.remove('grid');
grid.classList.add('hidden');
tableWrapper.classList.add('hidden');
const emptyMsg = `
검색 결과가 없습니다
입력하신 검색어나 선택한 필터를 확인해 주세요.
`;
grid.innerHTML = emptyMsg;
grid.classList.remove('hidden');
if (paginationContainer) paginationContainer.innerHTML = '';
}
// ==========================================================================
// 3. 그리드 & 테이블 뷰 상세 (View Details)
// ==========================================================================
/** 그리드 뷰 렌더링 */
function renderGridView(grid, tableWrapper, products) {
// 1. 표시 모드 전환 (테이블 숨기고 그리드 표시)
tableWrapper.classList.add('hidden');
grid.classList.remove('hidden');
// 2. [핵심] 렌더링 시 그리드 클래스 복구 및 보장
grid.classList.add('grid');
const summaryBar = document.getElementById('selection-summary');
if (summaryBar) summaryBar.classList.add('hidden');
grid.innerHTML = products.map((product) => createProductCardHTML(product)).join('');
setupLazyLoading();
}
/** 그리드 개별 카드 HTML */
function createProductCardHTML(product) {
const isSold = STATUS_META[product.status]?.soldOut === true;
const isNonSale = product.status === '미판매';
const conditionConfig = PRODUCT_CONDITIONS[product.specs?.condition];
const conditionDisplay = conditionConfig ? conditionConfig.label : product.specs?.condition || '';
return `
${product.status}
${
!isSold && product.images?.length > 1
? `
${product.images.map((_, i) => `
`).join('')}
`
: ''
}
${product.title}
${isNonSale ? 'Not for Sale' : `${product.currency || '₩'}${product.price.toLocaleString()}`}
${conditionDisplay ? `
${conditionDisplay}` : ''}
${product.description}
`;
}
/** 테이블 뷰 렌더링 */
function renderTableView(grid, tableWrapper, products) {
grid.classList.add('hidden');
tableWrapper.classList.remove('hidden');
const tableBody = document.getElementById('product-table-body');
tableBody.innerHTML = products.map((product) => createTableRowHTML(product)).join('');
updateSummary();
}
/** 테이블 개별 행 HTML */
function createTableRowHTML(product) {
const meta = STATUS_META[product.status];
const isSold = meta?.soldOut === true;
const isSelectable = meta?.selectable !== false;
const conditionConfig = PRODUCT_CONDITIONS[product.condition];
const conditionDisplay = conditionConfig ? conditionConfig.label : '상세 설명 참고 ℹ️';
const conditionClass = conditionConfig ? conditionConfig.color : 'text-slate-500';
return `
|
|
${product.title} |
${conditionDisplay} |
₩${product.price.toLocaleString()} |
${product.status}
|
`;
}
// ==========================================================================
// 4. 유틸리티 및 페이지네이션 (Utils)
// ==========================================================================
/** 전체 선택 체크박스 상태 동기화 */
function updateSelectAllCheckbox(page) {
const selectAllCheck = document.getElementById('select-all-current');
if (!selectAllCheck) return;
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));
}
/** Intersection Observer를 이용한 썸네일 지연 로딩 */
function setupLazyLoading() {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const card = entry.target;
const productId = card.getAttribute('data-id');
const product = state.visibleProducts.find((p) => p.id === productId);
const thumb = document.getElementById(`thumb-${productId}`);
if (product && thumb) {
thumb.style.backgroundImage = `url("${product.images[0]}")`;
// 마우스 호버를 대비해 나머지 이미지 미리 로드
if (!STATUS_META[product.status]?.soldOut && product.images.length > 1) {
product.images.slice(1).forEach((url) => {
const img = new Image();
img.src = url;
});
}
}
observer.unobserve(card);
}
});
},
{ threshold: 0.1 },
);
document.querySelectorAll('.product-card').forEach((card) => observer.observe(card));
}
/** 페이지네이션 버튼 렌더링 */
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 = ``;
for (let i = 1; i <= totalPages; i++) {
html += ``;
}
html += ``;
container.innerHTML = html;
}
/** 페이지 변경 처리 */
export function changePage(page) {
state.currentPage = page;
renderProducts(page);
window.scrollTo({ top: 0, behavior: 'smooth' });
}