오류 수정
This commit is contained in:
@@ -88,7 +88,7 @@ const games = [
|
||||
price: 60000,
|
||||
currency: '₩',
|
||||
category: 'Games',
|
||||
status: '판매중',
|
||||
status: '판매완료',
|
||||
customTag: '',
|
||||
|
||||
tags: ['Switch', 'JP'],
|
||||
|
||||
16
index.html
16
index.html
@@ -288,6 +288,22 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- selection-reset-modal -->
|
||||
<div id="selection-reset-modal" class="fixed inset-0 z-[100] hidden items-center justify-center p-4 bg-slate-900/60 backdrop-blur-sm">
|
||||
<div class="bg-white dark:bg-slate-900 rounded-2xl p-6 max-w-sm w-full shadow-2xl scale-95 transition-transform duration-200">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="size-12 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center mb-4">
|
||||
<span class="material-symbols-outlined text-red-600">delete_sweep</span>
|
||||
</div>
|
||||
<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 mb-6">현재 체크된 모든 상품의 선택이 해제됩니다.<br>계속하시겠습니까?</p>
|
||||
<div class="flex gap-3 w-full">
|
||||
<button onclick="closeSelectionResetModal()" class="flex-1 py-3 px-4 rounded-xl font-semibold text-slate-600 dark:text-slate-400 bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 transition-colors">취소</button>
|
||||
<button onclick="confirmSelectionReset()" class="flex-1 py-3 px-4 rounded-xl font-semibold text-white bg-red-500 hover:bg-red-600 transition-colors">초기화</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
<footer class="px-6 md:px-40 py-12 border-t border-slate-200 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50">
|
||||
<div class="max-w-3xl mb-10">
|
||||
|
||||
@@ -2,40 +2,46 @@
|
||||
|
||||
export const ITEMS_PER_PAGE = 20;
|
||||
|
||||
export const VISIBILITY_CONFIG = {
|
||||
showUnlisted: false,
|
||||
showSold: true,
|
||||
};
|
||||
// export const VISIBILITY_CONFIG = {
|
||||
// showUnlisted: false,
|
||||
// showSold: true,
|
||||
// };
|
||||
|
||||
export const STATUS_META = {
|
||||
미판매: {
|
||||
selectable: false,
|
||||
defaultVisible: false,
|
||||
selectable: false, // 체크박스 선택 불가
|
||||
isDefaultActive: false, // 초기 로드 시 미체크 상태
|
||||
isSystemVisible: false, // 아예 리스트/필터에서 제외 (완전 숨김)
|
||||
soldOut: false,
|
||||
},
|
||||
판매예정: {
|
||||
selectable: true,
|
||||
defaultVisible: true,
|
||||
selectable: false,
|
||||
isDefaultActive: true,
|
||||
isSystemVisible: true,
|
||||
soldOut: false,
|
||||
},
|
||||
판매중: {
|
||||
selectable: true,
|
||||
defaultVisible: true,
|
||||
isDefaultActive: true,
|
||||
isSystemVisible: true,
|
||||
soldOut: false,
|
||||
},
|
||||
판매완료: {
|
||||
selectable: true,
|
||||
defaultVisible: false,
|
||||
soldOut: true,
|
||||
selectable: false,
|
||||
isDefaultActive: false, // 초기에는 안 보이지만, 사용자가 필터 클릭하면 보임
|
||||
isSystemVisible: true,
|
||||
soldOut: true, // 이미지에 SOLD OUT 표시
|
||||
},
|
||||
};
|
||||
|
||||
export const STATUS_FILTERS = [
|
||||
{ key: '판매중', label: '판매중', defaultActive: true, visible: true },
|
||||
{ key: '판매예정', label: '판매 예정', defaultActive: true, visible: true },
|
||||
{ key: '미판매', label: '미판매', defaultActive: false, visible: VISIBILITY_CONFIG.showUnlisted },
|
||||
{ key: '판매완료', label: '판매완료', defaultActive: false, visible: VISIBILITY_CONFIG.showSold },
|
||||
];
|
||||
// STATUS_FILTERS를 수동으로 만들지 않고 META에서 자동으로 생성합니다.
|
||||
export const STATUS_FILTERS = Object.keys(STATUS_META)
|
||||
.filter(key => STATUS_META[key].isSystemVisible) // 시스템 가시성이 true인 것만 필터 칩 생성
|
||||
.map(key => ({
|
||||
key: key,
|
||||
label: key === '판매예정' ? '판매 예정' : key,
|
||||
defaultActive: STATUS_META[key].isDefaultActive
|
||||
}));
|
||||
|
||||
export const STATUS_ORDER = {
|
||||
판매중: 0,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** 상태·카테고리·검색 필터 로직 및 UI */
|
||||
import { state, productsData } from './state.js';
|
||||
import { VISIBILITY_CONFIG, STATUS_FILTERS, STATUS_ORDER, STATUS_COLOR, SEARCH_CONFIG } from './config.js';
|
||||
import { ITEMS_PER_PAGE, STATUS_META, STATUS_FILTERS, STATUS_ORDER, STATUS_COLOR, SEARCH_CONFIG, } from './config.js';
|
||||
import { renderProducts } from './productList.js';
|
||||
|
||||
function getStatusChipClass(status, isActive) {
|
||||
@@ -16,9 +16,12 @@ export function renderStatusChips() {
|
||||
if (!container) return;
|
||||
container.innerHTML = '';
|
||||
|
||||
STATUS_FILTERS.filter((f) => f.visible).forEach(({ key, label }) => {
|
||||
// 이제 config에서 자동 생성된 STATUS_FILTERS를 사용합니다.
|
||||
STATUS_FILTERS.forEach(({ key, label }) => {
|
||||
const isActive = state.activeStatuses.has(key);
|
||||
|
||||
const chip = document.createElement('button');
|
||||
// getStatusChipClass 함수가 기존에 정의되어 있다면 그대로 사용하세요.
|
||||
chip.className = `status-chip px-3 py-1.5 md:px-4 md:py-2 rounded-full text-xs md:text-sm font-medium transition-all duration-200 border ${getStatusChipClass(key, isActive)}`;
|
||||
chip.textContent = label;
|
||||
chip.onclick = () => toggleStatusFilter(key);
|
||||
@@ -39,37 +42,37 @@ function toggleStatusFilter(status) {
|
||||
renderStatusChips();
|
||||
}
|
||||
|
||||
// [핵심] 필터 적용 함수
|
||||
export function applyFilters() {
|
||||
state.currentPage = 1;
|
||||
// [수정] 무조건 1페이지로 초기화하지 않고, 나중에 데이터 개수에 맞춰 계산합니다.
|
||||
const keyword = state.searchKeyword.toLowerCase();
|
||||
|
||||
// 1. 데이터 필터링 및 정렬
|
||||
state.visibleProducts = productsData
|
||||
.filter((product) => {
|
||||
// [1] 가시성 및 상태/카테고리 필터
|
||||
if (product.status === '미판매' && !VISIBILITY_CONFIG.showUnlisted) return false;
|
||||
if (product.status === '판매완료' && !VISIBILITY_CONFIG.showSold) return false;
|
||||
const meta = STATUS_META[product.status];
|
||||
|
||||
// [1] 시스템 가시성 체크 (isSystemVisible이 false면 목록에서 완전히 제외)
|
||||
if (!meta || !meta.isSystemVisible) return false;
|
||||
|
||||
// [2] 상태 필터 체크 (사용자가 필터 칩을 클릭해 활성화했는지)
|
||||
const statusMatch = state.activeStatuses.has(product.status);
|
||||
|
||||
// [3] 카테고리 필터 체크
|
||||
const categoryMatch = state.activeCategories.has('All') || state.activeCategories.has(product.category);
|
||||
|
||||
// [2] config 설정을 기반으로 한 동적 검색 매칭
|
||||
// [4] 검색어 매칭 로직
|
||||
const searchMatch =
|
||||
keyword === '' ||
|
||||
(() => {
|
||||
const searchPool = [];
|
||||
|
||||
if (SEARCH_CONFIG.USE_TITLE) searchPool.push(product.title);
|
||||
|
||||
if (SEARCH_CONFIG.USE_CUSTOM_TAG && product.customTag) searchPool.push(product.customTag);
|
||||
|
||||
if (SEARCH_CONFIG.USE_DESCRIPTION && product.description) searchPool.push(product.description);
|
||||
|
||||
if (SEARCH_CONFIG.USE_TAGS && product.tags) searchPool.push(...product.tags); // 배열 요소를 풀어서 추가
|
||||
|
||||
if (SEARCH_CONFIG.USE_TAGS && product.tags) searchPool.push(...product.tags);
|
||||
if (SEARCH_CONFIG.USE_FULL_DESCRIPTION && product.fullDescription) searchPool.push(...product.fullDescription);
|
||||
|
||||
// 검색 풀(Pool)에 있는 단어 중 키워드를 포함하는 게 하나라도 있는지 확인
|
||||
return searchPool.some((text) => String(text).toLowerCase().includes(keyword));
|
||||
return searchPool.some((text) => String(text || '').toLowerCase().includes(keyword));
|
||||
})();
|
||||
|
||||
return statusMatch && categoryMatch && searchMatch;
|
||||
@@ -80,8 +83,22 @@ export function applyFilters() {
|
||||
return aOrder - bOrder;
|
||||
});
|
||||
|
||||
renderTotalCount(state.visibleProducts.length);
|
||||
// 2. [추가] 페이지 위치 안전 조정 로직
|
||||
// 필터링된 결과로 가질 수 있는 최대 페이지 계산
|
||||
const totalPages = Math.ceil(state.visibleProducts.length / ITEMS_PER_PAGE);
|
||||
|
||||
if (state.currentPage > totalPages) {
|
||||
// 만약 필터링 후 전체 페이지가 현재 페이지보다 적어지면 마지막 페이지로 이동
|
||||
state.currentPage = Math.max(1, totalPages);
|
||||
} else if (state.currentPage < 1) {
|
||||
// 혹시 모를 에러 방지용 1페이지 고정
|
||||
state.currentPage = 1;
|
||||
}
|
||||
// ※ 참고: 필터를 걸 때마다 무조건 첫 페이지를 보게 하고 싶다면
|
||||
// 위 로직 대신 단순히 state.currentPage = 1; 을 쓰시면 됩니다.
|
||||
|
||||
// 3. UI 업데이트
|
||||
renderTotalCount(state.visibleProducts.length);
|
||||
renderProducts(state.currentPage);
|
||||
}
|
||||
|
||||
@@ -100,13 +117,29 @@ export function getCategories(products) {
|
||||
export function renderCategoryChips(products) {
|
||||
const container = document.getElementById('filter-chips');
|
||||
if (!container) return;
|
||||
const categories = ['All', ...new Set(products.map((p) => p.category))];
|
||||
|
||||
// [핵심] 시스템 가시성이 true인 상품의 카테고리만 추출합니다.
|
||||
const validCategories = products
|
||||
.filter(p => {
|
||||
const meta = STATUS_META[p.status];
|
||||
// 해당 상태가 정의되어 있고, 시스템에서 보여주기로 한 경우만 포함
|
||||
return meta && meta.isSystemVisible;
|
||||
})
|
||||
.map(p => p.category);
|
||||
|
||||
// 'All'은 항상 포함하고, 필터링된 카테고리들만 중복 제거하여 합침
|
||||
const categories = ['All', ...new Set(validCategories)];
|
||||
|
||||
container.innerHTML = '';
|
||||
|
||||
categories.forEach((cat) => {
|
||||
const isActive = state.activeCategories.has(cat);
|
||||
const chip = document.createElement('button');
|
||||
chip.className = `filter-chip px-3 py-1.5 md:px-4 md:py-2 rounded-full text-xs md:text-sm font-medium transition border ${isActive ? 'bg-primary text-white border-primary' : 'bg-slate-50 text-slate-600 border-slate-200'}`;
|
||||
chip.className = `filter-chip px-3 py-1.5 md:px-4 md:py-2 rounded-full text-xs md:text-sm font-medium transition border ${
|
||||
isActive
|
||||
? 'bg-primary text-white border-primary shadow-sm'
|
||||
: 'bg-slate-50 text-slate-600 border-slate-200'
|
||||
}`;
|
||||
chip.textContent = cat;
|
||||
chip.dataset.category = cat;
|
||||
chip.onclick = () => toggleCategory(cat);
|
||||
@@ -168,4 +201,5 @@ document.getElementById('logo-title')?.addEventListener('click', () => {
|
||||
|
||||
// 5. 페이지 최상단으로 스크롤 (선택 사항)
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -169,12 +169,27 @@ window.toggleSelectAll = (isChecked) => {
|
||||
};
|
||||
|
||||
/** 선택 리셋 */
|
||||
// [수정] 기존 resetSelection을 모달 오픈으로 변경
|
||||
window.resetSelection = () => {
|
||||
if (!confirm('선택된 내역을 모두 초기화할까요?')) return;
|
||||
const modal = document.getElementById('selection-reset-modal');
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
};
|
||||
|
||||
// [추가] 모달 닫기
|
||||
window.closeSelectionResetModal = () => {
|
||||
const modal = document.getElementById('selection-reset-modal');
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
};
|
||||
|
||||
// [추가] 실제 초기화 실행 (기존 로직 그대로)
|
||||
window.confirmSelectionReset = () => {
|
||||
state.selectedIds.clear();
|
||||
saveSelection(); // 스토리지 동기화
|
||||
updateSummary();
|
||||
renderProducts(state.currentPage);
|
||||
window.closeSelectionResetModal();
|
||||
};
|
||||
|
||||
/** 선택 토글 시 스토리지 저장 추가 */
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
/** 상품 그리드·페이지네이션 렌더링 */
|
||||
import { state } from './state.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';
|
||||
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');
|
||||
@@ -59,11 +71,9 @@ export function renderProducts(page = 1) {
|
||||
grid.innerHTML = '';
|
||||
pagedProducts.forEach((product) => {
|
||||
const isSold = STATUS_META[product.status]?.soldOut === true;
|
||||
grid.insertAdjacentHTML(
|
||||
'beforeend',
|
||||
`
|
||||
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="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">
|
||||
@@ -80,46 +90,58 @@ export function renderProducts(page = 1) {
|
||||
<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 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) {
|
||||
// 1. 정의된 Key인 경우 (추천 방식)
|
||||
conditionDisplay = conditionConfig.label;
|
||||
conditionClass = conditionConfig.color;
|
||||
} else if (conditionKey && conditionKey.trim() !== '') {
|
||||
// 2. 정의되지 않았지만 텍스트가 있는 경우 (기존 수기 입력 데이터)
|
||||
conditionDisplay = conditionKey;
|
||||
} else {
|
||||
// 3. 데이터가 비어있는 경우
|
||||
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 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>`;
|
||||
})
|
||||
<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('');
|
||||
}
|
||||
|
||||
|
||||
@@ -4,20 +4,19 @@ import { STATUS_META } from './config.js';
|
||||
|
||||
export const productsData = products;
|
||||
|
||||
// 초기 로드 시 세션 스토리지에서 선택 내역 불러오기
|
||||
const savedIds = JSON.parse(sessionStorage.getItem('selectedProductIds') || '[]');
|
||||
|
||||
export const state = {
|
||||
currentPage: 1,
|
||||
activeCategories: new Set(['All']),
|
||||
visibleProducts: [...products],
|
||||
visibleProducts: [], // 초기값은 빈 배열로 두고 main.js나 filter.js에서 첫 계산
|
||||
searchKeyword: '',
|
||||
viewMode: 'grid', // 기본값
|
||||
selectedIds: new Set(savedIds),
|
||||
activeStatuses: new Set(
|
||||
viewMode: 'grid',
|
||||
selectedIds: new Set(JSON.parse(sessionStorage.getItem('selectedProductIds') || '[]')),
|
||||
|
||||
// visible이 true인 상태만 초기 활성 필터로 저장
|
||||
activeStatuses: new Set(
|
||||
Object.entries(STATUS_META)
|
||||
.filter(([_, meta]) => meta.defaultVisible)
|
||||
.map(([status]) => status),
|
||||
.filter(([_, meta]) => meta.isSystemVisible && meta.isDefaultActive)
|
||||
.map(([status]) => status)
|
||||
),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user