오류 수정

This commit is contained in:
2026-02-11 02:35:33 +09:00
parent f3e0e08c6b
commit 66d4f54e98
7 changed files with 165 additions and 73 deletions

View File

@@ -88,7 +88,7 @@ const games = [
price: 60000, price: 60000,
currency: '₩', currency: '₩',
category: 'Games', category: 'Games',
status: '판매', status: '판매완료',
customTag: '', customTag: '',
tags: ['Switch', 'JP'], tags: ['Switch', 'JP'],

View File

@@ -288,6 +288,22 @@
</div> </div>
</div> </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 -->
<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"> <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"> <div class="max-w-3xl mb-10">

View File

@@ -2,40 +2,46 @@
export const ITEMS_PER_PAGE = 20; export const ITEMS_PER_PAGE = 20;
export const VISIBILITY_CONFIG = { // export const VISIBILITY_CONFIG = {
showUnlisted: false, // showUnlisted: false,
showSold: true, // showSold: true,
}; // };
export const STATUS_META = { export const STATUS_META = {
미판매: { 미판매: {
selectable: false, selectable: false, // 체크박스 선택 불가
defaultVisible: false, isDefaultActive: false, // 초기 로드 시 미체크 상태
isSystemVisible: false, // 아예 리스트/필터에서 제외 (완전 숨김)
soldOut: false, soldOut: false,
}, },
판매예정: { 판매예정: {
selectable: true, selectable: false,
defaultVisible: true, isDefaultActive: true,
isSystemVisible: true,
soldOut: false, soldOut: false,
}, },
판매중: { 판매중: {
selectable: true, selectable: true,
defaultVisible: true, isDefaultActive: true,
isSystemVisible: true,
soldOut: false, soldOut: false,
}, },
판매완료: { 판매완료: {
selectable: true, selectable: false,
defaultVisible: false, isDefaultActive: false, // 초기에는 안 보이지만, 사용자가 필터 클릭하면 보임
soldOut: true, isSystemVisible: true,
soldOut: true, // 이미지에 SOLD OUT 표시
}, },
}; };
export const STATUS_FILTERS = [ // STATUS_FILTERS를 수동으로 만들지 않고 META에서 자동으로 생성합니다.
{ key: '판매중', label: '판매중', defaultActive: true, visible: true }, export const STATUS_FILTERS = Object.keys(STATUS_META)
{ key: '판매예정', label: '판매 예정', defaultActive: true, visible: true }, .filter(key => STATUS_META[key].isSystemVisible) // 시스템 가시성이 true인 것만 필터 칩 생성
{ key: '미판매', label: '미판매', defaultActive: false, visible: VISIBILITY_CONFIG.showUnlisted }, .map(key => ({
{ key: '판매완료', label: '판매완료', defaultActive: false, visible: VISIBILITY_CONFIG.showSold }, key: key,
]; label: key === '판매예정' ? '판매 예정' : key,
defaultActive: STATUS_META[key].isDefaultActive
}));
export const STATUS_ORDER = { export const STATUS_ORDER = {
판매중: 0, 판매중: 0,

View File

@@ -1,6 +1,6 @@
/** 상태·카테고리·검색 필터 로직 및 UI */ /** 상태·카테고리·검색 필터 로직 및 UI */
import { state, productsData } from './state.js'; 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'; import { renderProducts } from './productList.js';
function getStatusChipClass(status, isActive) { function getStatusChipClass(status, isActive) {
@@ -16,9 +16,12 @@ export function renderStatusChips() {
if (!container) return; if (!container) return;
container.innerHTML = ''; 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 isActive = state.activeStatuses.has(key);
const chip = document.createElement('button'); 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.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.textContent = label;
chip.onclick = () => toggleStatusFilter(key); chip.onclick = () => toggleStatusFilter(key);
@@ -39,37 +42,37 @@ function toggleStatusFilter(status) {
renderStatusChips(); renderStatusChips();
} }
// [핵심] 필터 적용 함수
export function applyFilters() { export function applyFilters() {
state.currentPage = 1; // [수정] 무조건 1페이지로 초기화하지 않고, 나중에 데이터 개수에 맞춰 계산합니다.
const keyword = state.searchKeyword.toLowerCase(); const keyword = state.searchKeyword.toLowerCase();
// 1. 데이터 필터링 및 정렬
state.visibleProducts = productsData state.visibleProducts = productsData
.filter((product) => { .filter((product) => {
// [1] 가시성 및 상태/카테고리 필터 const meta = STATUS_META[product.status];
if (product.status === '미판매' && !VISIBILITY_CONFIG.showUnlisted) return false;
if (product.status === '판매완료' && !VISIBILITY_CONFIG.showSold) return false;
// [1] 시스템 가시성 체크 (isSystemVisible이 false면 목록에서 완전히 제외)
if (!meta || !meta.isSystemVisible) return false;
// [2] 상태 필터 체크 (사용자가 필터 칩을 클릭해 활성화했는지)
const statusMatch = state.activeStatuses.has(product.status); const statusMatch = state.activeStatuses.has(product.status);
// [3] 카테고리 필터 체크
const categoryMatch = state.activeCategories.has('All') || state.activeCategories.has(product.category); const categoryMatch = state.activeCategories.has('All') || state.activeCategories.has(product.category);
// [2] config 설정을 기반으로 한 동적 검색 매칭 // [4] 검색 매칭 로직
const searchMatch = const searchMatch =
keyword === '' || keyword === '' ||
(() => { (() => {
const searchPool = []; const searchPool = [];
if (SEARCH_CONFIG.USE_TITLE) searchPool.push(product.title); 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_CUSTOM_TAG && product.customTag) searchPool.push(product.customTag);
if (SEARCH_CONFIG.USE_DESCRIPTION && product.description) searchPool.push(product.description); 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); 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; return statusMatch && categoryMatch && searchMatch;
@@ -80,8 +83,22 @@ export function applyFilters() {
return aOrder - bOrder; 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); renderProducts(state.currentPage);
} }
@@ -100,13 +117,29 @@ export function getCategories(products) {
export function renderCategoryChips(products) { export function renderCategoryChips(products) {
const container = document.getElementById('filter-chips'); const container = document.getElementById('filter-chips');
if (!container) return; 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 = ''; container.innerHTML = '';
categories.forEach((cat) => { categories.forEach((cat) => {
const isActive = state.activeCategories.has(cat); const isActive = state.activeCategories.has(cat);
const chip = document.createElement('button'); 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.textContent = cat;
chip.dataset.category = cat; chip.dataset.category = cat;
chip.onclick = () => toggleCategory(cat); chip.onclick = () => toggleCategory(cat);
@@ -168,4 +201,5 @@ document.getElementById('logo-title')?.addEventListener('click', () => {
// 5. 페이지 최상단으로 스크롤 (선택 사항) // 5. 페이지 최상단으로 스크롤 (선택 사항)
window.scrollTo({ top: 0, behavior: 'smooth' }); window.scrollTo({ top: 0, behavior: 'smooth' });
}); });

View File

@@ -169,12 +169,27 @@ window.toggleSelectAll = (isChecked) => {
}; };
/** 선택 리셋 */ /** 선택 리셋 */
// [수정] 기존 resetSelection을 모달 오픈으로 변경
window.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(); state.selectedIds.clear();
saveSelection(); // 스토리지 동기화 saveSelection(); // 스토리지 동기화
updateSummary(); updateSummary();
renderProducts(state.currentPage); renderProducts(state.currentPage);
window.closeSelectionResetModal();
}; };
/** 선택 토글 시 스토리지 저장 추가 */ /** 선택 토글 시 스토리지 저장 추가 */

View File

@@ -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 { ITEMS_PER_PAGE, STATUS_META, STATUS_COLOR, PRODUCT_CONDITIONS } from './config.js';
import { updateSummary } from './main.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) { export function renderProducts(page = 1) {
const grid = document.getElementById('product-grid'); const grid = document.getElementById('product-grid');
@@ -59,11 +71,9 @@ export function renderProducts(page = 1) {
grid.innerHTML = ''; grid.innerHTML = '';
pagedProducts.forEach((product) => { pagedProducts.forEach((product) => {
const isSold = STATUS_META[product.status]?.soldOut === true; const isSold = STATUS_META[product.status]?.soldOut === true;
grid.insertAdjacentHTML( grid.insertAdjacentHTML('beforeend', `
'beforeend',
`
<div class="group flex flex-col gap-4 cursor-pointer" onclick="openModal('${product.id}')"> <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" <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> style="background-image: url('${product.images[0]}')"></div>
<div class="absolute top-3 left-3"> <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> <p class="text-slate-500 dark:text-slate-400 text-sm font-normal line-clamp-1">${product.description}</p>
</div> </div>
</div> </div>
`, `);
);
}); });
} else { } else {
// 테이블 렌더링 (이전과 동일, 가격 포함) // [테이블 렌더링: 스타일 & 체크박스 제어 추가]
tableBody.innerHTML = pagedProducts tableBody.innerHTML = pagedProducts
.map((product) => { .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 conditionKey = product.specs.condition;
const conditionConfig = PRODUCT_CONDITIONS[conditionKey]; const conditionConfig = PRODUCT_CONDITIONS[conditionKey];
let conditionDisplay = ''; let conditionDisplay = '';
let conditionClass = 'text-slate-500'; let conditionClass = 'text-slate-500';
if (conditionConfig) { if (conditionConfig) {
// 1. 정의된 Key인 경우 (추천 방식)
conditionDisplay = conditionConfig.label; conditionDisplay = conditionConfig.label;
conditionClass = conditionConfig.color; conditionClass = conditionConfig.color;
} else if (conditionKey && conditionKey.trim() !== '') { } else if (conditionKey && conditionKey.trim() !== '') {
// 2. 정의되지 않았지만 텍스트가 있는 경우 (기존 수기 입력 데이터)
conditionDisplay = conditionKey; conditionDisplay = conditionKey;
} else { } else {
// 3. 데이터가 비어있는 경우
conditionDisplay = '상세 설명 참고 '; conditionDisplay = '상세 설명 참고 ';
conditionClass = 'text-indigo-500 italic'; 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 ` 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}')"> <tr class="hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors ${isSold ? 'opacity-50 grayscale cursor-not-allowed' : 'cursor-pointer'}"
<td class="py-4 px-4 text-center" onclick="event.stopPropagation()"> onclick="if(event.target.type !== 'checkbox') {
<input type="checkbox" ...> ${isSold ? "alert('판매 완료된 상품은 상세 정보를 볼 수 없습니다.');" : `openModal('${product.id}')`}
</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-center" onclick="event.stopPropagation()">
<td class="py-4 px-4 text-xs break-keep ${conditionClass}">${conditionDisplay}</td> <input type="checkbox"
<td class="py-4 px-4 text-right font-bold text-slate-900 dark:text-white">₩${product.price.toLocaleString()}</td> class="product-check rounded border-slate-300 w-4 h-4 ${isSelectable ? 'cursor-pointer text-primary' : 'opacity-20 cursor-not-allowed'}"
<td class="hidden lg:block py-4 px-4 text-center"> ${state.selectedIds.has(product.id) ? 'checked' : ''}
<span class="px-2 py-0.5 rounded text-[10px] font-bold border ${STATUS_COLOR[product.status]}">${product.status}</span> ${isSelectable ? '' : 'disabled'}
</td> onchange="window.toggleSelectItem('${product.id}')">
</tr>`; </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(''); .join('');
} }

View File

@@ -4,20 +4,19 @@ import { STATUS_META } from './config.js';
export const productsData = products; export const productsData = products;
// 초기 로드 시 세션 스토리지에서 선택 내역 불러오기
const savedIds = JSON.parse(sessionStorage.getItem('selectedProductIds') || '[]');
export const state = { export const state = {
currentPage: 1, currentPage: 1,
activeCategories: new Set(['All']), activeCategories: new Set(['All']),
visibleProducts: [...products], visibleProducts: [], // 초기값은 빈 배열로 두고 main.js나 filter.js에서 첫 계산
searchKeyword: '', searchKeyword: '',
viewMode: 'grid', // 기본값 viewMode: 'grid',
selectedIds: new Set(savedIds), selectedIds: new Set(JSON.parse(sessionStorage.getItem('selectedProductIds') || '[]')),
activeStatuses: new Set(
// visible이 true인 상태만 초기 활성 필터로 저장
activeStatuses: new Set(
Object.entries(STATUS_META) Object.entries(STATUS_META)
.filter(([_, meta]) => meta.defaultVisible) .filter(([_, meta]) => meta.isSystemVisible && meta.isDefaultActive)
.map(([status]) => status), .map(([status]) => status)
), ),
}; };