@@ -24,137 +24,184 @@ export function renderProducts(page = 1) {
if ( ! grid || ! tableWrapper ) return ;
// 1. 결과가 0개인 경우 (그리드/테이블 공통 안내)
// 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">
grid . classList . remove ( 'grid' ) ;
grid . classList . add ( 'hidden' ) ;
tableWrapper . classList . add ( 'hidden' ) ;
// 검색 결과 없음 메시지를 표시할 별도의 컨테이너가 없다면 grid 영역을 빌려 씁니다.
const emptyMsg = `
<div class="col-span-full 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' ) ;
grid . innerHTML = emptyMsg ;
grid . classList . remove ( 'hidden' ) ;
if ( paginationContainer ) paginationContainer . innerHTML = '' ;
return ;
}
// 2. 뷰 모드에 따른 컨테이너 노출 설 정
// 2. 뷰 모드 설정 및 컨테이너 노출 정리 (hidden/flex/grid 충돌 방지)
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 . remove ( 'flex' ) ;
summaryBar . classList . add ( 'hidden' ) ;
}
} else {
// 그리드 비활성화
grid . classList . remove ( 'grid' ) ;
grid . classList . add ( 'hidden' ) ;
// 테이블 활성화
tableWrapper . classList . remove ( 'hidden' ) ;
// 요약바 노출 여부는 데이터 상태에 따라 updateSummary에서 결정
updateSummary ( ) ;
updateSummary ( ) ; // 테이블일 때만 요약바 노출 여부 결정
}
// 3. 현재 페이지 데이터 슬라이싱
// 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>
// 1. 상태 판별
const isSold = STATUS _META [ product . status ] ? . soldOut === true ;
const isNonSale = product . status === '미판매' ; // 상태값이 '미판매'일 때
// 2. 스펙(Condition) 정보 추출
const conditionKey = product . specs ? . condition ;
const conditionConfig = PRODUCT _CONDITIONS [ conditionKey ] ;
const conditionDisplay = conditionConfig ? conditionConfig . label : ( conditionKey || '' ) ;
grid . insertAdjacentHTML ( 'beforeend' , `
<div class="product-card group flex flex-col gap-4 cursor-pointer"
data-id=" ${ product . id } "
onclick="window.openModal(' ${ product . id } ')"
${ ! isSold ? ` onmousemove="window.handleThumbnailHover(event, ' ${ product . id } ')"
onmouseleave="window.handleThumbnailLeave(' ${ product . id } ')" ` : '' } >
<div class="relative w-full aspect-card bg-slate-200 dark:bg-slate-800 rounded-xl overflow-hidden shadow-sm" >
<div id="thumb- ${ product . i d} "
class="w-full h-full bg-center bg-no-repeat bg-cover transform ${ isSold ? 'grayscale opacity-60' : 'group-hover:scale-105 transition-transform duration-500' } "
style="background-image: none; will-change: background-image;">
</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' ;
<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>
${ ! isSold && product . images ? . length > 1 ? `
<div id="indicator- ${ product . id } " class="absolute bottom-3 left-1/2 -translate-x-1/2 flex gap-1.5 opacity-0 group-hover:opacity-100 transition-all duration-300 pointer-events-none">
${ product . images . map ( ( _ , i ) => `
<div class="w-1.5 h-1.5 rounded-full transition-all duration-300 shadow-sm ${ i === 0 ? 'bg-white scale-125' : 'bg-white/40' } "></div>
` ) . join ( '' ) }
</div>
` : '' }
</div>
<div class="flex flex-col gap-1.5">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-baseline 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-base font-bold whitespace-nowrap ${ isNonSale ? 'text-slate-400 font-medium text-xs uppercase' : 'text-slate-900 dark:text-white' } ">
${ isNonSale ? 'Not for Sale' : ` ₩ ${ product . price . toLocaleString ( ) } ` }
</p>
</div>
<div class="flex flex-col">
${ conditionDisplay ? ` <span class="text-[11px] font-medium text-slate-400 mb-0.5"> ${ conditionDisplay } </span> ` : '' }
<p class="text-slate-500 dark:text-slate-400 text-sm font-normal line-clamp-1 italic">
${ product . description }
</p>
</div>
</div>
</div>
` ) ;
} ) ;
// 지연 로딩(Lazy Loading) 관찰자 설정
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 ) {
// 1. 첫 번째 이미지 로드
thumb . style . backgroundImage = ` url(" ${ product . images [ 0 ] } ") ` ;
// 2. 마우스 올리기 전, 나머지 이미지들 백그라운드 프리로드
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 } ) ;
// 2. 행 전체 스타일 및 제목 스타일
const rowClass = isSold ? 'opacity-50 grayscale-[0.5]' : '' ;
const titleClass = isSold ? 'line-through text-slate-400' : 'text-slate-900 dark:text-white' ;
document . querySelectorAll ( '.product-card' ) . forEach ( card => observer . observe ( card ) ) ;
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 ( '' ) ;
} else {
// 테이블 렌더링
tableBody . innerHTML = pagedProducts . map ( ( product ) => {
const meta = STATUS _META [ product . status ] ;
const isSold = meta ? . soldOut === true ;
const isSelectable = meta ? . selectable !== false ;
const conditionKey = product . specs . condition ;
const conditionConfig = PRODUCT _CONDITIONS [ conditionKey ] ;
let conditionDisplay = conditionConfig ? conditionConfig . label : ( conditionKey || '상세 설명 참고 ℹ ️ ' ) ;
let conditionClass = conditionConfig ? conditionConfig . color : 'text-slate-500' ;
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('판매 완료된 상품입니다.');" : ` window.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 ) ;
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 ( ) ;
// 페이지네이션 함수 호출 (이 함수는 외부에 정의되어 있어야 함)
if ( typeof renderPagination === 'function' ) {
renderPagination ( ) ;
}
}
export function renderPagination ( ) {