Convert Switch DB for used sales

This commit is contained in:
2026-05-19 11:10:05 +09:00
parent 95d092fbc2
commit 7904b6b89c
8 changed files with 19012 additions and 11753 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

9325
db/nsw.resale.db.js Normal file

File diff suppressed because it is too large Load Diff

9321
db/nsw.resale.db.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -158,39 +158,19 @@
</div> </div>
</div> </div>
<!-- 상태 필터 --> <!-- 판매 상태 필터 -->
<div class="space-y-2"> <div class="space-y-2">
<h3 class="text-base font-medium text-gray-900">Status</h3> <h3 class="text-base font-medium text-gray-900">Sale Status</h3>
<div class="space-y-2"> <div class="space-y-2">
<div class="flex items-center"> <div class="flex items-center">
<input <input
id="package" id="available"
name="status-filter" name="status-filter"
type="checkbox" type="checkbox"
value="package" value="available"
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600" /> class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600" />
<label for="package" class="ml-2 text-sm text-gray-900">Package</label> <label for="available" class="ml-2 text-sm text-gray-900">
</div> 판매 가능
<div class="flex items-center">
<input
id="download"
name="status-filter"
type="checkbox"
value="download"
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600" />
<label for="download" class="ml-2 text-sm text-gray-900">
Download
</label>
</div>
<div class="flex items-center">
<input
id="extension"
name="status-filter"
type="checkbox"
value="extension"
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600" />
<label for="extension" class="ml-2 text-sm text-gray-900">
Extension
</label> </label>
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
@@ -200,7 +180,7 @@
type="checkbox" type="checkbox"
value="sold" value="sold"
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600" /> class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600" />
<label for="sold" class="ml-2 text-sm text-gray-900">Sold</label> <label for="sold" class="ml-2 text-sm text-gray-900">판매완료</label>
</div> </div>
</div> </div>
</div> </div>
@@ -403,8 +383,8 @@
<li <li
class="relative cursor-pointer hover:bg-gray-100 select-none py-2 pl-3 pr-9 text-gray-900" class="relative cursor-pointer hover:bg-gray-100 select-none py-2 pl-3 pr-9 text-gray-900"
role="option" role="option"
data-value="sortByPurchaseDateDesc"> data-value="sortByPriceDesc">
<span class="block truncate">구매일 최신</span> <span class="block truncate">판매가 높은</span>
<span <span
class="absolute inset-y-0 right-0 flex items-center pr-4 text-indigo-600 hidden"> class="absolute inset-y-0 right-0 flex items-center pr-4 text-indigo-600 hidden">
<svg <svg
@@ -423,8 +403,8 @@
<li <li
class="relative cursor-pointer hover:bg-gray-100 select-none py-2 pl-3 pr-9 text-gray-900" class="relative cursor-pointer hover:bg-gray-100 select-none py-2 pl-3 pr-9 text-gray-900"
role="option" role="option"
data-value="sortByPurchaseDate"> data-value="sortByPrice">
<span class="block truncate">구매일 과거</span> <span class="block truncate">판매가 낮은</span>
<span <span
class="absolute inset-y-0 right-0 flex items-center pr-4 text-indigo-600 hidden"> class="absolute inset-y-0 right-0 flex items-center pr-4 text-indigo-600 hidden">
<svg <svg
@@ -480,17 +460,17 @@
</th> </th>
<th <th
scope="col" scope="col"
class="hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell"> class="hidden w-32 px-3 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell">
Infomation Infomation
</th> </th>
<th <th
scope="col" scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"> class="w-28 px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
Status Status
</th> </th>
<th <th
scope="col" scope="col"
class="hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell"> class="hidden w-56 px-3 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell">
Role Role
</th> </th>
<th <th

View File

