641 lines
17 KiB
HTML
641 lines
17 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Amiibo Used Sale</title>
|
|
<style>
|
|
:root {
|
|
color: #111827;
|
|
background: #f8fafc;
|
|
font-family:
|
|
Arial,
|
|
"Apple SD Gothic Neo",
|
|
"Noto Sans KR",
|
|
sans-serif;
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
margin: 0;
|
|
background: #f8fafc;
|
|
}
|
|
|
|
a {
|
|
color: inherit;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.page {
|
|
width: min(1440px, 100%);
|
|
margin: 0 auto;
|
|
padding: 28px 20px 48px;
|
|
}
|
|
|
|
.header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
align-items: flex-end;
|
|
border-bottom: 1px solid #e5e7eb;
|
|
padding-bottom: 20px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.header h1 {
|
|
margin: 0 0 8px;
|
|
font-size: clamp(1.9rem, 4vw, 3rem);
|
|
line-height: 1;
|
|
letter-spacing: -0.02em;
|
|
}
|
|
|
|
.header p {
|
|
margin: 0;
|
|
color: #64748b;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.summary {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
justify-content: flex-end;
|
|
min-width: 260px;
|
|
}
|
|
|
|
.summary-pill {
|
|
border: 1px solid #dbe4ef;
|
|
background: #fff;
|
|
border-radius: 6px;
|
|
padding: 8px 10px;
|
|
font-size: 0.82rem;
|
|
color: #475569;
|
|
}
|
|
|
|
.summary-pill strong {
|
|
display: block;
|
|
margin-top: 2px;
|
|
color: #0f172a;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.layout {
|
|
display: grid;
|
|
grid-template-columns: 280px minmax(0, 1fr);
|
|
gap: 20px;
|
|
align-items: start;
|
|
}
|
|
|
|
.filters {
|
|
position: sticky;
|
|
top: 16px;
|
|
display: grid;
|
|
gap: 18px;
|
|
border: 1px solid #e5e7eb;
|
|
background: #fff;
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
}
|
|
|
|
.filter-title {
|
|
margin: 0 0 8px;
|
|
color: #111827;
|
|
font-weight: 700;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.filter-options {
|
|
display: grid;
|
|
gap: 8px;
|
|
max-height: 220px;
|
|
overflow: auto;
|
|
}
|
|
|
|
.filter-option {
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: center;
|
|
color: #334155;
|
|
font-size: 0.9rem;
|
|
line-height: 1.35;
|
|
}
|
|
|
|
.filter-option input {
|
|
width: 16px;
|
|
height: 16px;
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
.field,
|
|
.select {
|
|
width: 100%;
|
|
border: 1px solid #d1d5db;
|
|
border-radius: 6px;
|
|
padding: 10px 12px;
|
|
font-size: 0.95rem;
|
|
background: #fff;
|
|
}
|
|
|
|
.actions {
|
|
display: grid;
|
|
gap: 8px;
|
|
}
|
|
|
|
.button {
|
|
border: 0;
|
|
border-radius: 6px;
|
|
padding: 10px 12px;
|
|
background: #4f46e5;
|
|
color: #fff;
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.button.secondary {
|
|
background: #e2e8f0;
|
|
color: #334155;
|
|
}
|
|
|
|
.result-head {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
align-items: center;
|
|
margin-bottom: 14px;
|
|
color: #475569;
|
|
}
|
|
|
|
.result-head strong {
|
|
color: #111827;
|
|
}
|
|
|
|
.grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
gap: 14px;
|
|
width: 100%;
|
|
max-width: 970px;
|
|
}
|
|
|
|
.card {
|
|
display: grid;
|
|
grid-template-columns: 88px minmax(0, 1fr);
|
|
gap: 14px;
|
|
min-height: 180px;
|
|
border: 1px solid #e5e7eb;
|
|
background: #fff;
|
|
border-radius: 8px;
|
|
padding: 12px;
|
|
}
|
|
|
|
.card img {
|
|
width: 88px;
|
|
height: 120px;
|
|
object-fit: contain;
|
|
border-radius: 6px;
|
|
background: #f8fafc;
|
|
}
|
|
|
|
.card-body {
|
|
min-width: 0;
|
|
display: grid;
|
|
gap: 8px;
|
|
align-content: start;
|
|
}
|
|
|
|
.title {
|
|
margin: 0;
|
|
font-size: 1rem;
|
|
line-height: 1.35;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
.series {
|
|
color: #64748b;
|
|
font-size: 0.82rem;
|
|
line-height: 1.35;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
.price-row {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 6px 8px;
|
|
align-items: center;
|
|
}
|
|
|
|
.price {
|
|
color: #111827;
|
|
font-size: 1.25rem;
|
|
font-weight: 800;
|
|
line-height: 1.1;
|
|
}
|
|
|
|
.badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
width: fit-content;
|
|
border-radius: 999px;
|
|
padding: 3px 8px;
|
|
font-size: 0.75rem;
|
|
font-weight: 700;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.badge.boxed {
|
|
background: #dcfce7;
|
|
color: #166534;
|
|
}
|
|
|
|
.badge.opened {
|
|
background: #e0f2fe;
|
|
color: #075985;
|
|
}
|
|
|
|
.badge.damaged {
|
|
background: #fee2e2;
|
|
color: #991b1b;
|
|
}
|
|
|
|
.meta {
|
|
color: #475569;
|
|
font-size: 0.84rem;
|
|
line-height: 1.45;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
.meta strong {
|
|
color: #334155;
|
|
}
|
|
|
|
.empty {
|
|
border: 1px solid #e5e7eb;
|
|
background: #fff;
|
|
border-radius: 8px;
|
|
padding: 36px;
|
|
text-align: center;
|
|
color: #64748b;
|
|
}
|
|
|
|
@media (max-width: 820px) {
|
|
.header {
|
|
align-items: flex-start;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.summary {
|
|
justify-content: flex-start;
|
|
}
|
|
|
|
.layout {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.filters {
|
|
position: static;
|
|
}
|
|
|
|
.grid {
|
|
max-width: none;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 520px) {
|
|
.page {
|
|
padding-inline: 12px;
|
|
}
|
|
|
|
.result-head {
|
|
align-items: flex-start;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.grid {
|
|
grid-template-columns: minmax(0, 1fr);
|
|
}
|
|
|
|
.card {
|
|
grid-template-columns: 68px minmax(0, 1fr);
|
|
gap: 10px;
|
|
padding: 10px;
|
|
}
|
|
|
|
.card img {
|
|
width: 68px;
|
|
height: 96px;
|
|
}
|
|
|
|
.price {
|
|
font-size: 1.1rem;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main class="page">
|
|
<header class="header">
|
|
<div>
|
|
<h1><a href="./index.html">Amiibo 보유 리스트</a></h1>
|
|
</div>
|
|
<div class="summary" id="summary"></div>
|
|
</header>
|
|
|
|
<div class="layout">
|
|
<aside class="filters" aria-label="판매 필터">
|
|
<div>
|
|
<p class="filter-title">직접 검색</p>
|
|
<input id="searchInput" class="field" type="search" placeholder="아미보명, 시리즈 검색" />
|
|
</div>
|
|
|
|
<div>
|
|
<p class="filter-title">정렬</p>
|
|
<select id="sortSelect" class="select">
|
|
<option value="priceDesc">판매가 높은순</option>
|
|
<option value="priceAsc">판매가 낮은순</option>
|
|
<option value="noDesc">최근 번호순</option>
|
|
<option value="series">시리즈순</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<p class="filter-title">제품 상태</p>
|
|
<div class="filter-options" id="conditionOptions"></div>
|
|
</div>
|
|
|
|
<div>
|
|
<p class="filter-title">가격 기준</p>
|
|
<div class="filter-options" id="basisOptions"></div>
|
|
</div>
|
|
|
|
<div>
|
|
<p class="filter-title">시리즈</p>
|
|
<div class="filter-options" id="seriesOptions"></div>
|
|
</div>
|
|
|
|
<div class="actions">
|
|
<button id="resetFilters" class="button secondary" type="button">필터 초기화</button>
|
|
</div>
|
|
</aside>
|
|
|
|
<section aria-live="polite">
|
|
<div class="result-head">
|
|
<div id="resultCount"></div>
|
|
</div>
|
|
<div id="amiiboList" class="grid"></div>
|
|
</section>
|
|
</div>
|
|
</main>
|
|
|
|
<script type="module">
|
|
import AMIIBO_DB, { AMIIBO_RESALE_METADATA } from './db/amiibo.resale.db.js';
|
|
|
|
const SHOW_PRICE_RANGE = false;
|
|
|
|
const seriesNameMap = {
|
|
'大乱闘スマッシュブラザーズシリーズ': '대난투 스매시브라더스 시리즈',
|
|
'スーパーマリオシリーズ': '슈퍼 마리오 시리즈',
|
|
'スプラトゥーンシリーズ': '스플래툰 시리즈',
|
|
'ヨッシー ウールワールドシリーズ': '요시 울월드 시리즈',
|
|
'SUPER MARIO BROS. 30thシリーズ': '슈퍼 마리오 브라더스 30주년 시리즈',
|
|
'ちびロボ!シリーズ': '치비로보! 시리즈',
|
|
'どうぶつの森シリーズ': '동물의 숲 시리즈',
|
|
'ゼルダの伝説シリーズ': '젤다의 전설 시리즈',
|
|
'星のカービィシリーズ': '별의 커비 시리즈',
|
|
'ショベルナイトシリーズ': '쇼벨 나이트 시리즈',
|
|
'モンスターハンター ストーリーズシリーズ': '몬스터 헌터 스토리즈 시리즈',
|
|
'ハコボーイ!シリーズ': '하코보이! 시리즈',
|
|
'ファイアーエムブレムシリーズ': '파이어 엠블렘 시리즈',
|
|
'ピクミンシリーズ': '피크민 시리즈',
|
|
'メトロイドシリーズ': '메트로이드 시리즈',
|
|
'ポケモンシリーズ': '포켓몬 시리즈',
|
|
'ロックマンシリーズ': '록맨 시리즈',
|
|
'DARK SOULSシリーズ': '다크 소울 시리즈',
|
|
'モンスターハンターシリーズ': '몬스터 헌터 시리즈',
|
|
'ゼノブレイドシリーズ': '제노블레이드 시리즈',
|
|
others: '기타',
|
|
};
|
|
|
|
const conditionLabels = {
|
|
UNOPENED_OR_BOXED: '미개봉/박스 보존',
|
|
OPENED_USED: '개봉 중고',
|
|
DAMAGED: '손상/파손',
|
|
};
|
|
|
|
const basisLabels = {
|
|
KR_USED_REFERENCE_ESTIMATE: '국내 중고 시세 참고',
|
|
JP_USED_REFERENCE_CONVERTED_TO_KRW_ESTIMATE: '일본 중고 시세 환산',
|
|
US_USED_REFERENCE_CONVERTED_TO_KRW_ESTIMATE: '미국 중고 시세 환산',
|
|
};
|
|
|
|
const selected = {
|
|
condition: new Set(),
|
|
basis: new Set(),
|
|
series: new Set(),
|
|
};
|
|
|
|
const searchInput = document.getElementById('searchInput');
|
|
const sortSelect = document.getElementById('sortSelect');
|
|
const conditionOptions = document.getElementById('conditionOptions');
|
|
const basisOptions = document.getElementById('basisOptions');
|
|
const seriesOptions = document.getElementById('seriesOptions');
|
|
const summary = document.getElementById('summary');
|
|
const resultCount = document.getElementById('resultCount');
|
|
const amiiboList = document.getElementById('amiiboList');
|
|
|
|
function formatKRW(value) {
|
|
if (typeof value !== 'number') return '-';
|
|
return `${value.toLocaleString('ko-KR')}원`;
|
|
}
|
|
|
|
function getTitle(item) {
|
|
return item.koTitle || item.usTitle || item.title;
|
|
}
|
|
|
|
function getSeriesLabel(series) {
|
|
return seriesNameMap[series] || series;
|
|
}
|
|
|
|
function getConditionLabel(item) {
|
|
return conditionLabels[item.condition?.saleCondition] || '상태 확인 필요';
|
|
}
|
|
|
|
function getConditionClass(item) {
|
|
if (item.condition?.isDamaged) return 'damaged';
|
|
if (item.condition?.isOpen) return 'opened';
|
|
return 'boxed';
|
|
}
|
|
|
|
function getBasisLabel(item) {
|
|
return basisLabels[item.sale?.pricingBasis] || '가격 기준 확인 필요';
|
|
}
|
|
|
|
function renderSummary() {
|
|
summary.innerHTML = `
|
|
<div class="summary-pill">상품 수<strong>${AMIIBO_RESALE_METADATA.itemCount.toLocaleString('ko-KR')}종</strong></div>
|
|
<div class="summary-pill">총 수량<strong>${AMIIBO_RESALE_METADATA.quantityTotal.toLocaleString('ko-KR')}개</strong></div>
|
|
`;
|
|
}
|
|
|
|
function renderCheckboxes(container, entries, group) {
|
|
container.innerHTML = entries
|
|
.map(
|
|
([value, label]) => `
|
|
<label class="filter-option">
|
|
<input type="checkbox" value="${value}" data-filter-group="${group}" />
|
|
<span>${label}</span>
|
|
</label>
|
|
`,
|
|
)
|
|
.join('');
|
|
}
|
|
|
|
function renderFilters() {
|
|
renderCheckboxes(
|
|
conditionOptions,
|
|
Object.entries(conditionLabels),
|
|
'condition',
|
|
);
|
|
renderCheckboxes(basisOptions, Object.entries(basisLabels), 'basis');
|
|
|
|
const seriesEntries = [...new Set(AMIIBO_DB.map(item => item.series))]
|
|
.sort((a, b) => getSeriesLabel(a).localeCompare(getSeriesLabel(b), 'ko-KR'))
|
|
.map(series => [series, getSeriesLabel(series)]);
|
|
renderCheckboxes(seriesOptions, seriesEntries, 'series');
|
|
}
|
|
|
|
function getFilteredItems() {
|
|
const query = searchInput.value.trim().toLowerCase();
|
|
|
|
return AMIIBO_DB.filter(item => {
|
|
if (query) {
|
|
const target = [getTitle(item), item.title, item.usTitle, getSeriesLabel(item.series), item.series]
|
|
.filter(Boolean)
|
|
.join(' ')
|
|
.toLowerCase();
|
|
if (!target.includes(query)) return false;
|
|
}
|
|
|
|
if (
|
|
selected.condition.size > 0 &&
|
|
!selected.condition.has(item.condition?.saleCondition)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
if (selected.basis.size > 0 && !selected.basis.has(item.sale?.pricingBasis)) {
|
|
return false;
|
|
}
|
|
|
|
if (selected.series.size > 0 && !selected.series.has(item.series)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
function sortItems(items) {
|
|
const sortBy = sortSelect.value;
|
|
const sorted = [...items];
|
|
|
|
if (sortBy === 'priceDesc') {
|
|
return sorted.sort((a, b) => b.sale.suggestedPrice - a.sale.suggestedPrice || b.no - a.no);
|
|
}
|
|
|
|
if (sortBy === 'priceAsc') {
|
|
return sorted.sort((a, b) => a.sale.suggestedPrice - b.sale.suggestedPrice || b.no - a.no);
|
|
}
|
|
|
|
if (sortBy === 'series') {
|
|
return sorted.sort(
|
|
(a, b) =>
|
|
getSeriesLabel(a.series).localeCompare(getSeriesLabel(b.series), 'ko-KR') ||
|
|
b.sale.suggestedPrice - a.sale.suggestedPrice,
|
|
);
|
|
}
|
|
|
|
return sorted.sort((a, b) => b.no - a.no);
|
|
}
|
|
|
|
function renderList() {
|
|
const filtered = sortItems(getFilteredItems());
|
|
|
|
resultCount.innerHTML = `<strong>${filtered.length.toLocaleString('ko-KR')}종</strong> 표시 중`;
|
|
|
|
if (filtered.length === 0) {
|
|
amiiboList.innerHTML = '<div class="empty">조건에 맞는 아미보가 없습니다.</div>';
|
|
return;
|
|
}
|
|
|
|
amiiboList.innerHTML = filtered
|
|
.map(item => {
|
|
const range = item.sale.priceRange;
|
|
const priceRangeHtml = SHOW_PRICE_RANGE
|
|
? `<div class="meta"><strong>가격 범위</strong> ${formatKRW(range.min)} ~ ${formatKRW(range.max)}</div>`
|
|
: '';
|
|
const notes = item.sale.conditionNotes?.length
|
|
? `<div class="meta">${item.sale.conditionNotes.join(', ')}</div>`
|
|
: '';
|
|
|
|
return `
|
|
<article class="card">
|
|
<img src="${item.image}" alt="${getTitle(item)}" loading="lazy" />
|
|
<div class="card-body">
|
|
<div>
|
|
<h2 class="title">${item.no}. ${getTitle(item)}</h2>
|
|
<div class="series">${getSeriesLabel(item.series)}</div>
|
|
</div>
|
|
<div class="price-row">
|
|
<span class="price">${formatKRW(item.sale.suggestedPrice)}</span>
|
|
<span class="badge ${getConditionClass(item)}">${getConditionLabel(item)}</span>
|
|
</div>
|
|
${priceRangeHtml}
|
|
<div class="meta"><strong>가격 기준</strong> ${getBasisLabel(item)}</div>
|
|
<div class="meta"><strong>수량</strong> ${item.count.toLocaleString('ko-KR')}개 · <strong>소계</strong> ${formatKRW(item.sale.totalAssetValue)}</div>
|
|
${notes}
|
|
</div>
|
|
</article>
|
|
`;
|
|
})
|
|
.join('');
|
|
}
|
|
|
|
function handleFilterChange(event) {
|
|
const input = event.target;
|
|
const group = input.dataset.filterGroup;
|
|
if (!group) return;
|
|
|
|
if (input.checked) {
|
|
selected[group].add(input.value);
|
|
} else {
|
|
selected[group].delete(input.value);
|
|
}
|
|
|
|
renderList();
|
|
}
|
|
|
|
function resetFilters() {
|
|
Object.values(selected).forEach(values => values.clear());
|
|
document.querySelectorAll('[data-filter-group]').forEach(input => {
|
|
input.checked = false;
|
|
});
|
|
searchInput.value = '';
|
|
sortSelect.value = 'priceDesc';
|
|
renderList();
|
|
}
|
|
|
|
renderSummary();
|
|
renderFilters();
|
|
renderList();
|
|
|
|
searchInput.addEventListener('input', renderList);
|
|
sortSelect.addEventListener('change', renderList);
|
|
document.querySelector('.filters').addEventListener('change', handleFilterChange);
|
|
document.getElementById('resetFilters').addEventListener('click', resetFilters);
|
|
</script>
|
|
</body>
|
|
</html>
|