프리티어 변경

필터링 AND OR 조건 추가
데이터 JSON 구조 변경
(오류 발생 리스트 형식 안나오는중)
This commit is contained in:
2026-02-22 15:11:22 +09:00
parent 42b5c0f047
commit c8e0aa46f9
11 changed files with 1206 additions and 908 deletions

View File

@@ -1,14 +0,0 @@
{
"printWidth": 300,
"tabWidth": 4,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"bracketSameLine": true,
"bracketSpacing": true,
"arrowParens": "always",
"endOfLine": "auto",
"htmlWhitespaceSensitivity": "ignore",
"embeddedLanguageFormatting": "auto"
}

24
.prettierrc.js Normal file
View File

@@ -0,0 +1,24 @@
/** @type {import("prettier").Config} */
module.exports = {
printWidth: 300,
tabWidth: 4,
useTabs: false,
semi: true,
singleQuote: true,
trailingComma: 'all',
bracketSameLine: true,
bracketSpacing: true,
arrowParens: 'always',
endOfLine: 'auto',
htmlWhitespaceSensitivity: 'ignore',
embeddedLanguageFormatting: 'auto',
overrides: [
{
files: ['games.js', 'tech.js', 'furniture.js'],
options: {
printWidth: 100,
},
},
],
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,6 @@
import { STATUS, CONDITIONS } from '../scripts/config.js';
const tech = [
];
export default tech;

View File

@@ -1 +1,11 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
{
"name": "",
"short_name": "",
"icons": [
{ "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

View File

@@ -37,31 +37,30 @@ export const SEARCH_CONFIG = {
* isSystemVisible: 필터 목록 및 리스트 노출 여부
* soldOut: 판매 완료 처리 여부 (이미지 그레이스케일 등)
*/
export const STATUS = {
UNLISTED: '미판매',
RESERVED: '판매예정',
ON_SALE: '판매중',
SOLD_OUT: '판매완료',
};
export const STATUS_META = {
미판매: {
selectable: false,
isDefaultActive: false,
isSystemVisible: true,
soldOut: false,
},
판매예정: {
selectable: false,
isDefaultActive: false,
isSystemVisible: true,
soldOut: false,
},
판매중: {
selectable: true,
isDefaultActive: true,
isSystemVisible: true,
soldOut: false,
},
판매완료: {
selectable: false,
isDefaultActive: false,
isSystemVisible: true,
soldOut: true,
},
[STATUS.UNLISTED]: { selectable: false, isDefaultActive: false, isSystemVisible: true, soldOut: false },
[STATUS.RESERVED]: { selectable: false, isDefaultActive: false, isSystemVisible: true, soldOut: false },
[STATUS.ON_SALE]: { selectable: true, isDefaultActive: true, isSystemVisible: true, soldOut: false },
[STATUS.SOLD_OUT]: { selectable: false, isDefaultActive: false, isSystemVisible: true, soldOut: true },
};
export const CATEGORIES = {
GAMES: 'Games',
TECH: 'Tech',
FURNITURE: 'Furniture',
};
export const CURRENCIES = {
KRW: '₩',
USD: '$',
JPY: '¥'
};
/** 필터 칩 표시 순서 정의 */
@@ -108,13 +107,24 @@ export const TAG_DEFAULT_STYLE = 'bg-indigo-100 text-indigo-700 dark:bg-indigo-9
/** * 상품 상태(Condition) 등급 및 라벨 설정
* specs.condition 값과 매칭됩니다.
*/
export const CONDITIONS = {
BRAND_NEW: 'BRAND_NEW',
LIKE_NEW: 'LIKE_NEW',
EXCELLENT: 'EXCELLENT',
GOOD: 'GOOD',
INCOMPLETE: 'INCOMPLETE',
DAMAGED: 'DAMAGED',
JUNK: 'JUNK',
OTHER: 'OTHER',
};
export const PRODUCT_CONDITIONS = {
BRAND_NEW: { label: 'Brand New (미개봉)', color: 'text-emerald-600', level: 'S' },
LIKE_NEW: { label: 'Like New (단순개봉)', color: 'text-blue-600', level: 'A+' },
EXCELLENT: { label: 'Excellent (최상급)', color: 'text-sky-600', level: 'A' },
GOOD: { label: 'Good (보통/사용감)', color: 'text-slate-600', level: 'B' },
INCOMPLETE: { label: 'Incomplete (구성품 누락)', color: 'text-amber-600', level: 'C' },
DAMAGED: { label: 'Damaged (하자/파손)', color: 'text-orange-600', level: 'D' },
JUNK: { label: 'Junk (동작불가/부품용)', color: 'text-red-600', level: 'F' },
OTHER: { label: '기타 (상세설명 참고)', color: 'text-indigo-600', level: '-' },
[CONDITIONS.BRAND_NEW]: { label: 'Brand New (미개봉)', color: 'text-emerald-600', level: 'S' },
[CONDITIONS.LIKE_NEW]: { label: 'Like New (단순개봉)', color: 'text-blue-600', level: 'A+' },
[CONDITIONS.EXCELLENT]: { label: 'Excellent (최상급)', color: 'text-sky-600', level: 'A' },
[CONDITIONS.GOOD]: { label: 'Good (보통/사용감)', color: 'text-slate-600', level: 'B' },
[CONDITIONS.INCOMPLETE]: { label: 'Incomplete (구성품 누락)', color: 'text-amber-600', level: 'C' },
[CONDITIONS.DAMAGED]: { label: 'Damaged (하자/파손)', color: 'text-orange-600', level: 'D' },
[CONDITIONS.JUNK]: { label: 'Junk (동작불가/부품용)', color: 'text-red-600', level: 'F' },
[CONDITIONS.OTHER]: { label: '기타 (상세설명 참고)', color: 'text-indigo-600', level: '-' },
};

View File

@@ -67,7 +67,7 @@ export function applyFilters() {
const statusMatch = state.activeStatuses.has(product.status);
const categoryMatch = state.activeCategories.has('All') || state.activeCategories.has(product.category);
const searchMatch = checkSearchMatch(product, keyword);
const tagMatch = state.activeTags.size === 0 || Array.from(state.activeTags).every((tag) => product.tags && product.tags.includes(tag));
const tagMatch = state.activeTags.size === 0 || (state.tagMode === 'AND' ? Array.from(state.activeTags).every((tag) => product.tags?.includes(tag)) : Array.from(state.activeTags).some((tag) => product.tags?.includes(tag)));
return statusMatch && categoryMatch && searchMatch && tagMatch;
})
@@ -195,8 +195,15 @@ export function renderTagChips() {
${hasActive ? 'bg-red-50 text-red-500 border-red-200 hover:bg-red-100' : 'bg-slate-50 text-slate-400 border-slate-200 opacity-60'}" title="태그 초기화">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
</button>`;
const modeBtnHtml = `
<button id="tag-mode-btn" class="px-2 py-1 rounded-full text-[10px] font-bold border transition-colors
${state.tagMode === 'AND' ? 'bg-indigo-100 text-indigo-600 border-indigo-200' : 'bg-orange-100 text-orange-600 border-orange-200'}">
${state.tagMode}
</button>
`;
container.innerHTML =
modeBtnHtml +
resetBtnHtml +
sortedTags
.map((tag) => {
@@ -229,6 +236,11 @@ export function renderTagChips() {
applyFilters();
};
document.getElementById('tag-mode-btn').onclick = () => {
state.tagMode = state.tagMode === 'AND' ? 'OR' : 'AND';
applyFilters(); // 필터 재적용 및 UI 갱신
};
container.querySelectorAll('.tag-chip').forEach((chip) => {
chip.onclick = () => {
const tag = chip.dataset.tag;

View File

@@ -10,6 +10,9 @@ import { renderProducts, changePage } from './productList.js';
import { openModal, closeModal } from './modal.js';
import { scrollToImage } from './carousel.js';
// ==========================================================================
// 1. 전역 설정 및 윈도우 객체 등록 (Global Setup)
// ==========================================================================
@@ -378,6 +381,8 @@ window.exportToExcel = () => {
// ==========================================================================
document.addEventListener('DOMContentLoaded', () => {
console.log('App initialized. Products:', productsData.length);
initTheme();

View File

@@ -153,7 +153,7 @@ function renderModalInfo(product) {
function renderSpecs(product) {
// 구매일자
const modalDateRow = document.getElementById('modal-date-row');
const pDate = product.specs?.purchaseDate;
const pDate = product.purchaseDate;
if (pDate && String(pDate).trim() !== '' && String(pDate) !== 'null') {
document.getElementById('modal-date').textContent = pDate;
modalDateRow?.classList.replace('hidden', 'flex');
@@ -162,7 +162,7 @@ function renderSpecs(product) {
}
// 제품 상태(Condition)
const conditionKey = product.specs?.condition;
const conditionKey = product.condition;
const conditionRowWrap = document.getElementById('modal-condition-row')?.parentElement;
if (conditionKey) {
const conditionLabel = PRODUCT_CONDITIONS[conditionKey]?.label || conditionKey;
@@ -170,7 +170,7 @@ function renderSpecs(product) {
conditionRowWrap?.classList.replace('hidden', 'flex');
const verifiedIcon = document.getElementById('modal-verified-icon');
if (verifiedIcon) verifiedIcon.classList.toggle('hidden', !product.specs?.isVerified);
if (verifiedIcon) verifiedIcon.classList.toggle('hidden', !product.isVerified);
} else {
conditionRowWrap?.classList.replace('flex', 'hidden');
}

View File

@@ -81,7 +81,7 @@ window.handleThumbnailLeave = function (id) {
window.toggleSelectItem = function (id) {
if (state.selectedIds.has(id)) state.selectedIds.delete(id);
else state.selectedIds.add(id);
saveSelection();
renderProducts(state.currentPage);
updateSummary();
@@ -128,7 +128,7 @@ function renderEmpty(grid, tableWrapper, paginationContainer) {
grid.classList.remove('grid');
grid.classList.add('hidden');
tableWrapper.classList.add('hidden');
const emptyMsg = `
<div class="col-span-full flex flex-col items-center justify-center py-20 w-full text-center">
<svg class="w-20 h-20 text-slate-300 dark:text-slate-700 mb-6" viewBox="0 -960 960 960" fill="currentColor">
@@ -137,7 +137,7 @@ function renderEmpty(grid, tableWrapper, paginationContainer) {
<h3 class="text-xl font-bold text-slate-900 dark:text-white mb-2">검색 결과가 없습니다</h3>
<p class="text-slate-500 dark:text-slate-400 text-sm">입력하신 검색어나 선택한 필터를 확인해 주세요.</p>
</div>`;
grid.innerHTML = emptyMsg;
grid.classList.remove('hidden');
if (paginationContainer) paginationContainer.innerHTML = '';
@@ -149,13 +149,21 @@ function renderEmpty(grid, tableWrapper, paginationContainer) {
/** 그리드 뷰 렌더링 */
function renderGridView(grid, tableWrapper, products) {
grid.classList.replace('hidden', 'grid');
// 1. 표시 모드 전환
grid.classList.remove('hidden');
tableWrapper.classList.add('hidden');
// 2. [핵심] 그리드 속성 및 간격(Gap) 강제 적용
// 기존 클래스를 유지하면서 필수 클래스만 추가합니다.
grid.classList.add('grid', 'gap-6', 'grid-cols-2', 'md:grid-cols-3', 'lg:grid-cols-4');
// 3. 만약 renderEmpty에서 제거했던 클래스가 있다면 여기서 다시 정렬
grid.style.display = 'grid';
const summaryBar = document.getElementById('selection-summary');
if (summaryBar) summaryBar.classList.add('hidden');
grid.innerHTML = products.map(product => createProductCardHTML(product)).join('');
grid.innerHTML = products.map((product) => createProductCardHTML(product)).join('');
setupLazyLoading();
}
@@ -163,33 +171,37 @@ function renderGridView(grid, tableWrapper, products) {
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 || '';
const conditionConfig = PRODUCT_CONDITIONS[product.condition];
const conditionDisplay = conditionConfig ? conditionConfig.label : product.condition || '';
return `
<div class="product-card group flex flex-col gap-4 cursor-pointer"
data-id="${product.id}"
onclick="if(!window.isDragging) window.openModal('${product.id}')"
ontouchstart="window.handleTouchStart(event)"
ontouchmove="window.handleTouchMove(event)"
ontouchend="window.handleTouchEnd(event)"
onmousemove="window.handleThumbnailHover(event, '${product.id}')"
onmouseleave="window.handleThumbnailLeave('${product.id}')">
data-id="${product.id}"
onclick="if(!window.isDragging) window.openModal('${product.id}')"
ontouchstart="window.handleTouchStart(event)"
ontouchmove="window.handleTouchMove(event)"
ontouchend="window.handleTouchEnd(event)"
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.id}"
class="w-full h-full bg-center bg-no-repeat bg-cover absolute inset-0 transition-all duration-500 ${isSold ? 'grayscale opacity-60' : 'group-hover:scale-105'}"
style="background-image: none; will-change: background-image, transform;">
class="w-full h-full bg-center bg-no-repeat bg-cover absolute inset-0 transition-all duration-500 ${isSold ? 'grayscale opacity-60' : 'group-hover:scale-105'}"
style="background-image: none; will-change: background-image, transform;">
</div>
<div class="absolute top-3 left-3 z-10">
<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 ? `
${
!isSold && product.images?.length > 1
? `
<div id="indicator-${product.id}" class="absolute bottom-3 left-1/2 -translate-x-1/2 hidden md:flex gap-1.5 opacity-0 group-hover:opacity-100 transition-all duration-300 pointer-events-none z-10">
${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>
<div class="flex flex-col gap-1.5">
@@ -211,9 +223,9 @@ function createProductCardHTML(product) {
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('');
tableBody.innerHTML = products.map((product) => createTableRowHTML(product)).join('');
updateSummary();
}
@@ -222,7 +234,7 @@ function createTableRowHTML(product) {
const meta = STATUS_META[product.status];
const isSold = meta?.soldOut === true;
const isSelectable = meta?.selectable !== false;
const conditionConfig = PRODUCT_CONDITIONS[product.specs.condition];
const conditionConfig = PRODUCT_CONDITIONS[product.condition];
const conditionDisplay = conditionConfig ? conditionConfig.label : '상세 설명 참고 ';
const conditionClass = conditionConfig ? conditionConfig.color : 'text-slate-500';
@@ -253,35 +265,38 @@ function updateSelectAllCheckbox(page) {
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);
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));
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}`);
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; });
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);
}
observer.unobserve(card);
}
});
}, { threshold: 0.1 });
});
},
{ threshold: 0.1 },
);
document.querySelectorAll('.product-card').forEach((card) => observer.observe(card));
}
@@ -314,4 +329,4 @@ export function changePage(page) {
state.currentPage = page;
renderProducts(page);
window.scrollTo({ top: 0, behavior: 'smooth' });
}
}

View File

@@ -33,6 +33,8 @@ export const state = {
/** 화면 보기 모드 ('grid' | 'table') */
viewMode: 'grid',
tagMode: 'AND', // 태그 필터링 모드 ('AND' 또는 'OR')
/** 장바구니/내보내기 등을 위해 선택된 상품 ID 세트 (세션 스토리지 복원) */
selectedIds: new Set(JSON.parse(sessionStorage.getItem('selectedProductIds') || '[]')),
@@ -60,4 +62,31 @@ export const state = {
export function saveSelection() {
const idsArray = Array.from(state.selectedIds);
sessionStorage.setItem('selectedProductIds', JSON.stringify(idsArray));
}
}
window.toggleViewMode = function (mode) {
// 1. 이미 해당 모드라면 실행 중단
if (state.viewMode === mode) return;
// 2. 상태 변경
state.viewMode = mode;
// 3. 버튼 UI 업데이트 (활성화된 아이콘 강조)
const gridBtn = document.getElementById('view-grid-btn'); // HTML의 그리드 아이콘 ID
const listBtn = document.getElementById('view-list-btn'); // HTML의 리스트 아이콘 ID
if (gridBtn && listBtn) {
if (mode === 'grid') {
gridBtn.classList.add('text-primary', 'dark:text-white'); // 강조색
listBtn.classList.remove('text-primary', 'dark:text-white');
listBtn.classList.add('text-slate-400'); // 비활성색
} else {
listBtn.classList.add('text-primary', 'dark:text-white');
gridBtn.classList.remove('text-primary', 'dark:text-white');
gridBtn.classList.add('text-slate-400');
}
}
// 4. 즉시 화면 다시 그리기
renderProducts(state.currentPage);
};