@@ -33,7 +33,7 @@
<div class="flex justify-between px-4 py-5 sm:px-6"> <div class="flex justify-between px-4 py-5 sm:px-6">
<div> <div>
<h3 id="purchaseTitle" class="text-base font-semibold leading-6 text-gray-900"> <h3 id="purchaseTitle" class="text-base font-semibold leading-6 text-gray-900">
매 정보 매 정보
</h3> </h3>
<p id="purchaseGameTitle" class="mt-1 max-w-2xl text-sm text-gray-500"></p> <p id="purchaseGameTitle" class="mt-1 max-w-2xl text-sm text-gray-500"></p>
</div> </div>
@@ -45,7 +45,7 @@
</div> </div>
</div> </div>
<div id="purchaseInfo" class="border-t border-gray-200 px-4 py-5 sm:px-6"> <div id="purchaseInfo" class="border-t border-gray-200 px-4 py-5 sm:px-6">
<!-- 매 정보가 여기에 동적으로 추가됩니다 --> <!-- 매 정보가 여기에 동적으로 추가됩니다 -->
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,4 @@
import NSW_DB from '../db/nsw.db.js'; import NSW_DB from '../db/nsw.resale.db.js';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// URL에서 게임 번호 가져오기 // URL에서 게임 번호 가져오기
@@ -18,7 +18,7 @@ document.addEventListener('DOMContentLoaded', () => {
const texts = { const texts = {
ko: { ko: {
infoTitle: '게임 정보', infoTitle: '게임 정보',
purchaseTitle: '매 정보', purchaseTitle: '매 정보',
requiredCapacity: '필요한 용량', requiredCapacity: '필요한 용량',
playMode: '플레이 모드', playMode: '플레이 모드',
playUser: '플레이 인원', playUser: '플레이 인원',
@@ -31,10 +31,10 @@ document.addEventListener('DOMContentLoaded', () => {
cero: '심의 등급', cero: '심의 등급',
iarc: '심의 등급', iarc: '심의 등급',
releaseDate: '출시일', releaseDate: '출시일',
purchaseDate: '구매일', suggestedPrice: '추천 판매가',
store: '구매처', priceRange: '판매가 범위',
price: '구매 가격', pricingBasis: '가격 참고 기준',
orderNumber: '주문번호', checkedAt: '기준일',
extension: '추가 콘텐츠', extension: '추가 콘텐츠',
none: '없음', none: '없음',
supported: '대응', supported: '대응',
@@ -42,7 +42,7 @@ document.addEventListener('DOMContentLoaded', () => {
}, },
ja: { ja: {
infoTitle: 'Infomation', infoTitle: 'Infomation',
purchaseTitle: '購入情報', purchaseTitle: '販売情報',
requiredCapacity: '必要な容量', requiredCapacity: '必要な容量',
playMode: 'プレイモード', playMode: 'プレイモード',
playUser: 'プレイ人数', playUser: 'プレイ人数',
@@ -55,10 +55,10 @@ document.addEventListener('DOMContentLoaded', () => {
cero: 'CERO', cero: 'CERO',
iarc: 'IARC', iarc: 'IARC',
releaseDate: '配信日', releaseDate: '配信日',
purchaseDate: '購入日', suggestedPrice: '推奨販売価格',
store: '購入先', priceRange: '価格帯',
price: '購入価格', pricingBasis: '価格参考基準',
orderNumber: '注文番号', checkedAt: '確認日',
extension: '追加コンテンツ', extension: '追加コンテンツ',
none: 'なし', none: 'なし',
supported: '対応', supported: '対応',
@@ -68,11 +68,153 @@ document.addEventListener('DOMContentLoaded', () => {
const currentTexts = texts[language]; const currentTexts = texts[language];
function formatKRW(value) {
if (typeof value !== 'number') return '';
return `${value.toLocaleString('ko-KR')}`;
}
function formatPricingBasis(pricingBasis) {
const labels = {
ko: {
KR_USED_REFERENCE_ESTIMATE: '국내 중고 시세 참고',
JP_USED_REFERENCE_CONVERTED_TO_KRW_ESTIMATE: '일본 중고 시세 환산',
MANUAL: '직접 입력가',
SOLD_HISTORY: '판매 이력 기준',
},
ja: {
KR_USED_REFERENCE_ESTIMATE: '韓国中古相場参考',
JP_USED_REFERENCE_CONVERTED_TO_KRW_ESTIMATE: '日本中古相場換算',
MANUAL: '手入力価格',
SOLD_HISTORY: '販売履歴基準',
},
};
return labels[language][pricingBasis] || '';
}
function convertLanguage(value) {
if (!value || language !== 'ko') return value;
const labels = {
日本語: '일본어',
英語: '영어',
韓国語: '한국어',
フランス語: '프랑스어',
ドイツ語: '독일어',
イタリア語: '이탈리아어',
スペイン語: '스페인어',
ロシア語: '러시아어',
オランダ語: '네덜란드어',
ポルトガル語: '포르투갈어',
'中国語 (簡体字)': '중국어 (간체)',
'中国語 (繁体字)': '중국어 (번체)',
};
return value
.split(',')
.map(item => labels[item.trim()] || item.trim())
.join(', ');
}
function convertMaker(value) {
if (!value || language !== 'ko') return value;
const labels = {
任天堂: '닌텐도',
スクウェアエニックス: '스퀘어 에닉스',
コーエーテクモゲームス: '코에이 테크모 게임스',
ポケモン: '포켓몬',
バンダイナムコエンターテインメント: '반다이 남코 엔터테인먼트',
アトラス: '아틀러스',
セガ: '세가',
マーベラス: '마블러스 엔터테인먼트',
カプコン: '캡콤',
ユービーアイソフト: '유비소프트',
日本一ソフトウェア: '니폰이치 소프트웨어',
アークシステムワークス: '아크 시스템 웍스',
日本ファルコム: '일본 팔콤',
};
return value
.split(',')
.map(item => labels[item.trim()] || item.trim())
.join(', ');
}
function convertTags(value) {
if (!value || language !== 'ko') return value;
const labels = {
アクション: '액션',
アドベンチャー: '어드벤처',
テキストアドベンチャー: '텍스트 어드벤처',
ロールプレイング: '롤플레잉',
シューティング: '슈팅',
シミュレーション: '시뮬레이션',
ストラテジー: '전략',
パズル: '퍼즐',
レース: '레이스',
スポーツ: '스포츠',
格闘: '격투',
音楽ゲーム: '음악 게임',
恋愛: '연애',
難易度が選べる: '난이도 선택 가능',
戦うたびに強くなる: '싸울수록 성장',
キャラクターボイス: '캐릭터 음성',
オンラインで対戦: '온라인 대전',
オンラインで協力: '온라인 협력',
オンラインでフレンドと: '친구와 온라인으로',
キャラクターカスタマイズ: '캐릭터 커스터마이즈',
'3人称視点': '3인칭 시점',
'1台の本体でいっしょにあそべる': '한 대의 본체에서 함께 플레이',
ともだちや家族と集まって: '친구와 가족과 함께',
オンラインランキング: '온라인 랭킹',
本体を持ちよってあそべる: '게임기를 들고 플레이',
世界を自由にかけ回る: '세계를 자유롭게 탐험',
目的はあなた次第: '목적은 자유롭게 선택',
};
return value
.split(',')
.map(item => labels[item.trim()] || item.trim())
.join(', ');
}
function convertReleaseDate(value) {
if (!value || language !== 'ko') return value;
const date = new Date(value.replace(/年|月/g, '/').replace('日', ''));
if (Number.isNaN(date.getTime())) return value;
return `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}`;
}
function convertSupportValue(value) {
if (!value || language !== 'ko') return value;
return value.replaceAll('対応', '대응').replaceAll('非対応', '비대응');
}
function getGameInfoValue(key) {
switch (key) {
case 'maker':
return convertMaker(game[key]);
case 'language':
return convertLanguage(game[key]);
case 'release':
return convertReleaseDate(game[key]);
case 'onlineDataSave':
return convertSupportValue(game[key]);
default:
return game[key];
}
}
// 기본 정보 설정 // 기본 정보 설정
document.getElementById('gameImage').src = window.innerWidth < 640 ? game.thumbnail : game.image; document.getElementById('gameImage').src = window.innerWidth < 640 ? game.thumbnail : game.image;
document.getElementById('gameTitle').textContent = document.getElementById('gameTitle').textContent =
language === 'ko' ? game.koTitle || game.title : game.title; language === 'ko' ? game.koTitle || game.title : game.title;
document.getElementById('gameTags').textContent = game.tags; document.getElementById('gameTags').textContent = convertTags(game.tags);
document.getElementById('infoTitle').textContent = currentTexts.infoTitle; document.getElementById('infoTitle').textContent = currentTexts.infoTitle;
document.getElementById('purchaseTitle').textContent = currentTexts.purchaseTitle; document.getElementById('purchaseTitle').textContent = currentTexts.purchaseTitle;
document.getElementById('purchaseGameTitle').textContent = document.getElementById('purchaseGameTitle').textContent =
@@ -80,14 +222,18 @@ document.addEventListener('DOMContentLoaded', () => {
// 게임 상태 설정 // 게임 상태 설정
const statusClass = { const statusClass = {
package: 'bg-green-100 text-green-800', available: 'bg-green-100 text-green-800',
download: 'bg-yellow-100 text-yellow-800', download: 'bg-yellow-100 text-yellow-800',
expansion: 'bg-blue-100 text-blue-800', expansion: 'bg-blue-100 text-blue-800',
sold: 'bg-red-100 text-red-800',
}[game.status]; }[game.status];
document.getElementById( document.getElementById(
'gameStatus', 'gameStatus',
).className = `inline-flex rounded-full px-2 text-xs font-semibold leading-5 ${statusClass}`; ).className = `inline-flex rounded-full px-2 text-xs font-semibold leading-5 ${statusClass}`;
document.getElementById('gameStatus').textContent = game.status; document.getElementById('gameStatus').textContent =
language === 'ko'
? { available: '판매중', sold: '판매완료' }[game.status] || game.status
: { available: '販売中', sold: '販売済み' }[game.status] || game.status;
// 국가 설정 // 국가 설정
const countryClass = const countryClass =
@@ -116,41 +262,47 @@ document.addEventListener('DOMContentLoaded', () => {
{ key: 'language', label: currentTexts.language }, { key: 'language', label: currentTexts.language },
{ key: 'cero', label: currentTexts.cero }, { key: 'cero', label: currentTexts.cero },
{ key: 'iarc', label: currentTexts.iarc }, { key: 'iarc', label: currentTexts.iarc },
{ key: 'releaseDate', label: currentTexts.releaseDate }, { key: 'release', label: currentTexts.releaseDate },
]; ];
infoItems.forEach(item => { infoItems.forEach(item => {
if (game[item.key]) { const value = getGameInfoValue(item.key);
if (value) {
const div = document.createElement('div'); const div = document.createElement('div');
div.className = 'border-t border-gray-200 pt-4'; div.className = 'border-t border-gray-200 pt-4';
div.innerHTML = ` div.innerHTML = `
<dt class="font-medium text-gray-900">${item.label}</dt> <dt class="font-medium text-gray-900">${item.label}</dt>
<dd class="mt-2 text-sm text-gray-500">${game[item.key]}</dd> <dd class="mt-2 text-sm text-gray-500">${value}</dd>
`; `;
gameInfo.appendChild(div); gameInfo.appendChild(div);
} }
}); });
// 매 정보 설정 // 매 정보 설정
const purchaseInfo = document.getElementById('purchaseInfo'); const purchaseInfo = document.getElementById('purchaseInfo');
if (game.purchaseInformation) { if (game.sale) {
const purchaseItems = [ const priceRange =
{ key: 'date', label: currentTexts.purchaseDate }, game.sale.priceRange?.min && game.sale.priceRange?.max
{ key: 'store', label: currentTexts.store }, ? `${formatKRW(game.sale.priceRange.min)} ~ ${formatKRW(game.sale.priceRange.max)}`
{ key: 'price', label: currentTexts.price }, : '';
{ key: 'orderNumber', label: currentTexts.orderNumber }, const saleItems = [
{ value: formatKRW(game.sale.suggestedPrice), label: currentTexts.suggestedPrice },
{ value: priceRange, label: currentTexts.priceRange },
{ value: formatPricingBasis(game.sale.pricingBasis), label: currentTexts.pricingBasis },
{ value: game.sale.checkedAt, label: currentTexts.checkedAt },
]; ];
const dl = document.createElement('dl'); const dl = document.createElement('dl');
dl.className = 'grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2'; dl.className = 'grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2';
purchaseItems.forEach(item => { saleItems.forEach(item => {
if (game.purchaseInformation[item.key]) { if (item.value) {
const div = document.createElement('div'); const div = document.createElement('div');
div.className = 'sm:col-span-1'; div.className = 'sm:col-span-1';
div.innerHTML = ` div.innerHTML = `
<dt class="text-sm font-medium text-gray-500">${item.label}</dt> <dt class="text-sm font-medium text-gray-500">${item.label}</dt>
<dd class="mt-1 text-sm text-gray-900">${game.purchaseInformation[item.key]}</dd> <dd class="mt-1 text-sm text-gray-900">${item.value}</dd>
`; `;
dl.appendChild(div); dl.appendChild(div);
} }

View File

@@ -1,4 +1,6 @@
import NSW_DB from '../db/nsw.db.js'; import NSW_DB from '../db/nsw.resale.db.js';
const SHOW_SOLD_BY_DEFAULT = false;
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const gameList = document.getElementById('gameList'); const gameList = document.getElementById('gameList');
@@ -9,7 +11,7 @@ document.addEventListener('DOMContentLoaded', () => {
// 언어별 UI 텍스트 // 언어별 UI 텍스트
const uiTexts = { const uiTexts = {
ko: { ko: {
title: 'Switch DB', title: 'Switch 중고 판매 목록',
languageSelect: '언어 선택', languageSelect: '언어 선택',
languageDescription: '선택한 언어로 표시됩니다', languageDescription: '선택한 언어로 표시됩니다',
korean: '한국어', korean: '한국어',
@@ -18,18 +20,18 @@ document.addEventListener('DOMContentLoaded', () => {
loading: '로딩중...', loading: '로딩중...',
tableHeaders: { tableHeaders: {
title: '제목', title: '제목',
info: '정보', info: '판매가',
status: '상태', status: '판매 상태',
role: '역할', role: '가격 범위',
location: '지역' location: '지역',
}, },
sortOptions: { sortOptions: {
sortByNoDesc: '순번 최신순', sortByNoDesc: '순번 최신순',
sortByNo: '순번 과거순', sortByNo: '순번 과거순',
sortByDateDesc: '발매일 최신순', sortByDateDesc: '발매일 최신순',
sortByDate: '발매일 과거순', sortByDate: '발매일 과거순',
sortByPurchaseDateDesc: '구매일 최신순', sortByPriceDesc: '판매가 높은순',
sortByPurchaseDate: '구매일 과거순', sortByPrice: '판매가 낮은순',
sortByRandom: '무작위', sortByRandom: '무작위',
}, },
filter: { filter: {
@@ -39,7 +41,7 @@ document.addEventListener('DOMContentLoaded', () => {
}, },
}, },
ja: { ja: {
title: 'ゲーム一覧', title: '中古販売リスト',
languageSelect: '言語選択', languageSelect: '言語選択',
languageDescription: '選択した言語で表示されます', languageDescription: '選択した言語で表示されます',
korean: '韓国語', korean: '韓国語',
@@ -48,18 +50,18 @@ document.addEventListener('DOMContentLoaded', () => {
loading: '読み込み中...', loading: '読み込み中...',
tableHeaders: { tableHeaders: {
title: 'タイトル', title: 'タイトル',
info: '情報', info: '販売価格',
status: '状態', status: '販売状態',
role: '役割', role: '価格帯',
location: '地域' location: '地域',
}, },
sortOptions: { sortOptions: {
sortByNoDesc: '番号降順', sortByNoDesc: '番号降順',
sortByNo: '番号昇順', sortByNo: '番号昇順',
sortByDateDesc: '発売日降順', sortByDateDesc: '発売日降順',
sortByDate: '発売日昇順', sortByDate: '発売日昇順',
sortByPurchaseDateDesc: '購入日降順', sortByPriceDesc: '販売価格降順',
sortByPurchaseDate: '購入日昇順', sortByPrice: '販売価格昇順',
sortByRandom: 'ランダム', sortByRandom: 'ランダム',
}, },
filter: { filter: {
@@ -79,20 +81,10 @@ document.addEventListener('DOMContentLoaded', () => {
country: [], country: [],
cero: [], cero: [],
}, },
searchText: '',
sortBy: 'sortByNoDesc', // 기본 정렬 옵션 sortBy: 'sortByNoDesc', // 기본 정렬 옵션
}; };
// 정렬 옵션 정의
const sortOptions = [
{ name: '순번 최신순', value: 'sortByNoDesc' },
{ name: '순번 과거순', value: 'sortByNo' },
{ name: '발매일 최신순', value: 'sortByDateDesc' },
{ name: '발매일 과거순', value: 'sortByDate' },
{ name: '구매일 최신순', value: 'sortByPurchaseDateDesc' },
{ name: '구매일 과거순', value: 'sortByPurchaseDate' },
{ name: '무작위', value: 'sortByRandom' },
];
// UI 텍스트 업데이트 함수 // UI 텍스트 업데이트 함수
function updateUITexts() { function updateUITexts() {
const texts = uiTexts[filterState.language]; const texts = uiTexts[filterState.language];
@@ -128,11 +120,14 @@ document.addEventListener('DOMContentLoaded', () => {
languageRadios.forEach(radio => { languageRadios.forEach(radio => {
radio.addEventListener('change', e => { radio.addEventListener('change', e => {
filterState.language = e.target.value; filterState.language = e.target.value;
localStorage.setItem('language', filterState.language);
updateUITexts(); updateUITexts();
renderGames(); renderGames();
}); });
}); });
localStorage.setItem('language', filterState.language);
// 언어 변환 함수 // 언어 변환 함수
function convertLanguage(language) { function convertLanguage(language) {
if (!language) return ''; if (!language) return '';
@@ -581,27 +576,91 @@ document.addEventListener('DOMContentLoaded', () => {
} }
function formatGameData(game) { function formatGameData(game) {
const suggestedPrice = game.sale?.suggestedPrice;
const priceRange = game.sale?.priceRange;
return { return {
...game, ...game,
formattedTitle: filterState.language === 'ko' ? game.koTitle || game.title : game.title, formattedTitle: filterState.language === 'ko' ? game.koTitle || game.title : game.title,
formattedReleaseDate: convertReleaseDate(game.release),
formattedMaker: convertMaker(game.maker), formattedMaker: convertMaker(game.maker),
formattedLanguage: convertLanguage(game.language), formattedLanguage: convertLanguage(game.language),
formattedTags: convertTags(game.tags), formattedTags: convertTags(game.tags),
formattedCountry: convertCountry(game.country), formattedCountry: convertCountry(game.country),
formattedSuggestedPrice: formatKRW(suggestedPrice),
formattedPriceRange:
priceRange?.min && priceRange?.max
? `${formatKRW(priceRange.min)} ~ ${formatKRW(priceRange.max)}`
: '',
formattedPricingBasis: formatPricingBasis(game.sale?.pricingBasis),
formattedConfidenceDescription: formatConfidenceDescription(game.sale?.confidence),
formattedSaleStatus: formatSaleStatus(game.status),
}; };
} }
function getStatusClass(status) { function formatKRW(value) {
if (typeof value !== 'number') return '';
return `${value.toLocaleString('ko-KR')}`;
}
function formatSaleStatus(status) {
const labels = {
ko: {
available: '판매 가능',
sold: '판매완료',
},
ja: {
available: '販売可',
sold: '販売済み',
},
};
return labels[filterState.language][status] || status;
}
function formatPricingBasis(pricingBasis) {
const labels = {
ko: {
KR_USED_REFERENCE_ESTIMATE: '국내 중고 시세 참고',
JP_USED_REFERENCE_CONVERTED_TO_KRW_ESTIMATE: '일본 중고 시세 환산',
MANUAL: '직접 입력가',
SOLD_HISTORY: '판매 이력 기준',
},
ja: {
KR_USED_REFERENCE_ESTIMATE: '韓国中古相場参考',
JP_USED_REFERENCE_CONVERTED_TO_KRW_ESTIMATE: '日本中古相場換算',
MANUAL: '手入力価格',
SOLD_HISTORY: '販売履歴基準',
},
};
return labels[filterState.language][pricingBasis] || '';
}
function formatConfidenceDescription(confidence) {
const descriptions = {
ko: {
high: '최근 거래가가 안정적인 편이라 표시가에 가까운 거래를 기대할 수 있습니다.',
medium: '중고 시세를 참고한 합리적인 기준가이며 상태에 따라 소폭 조정될 수 있습니다.',
'medium-low': '판본, 언어, 구성품에 따라 가격 차이가 있어 확인 후 조정 가능합니다.',
low: '거래 사례가 적은 타이틀이라 상태 확인 후 가격 협의 여지가 있습니다.',
},
ja: {
high: '最近の取引価格が安定しており、表示価格に近い取引が期待できます。',
medium: '中古相場を参考にした基準価格で、状態により多少調整できます。',
'medium-low': '版、言語、付属品により価格差があるため、確認後に調整できます。',
low: '取引例が少ないタイトルのため、状態確認後に価格相談できます。',
},
};
return descriptions[filterState.language][confidence] || '';
}
function getSaleStatusClass(status) {
switch (status) { switch (status) {
case 'package': case 'sold':
return 'bg-green-100 text-green-800'; return 'bg-red-100 text-red-800';
case 'download':
return 'bg-yellow-100 text-yellow-800';
case 'expansion':
return 'bg-blue-100 text-blue-800';
default: default:
return ''; return 'bg-green-100 text-green-800';
} }
} }
@@ -627,31 +686,31 @@ document.addEventListener('DOMContentLoaded', () => {
<img class="h-10 w-10 rounded-xl object-cover" src="${game.thumbnail}" alt="" /> <img class="h-10 w-10 rounded-xl object-cover" src="${game.thumbnail}" alt="" />
</div> </div>
<div class="ml-4"> <div class="ml-4">
<div class="font-medium text-gray-900 ${ <div class="font-medium text-gray-900">
game.status === 'sold' ? 'text-red-600 line-through' : ''
}">
${game.no}. ${game.formattedTitle} ${game.no}. ${game.formattedTitle}
</div> </div>
<div class="text-gray-500">${game.formattedReleaseDate}</div> <div class="mt-1 max-w-lg text-xs leading-5 text-gray-500">
${game.formattedConfidenceDescription}
</div>
</div> </div>
</div> </div>
</td> </td>
<td class="hidden w-3/12 px-3 py-4 text-sm text-gray-500 lg:table-cell"> <td class="hidden w-32 px-3 py-4 text-sm text-gray-500 lg:table-cell">
<div class="text-gray-900">${game.formattedMaker}</div> <div class="whitespace-nowrap font-semibold text-gray-900">
<div class="text-gray-500">${game.formattedLanguage}</div> ${game.formattedSuggestedPrice || '-'}
</div>
<div class="mt-1 text-xs text-gray-400">${game.formattedPricingBasis}</div>
</td> </td>
<td class="px-3 py-4 text-sm text-gray-500 mx-auto"> <td class="w-28 px-3 py-4 text-sm text-gray-500 mx-auto">
<div class="flex justify-center rounded-full px-2 text-xs font-semibold leading-5 ${getStatusClass( <div class="flex justify-center whitespace-nowrap rounded-full px-2 text-xs font-semibold leading-5 ${getSaleStatusClass(
game.status, game.status,
)}"> )}">
${game.status} ${game.formattedSaleStatus}
</div>
<div class="text-xs mt-1 text-center text-gray-300 hidden">
${game?.purchaseInformation?.date || ''}
</div> </div>
</td> </td>
<td class="w-3/12 hidden px-3 py-4 text-sm text-gray-500 lg:table-cell"> <td class="hidden w-56 px-3 py-4 text-sm text-gray-500 lg:table-cell">
${game.formattedTags} <div class="whitespace-nowrap text-gray-900">${game.formattedPriceRange || '-'}</div>
<div class="mt-1 line-clamp-2 text-xs text-gray-400">${game.sale?.checkedAt || ''}</div>
</td> </td>
<td class="hidden py-4 pl-3 pr-4 text-right text-sm font-medium sm:table-cell"> <td class="hidden py-4 pl-3 pr-4 text-right text-sm font-medium sm:table-cell">
<div class="${getCountryClass(game.country)}"> <div class="${getCountryClass(game.country)}">
@@ -677,6 +736,8 @@ document.addEventListener('DOMContentLoaded', () => {
country: [], country: [],
cero: [], cero: [],
}; };
filterState.searchText = '';
document.getElementById('search-input').value = '';
// 게임 목록 다시 렌더링 // 게임 목록 다시 렌더링
renderGames(); renderGames();
@@ -687,6 +748,27 @@ document.addEventListener('DOMContentLoaded', () => {
// 필터 체크박스 이벤트 리스너 설정 // 필터 체크박스 이벤트 리스너 설정
function setupFilterListeners() { function setupFilterListeners() {
const searchInput = document.getElementById('search-input');
const searchForm = document.getElementById('searchForm');
const resetSearch = document.getElementById('reset-search');
searchForm.addEventListener('submit', event => {
event.preventDefault();
filterState.searchText = searchInput.value.trim().toLowerCase();
renderGames();
});
searchInput.addEventListener('input', event => {
filterState.searchText = event.target.value.trim().toLowerCase();
renderGames();
});
resetSearch.addEventListener('click', () => {
filterState.searchText = '';
searchInput.value = '';
renderGames();
});
// 언어 필터 // 언어 필터
document.querySelectorAll('input[name="language-filter"]').forEach(checkbox => { document.querySelectorAll('input[name="language-filter"]').forEach(checkbox => {
checkbox.addEventListener('change', () => { checkbox.addEventListener('change', () => {
@@ -695,7 +777,7 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
}); });
// 상태 필터 // 판매 상태 필터
document.querySelectorAll('input[name="status-filter"]').forEach(checkbox => { document.querySelectorAll('input[name="status-filter"]').forEach(checkbox => {
checkbox.addEventListener('change', () => { checkbox.addEventListener('change', () => {
updateFilters('status'); updateFilters('status');
@@ -729,48 +811,53 @@ document.addEventListener('DOMContentLoaded', () => {
// 필터링 함수 // 필터링 함수
function filterGames(games) { function filterGames(games) {
return games.filter(game => { return games.filter(game => {
if (filterState.searchText) {
const searchableText = [
game.title,
game.koTitle,
game.maker,
game.tags,
game.country,
game.status,
String(game.sale?.suggestedPrice || ''),
]
.filter(Boolean)
.join(' ')
.toLowerCase();
if (!searchableText.includes(filterState.searchText)) return false;
}
// 언어 필터 // 언어 필터
if (filterState.filters.language.length > 0) { if (filterState.filters.language.length > 0) {
const hasKorean = game.language.includes('韓国語'); const hasKorean = game.language?.includes('韓国語');
const hasKoreanFilter = filterState.filters.language.includes('koreanSupport'); const hasKoreanFilter = filterState.filters.language.includes('koreanSupport');
const hasNotSupportedFilter = filterState.filters.language.includes('koreanNotSupport'); const hasNotSupportedFilter = filterState.filters.language.includes('koreanNotSupport');
// 한국어 지원과 미지원이 모두 체크된 경우 모든 게임 표시
if (hasKoreanFilter && hasNotSupportedFilter) {
return true;
}
// 한국어 지원만 체크된 경우 한국어 지원 게임만 표시 // 한국어 지원만 체크된 경우 한국어 지원 게임만 표시
if (hasKoreanFilter && !hasNotSupportedFilter) { if (hasKoreanFilter && !hasNotSupportedFilter) {
return hasKorean; if (!hasKorean) return false;
} }
// 한국어 미지원만 체크된 경우 한국어 미지원 게임만 표시 // 한국어 미지원만 체크된 경우 한국어 미지원 게임만 표시
if (!hasKoreanFilter && hasNotSupportedFilter) { if (!hasKoreanFilter && hasNotSupportedFilter) {
return !hasKorean; if (hasKorean) return false;
} }
// 아무것도 체크되지 않은 경우 필터링하지 않음
return true;
} }
// 상태 필터 if (
if (filterState.filters.status.length > 0) { !SHOW_SOLD_BY_DEFAULT &&
const hasExtensionFilter = filterState.filters.status.includes('extension'); filterState.filters.status.length === 0 &&
const hasOtherStatusFilters = game.status === 'sold'
filterState.filters.status.filter(status => status !== 'extension').length > 0; ) {
return false;
}
// extension 필터가 체크된 경우 if (
if (hasExtensionFilter) { filterState.filters.status.length > 0 &&
// extension 값이 null이 아닌 게임만 표시 !filterState.filters.status.includes(game.status)
if (game.extension === null) return false; ) {
} return false;
// 다른 상태 필터가 체크된 경우
if (hasOtherStatusFilters) {
const otherStatuses = filterState.filters.status.filter(status => status !== 'extension');
if (!otherStatuses.includes(game.status)) return false;
}
} }
// 국가 필터 // 국가 필터
@@ -811,25 +898,15 @@ document.addEventListener('DOMContentLoaded', () => {
const dateDiff = new Date(aDate) - new Date(bDate); const dateDiff = new Date(aDate) - new Date(bDate);
return dateDiff === 0 ? b.no - a.no : dateDiff; return dateDiff === 0 ? b.no - a.no : dateDiff;
}); });
case 'sortByPurchaseDateDesc': case 'sortByPriceDesc':
return [...games].sort((a, b) => { return [...games].sort((a, b) => {
if (!a.purchaseInformation?.date || !b.purchaseInformation?.date) { const priceDiff = (b.sale?.suggestedPrice || 0) - (a.sale?.suggestedPrice || 0);
return !a.purchaseInformation?.date ? 1 : -1; return priceDiff === 0 ? b.no - a.no : priceDiff;
}
const aDate = new Date(a.purchaseInformation.date.replace(/\./g, '/')).toUTCString();
const bDate = new Date(b.purchaseInformation.date.replace(/\./g, '/')).toUTCString();
const dateDiff = new Date(bDate) - new Date(aDate);
return dateDiff === 0 ? b.no - a.no : dateDiff;
}); });
case 'sortByPurchaseDate': case 'sortByPrice':
return [...games].sort((a, b) => { return [...games].sort((a, b) => {
if (!a.purchaseInformation?.date || !b.purchaseInformation?.date) { const priceDiff = (a.sale?.suggestedPrice || 0) - (b.sale?.suggestedPrice || 0);
return !a.purchaseInformation?.date ? 1 : -1; return priceDiff === 0 ? b.no - a.no : priceDiff;
}
const aDate = new Date(a.purchaseInformation.date.replace(/\./g, '/')).toUTCString();
const bDate = new Date(b.purchaseInformation.date.replace(/\./g, '/')).toUTCString();
const dateDiff = new Date(aDate) - new Date(bDate);
return dateDiff === 0 ? b.no - a.no : dateDiff;
}); });
case 'sortByRandom': case 'sortByRandom':
return [...games].sort(() => Math.random() - 0.5); return [...games].sort(() => Math.random() - 0.5);