1251 lines
40 KiB
JavaScript
1251 lines
40 KiB
JavaScript
import NSW_DB from '../db/nsw.resale.db.js?v=20260521-badges-v2';
|
|
|
|
window.NSW_APP_VERSION = '20260521-badges-v2';
|
|
|
|
const SHOW_SOLD_BY_DEFAULT = false;
|
|
const SHOW_RECOMMENDED_PRICE_RANGE_BY_DEFAULT = false;
|
|
|
|
/** @type {string} 가격 확인 완료 표시 */
|
|
const VERIFIED_PRICE_MARK = '⭐';
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const gameList = document.getElementById('gameList');
|
|
const gameCount = document.getElementById('gameCount');
|
|
const loading = document.getElementById('loading');
|
|
const languageRadios = document.querySelectorAll('input[name="language"]');
|
|
|
|
// 언어별 UI 텍스트
|
|
const uiTexts = {
|
|
ko: {
|
|
title: 'Switch 패키지 판매 목록',
|
|
languageSelect: '언어 선택',
|
|
languageDescription: '선택한 언어로 표시됩니다',
|
|
korean: '한국어',
|
|
japanese: '일본어',
|
|
count: '개수',
|
|
listIntro: '우선 이런 게임들이 있습니다. 가격은 순차적으로 작성중입니다.',
|
|
pricePending: '-',
|
|
loading: '로딩중...',
|
|
tableHeaders: {
|
|
title: '제목',
|
|
info: '판매가',
|
|
status: '판매 상태',
|
|
},
|
|
sortOptions: {
|
|
sortByDateDesc: '발매일 최신순',
|
|
sortByDate: '발매일 과거순',
|
|
sortByPriceDesc: '판매가 높은순',
|
|
sortByPrice: '판매가 낮은순',
|
|
sortByRandom: '무작위',
|
|
},
|
|
filter: {
|
|
reset: '필터 초기화',
|
|
koreanSupport: '한국어 지원',
|
|
koreanNotSupport: '한국어 미지원',
|
|
},
|
|
},
|
|
ja: {
|
|
title: '中古販売リスト',
|
|
languageSelect: '言語選択',
|
|
languageDescription: '選択した言語で表示されます',
|
|
korean: '韓国語',
|
|
japanese: '日本語',
|
|
count: '件数',
|
|
listIntro: 'まずは所持タイトルを掲載しています。価格は順次入力中です。',
|
|
pricePending: '-',
|
|
loading: '読み込み中...',
|
|
tableHeaders: {
|
|
title: 'タイトル',
|
|
info: '販売価格',
|
|
status: '販売状態',
|
|
},
|
|
sortOptions: {
|
|
sortByDateDesc: '発売日降順',
|
|
sortByDate: '発売日昇順',
|
|
sortByPriceDesc: '販売価格降順',
|
|
sortByPrice: '販売価格昇順',
|
|
sortByRandom: 'ランダム',
|
|
},
|
|
filter: {
|
|
reset: 'フィルターリセット',
|
|
koreanSupport: '韓国語対応',
|
|
koreanNotSupport: '韓国語非対応',
|
|
},
|
|
},
|
|
};
|
|
|
|
// 필터 상태 관리 객체
|
|
const filterState = {
|
|
language: 'ko',
|
|
filters: {
|
|
language: [],
|
|
status: [],
|
|
country: [],
|
|
cero: [],
|
|
},
|
|
searchText: '',
|
|
sortBy: 'sortByRandom',
|
|
};
|
|
|
|
// UI 텍스트 업데이트 함수
|
|
function updateUITexts() {
|
|
const texts = uiTexts[filterState.language];
|
|
|
|
// 제목 업데이트
|
|
document.querySelector('h1').textContent = texts.title;
|
|
|
|
// 언어 선택 섹션 업데이트
|
|
setTextContent('label.text-base', texts.languageSelect);
|
|
setTextContent('p.text-sm', texts.languageDescription);
|
|
setTextContent('label[for="ko"]', texts.korean);
|
|
setTextContent('label[for="ja"]', texts.japanese);
|
|
|
|
// 테이블 헤더 업데이트
|
|
const headers = document.querySelectorAll('th');
|
|
if (headers[0]) headers[0].textContent = texts.tableHeaders.title;
|
|
if (headers[1]) headers[1].textContent = texts.tableHeaders.info;
|
|
if (headers[2]) headers[2].textContent = texts.tableHeaders.status;
|
|
|
|
// 로딩 텍스트 업데이트
|
|
loading.textContent = texts.loading;
|
|
setTextContent('#priceProgressNotice', texts.listIntro);
|
|
|
|
// 필터 텍스트 업데이트
|
|
setTextContent('#resetFilters', texts.filter.reset);
|
|
setTextContent('label[for="korean-support"]', texts.filter.koreanSupport);
|
|
setTextContent('label[for="korean-not-support"]', texts.filter.koreanNotSupport);
|
|
}
|
|
|
|
function setTextContent(selector, text) {
|
|
const element = document.querySelector(selector);
|
|
if (element) element.textContent = text;
|
|
}
|
|
|
|
// 언어 변경 이벤트 리스너
|
|
languageRadios.forEach(radio => {
|
|
radio.addEventListener('change', e => {
|
|
filterState.language = e.target.value;
|
|
localStorage.setItem('language', filterState.language);
|
|
updateUITexts();
|
|
renderGames();
|
|
});
|
|
});
|
|
|
|
localStorage.setItem('language', filterState.language);
|
|
|
|
// 언어 변환 함수
|
|
function convertLanguage(language) {
|
|
if (!language) return '';
|
|
if (filterState.language !== 'ko') return language;
|
|
|
|
const languages = language.split(',');
|
|
const koreanLanguages = [];
|
|
|
|
for (let i = 0; i < languages.length; i++) {
|
|
switch (languages[i].trim()) {
|
|
case '日本語':
|
|
koreanLanguages.push('일본어');
|
|
break;
|
|
case '英語':
|
|
koreanLanguages.push('영어');
|
|
break;
|
|
case '韓国語':
|
|
koreanLanguages.push('한국어');
|
|
break;
|
|
case 'フランス語':
|
|
koreanLanguages.push('프랑스어');
|
|
break;
|
|
case 'ドイツ語':
|
|
koreanLanguages.push('독일어');
|
|
break;
|
|
case 'イタリア語':
|
|
koreanLanguages.push('이탈리아어');
|
|
break;
|
|
case 'スペイン語':
|
|
koreanLanguages.push('스페인어');
|
|
break;
|
|
case 'ロシア語':
|
|
koreanLanguages.push('러시아어');
|
|
break;
|
|
case 'オランダ語':
|
|
koreanLanguages.push('네덜란드어');
|
|
break;
|
|
case 'ポルトガル語':
|
|
koreanLanguages.push('포르투칼어');
|
|
break;
|
|
case '中国語 (簡体字)':
|
|
koreanLanguages.push('중국어 (간체)');
|
|
break;
|
|
case '中国語 (繁体字)':
|
|
koreanLanguages.push('중국어 (번체)');
|
|
break;
|
|
default:
|
|
koreanLanguages.push(languages[i]);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return koreanLanguages.join(', ');
|
|
}
|
|
|
|
// 메이커 변환 함수
|
|
function convertMaker(maker) {
|
|
if (!maker) return '';
|
|
if (filterState.language !== 'ko') return maker;
|
|
|
|
const makers = maker.split(',');
|
|
const koreanMakers = [];
|
|
|
|
for (let i = 0; i < makers.length; i++) {
|
|
switch (makers[i].trim()) {
|
|
// 메이저 Maker
|
|
case '任天堂':
|
|
koreanMakers.push('닌텐도');
|
|
break;
|
|
case 'スクウェア・エニックス':
|
|
koreanMakers.push('스퀘어 에닉스');
|
|
break;
|
|
case 'コーエーテクモゲームス':
|
|
koreanMakers.push('코에이 테크모 게임스');
|
|
break;
|
|
case 'ポケモン':
|
|
koreanMakers.push('포켓몬');
|
|
break;
|
|
case 'バンダイナムコエンターテインメント':
|
|
koreanMakers.push('반다이 남코 엔터테인먼트');
|
|
break;
|
|
case 'アトラス':
|
|
koreanMakers.push('아틀러스');
|
|
break;
|
|
case 'セガ':
|
|
koreanMakers.push('세가');
|
|
break;
|
|
// 소규모 Maker
|
|
case '日本一ソフトウェア':
|
|
koreanMakers.push('니폰이치 소프트웨어');
|
|
break;
|
|
case 'エンターグラム':
|
|
koreanMakers.push('Entergram');
|
|
break;
|
|
case 'フライハイワークス':
|
|
koreanMakers.push('Flyhigh Works');
|
|
break;
|
|
case 'フリュー':
|
|
koreanMakers.push('후류');
|
|
break;
|
|
case 'ポノス':
|
|
koreanMakers.push('포노스');
|
|
break;
|
|
case 'マーベラス':
|
|
koreanMakers.push('마블러스 엔터테인먼트');
|
|
break;
|
|
case 'スパイク・チュンソフト':
|
|
koreanMakers.push('스파이크 춘 소프트');
|
|
break;
|
|
case 'ドラガミゲームス':
|
|
koreanMakers.push('DRAGAMI GAMES');
|
|
break;
|
|
case 'アイディアファクトリー':
|
|
koreanMakers.push('Idea Factory');
|
|
break;
|
|
case 'アークシステムワークス':
|
|
koreanMakers.push('아크 시스템 웍스');
|
|
break;
|
|
case 'ユービーアイソフト':
|
|
koreanMakers.push('유비소프트');
|
|
break;
|
|
case 'レイアーク':
|
|
koreanMakers.push('레이아크');
|
|
break;
|
|
case 'ワンオアエイト':
|
|
koreanMakers.push('One or Eight');
|
|
break;
|
|
case 'インティ・クリエイツ':
|
|
koreanMakers.push('인티 크리에이츠');
|
|
break;
|
|
case 'クラウディッドレパードエンタテインメント':
|
|
koreanMakers.push('Clouded Leopard Entertainment');
|
|
break;
|
|
case 'カプコン':
|
|
koreanMakers.push('캡콤');
|
|
break;
|
|
case 'プロトタイプ':
|
|
koreanMakers.push('PROTOTYPE Ltd');
|
|
break;
|
|
case 'ディースリー・パブリッシャー':
|
|
koreanMakers.push('D3 퍼블리셔');
|
|
break;
|
|
case 'タイトー':
|
|
koreanMakers.push('타이토');
|
|
break;
|
|
case 'シーエフケー':
|
|
koreanMakers.push('CFK');
|
|
break;
|
|
case 'アクワイア':
|
|
koreanMakers.push('어콰이어');
|
|
break;
|
|
case '日本ファルコム':
|
|
koreanMakers.push('일본 팔콤');
|
|
break;
|
|
// 거르면 되는 Maker
|
|
case 'ヒューネックス':
|
|
koreanMakers.push('휴넥스');
|
|
break;
|
|
case 'アニプレックス':
|
|
koreanMakers.push('애니플렉스');
|
|
break;
|
|
case 'ジー・モード':
|
|
koreanMakers.push('G-Mode');
|
|
break;
|
|
case 'イザナギゲームズ':
|
|
koreanMakers.push('IzanagiGames');
|
|
break;
|
|
case 'ラセングル':
|
|
koreanMakers.push('Lasengle');
|
|
break;
|
|
case 'ケムコ':
|
|
koreanMakers.push('켐코');
|
|
break;
|
|
case 'ネットマーブルコーポレーション':
|
|
koreanMakers.push('넷마블');
|
|
break;
|
|
case '工画堂スタジオ':
|
|
koreanMakers.push('코가도 스튜디오');
|
|
break;
|
|
case '賈船':
|
|
koreanMakers.push('COSEN');
|
|
break;
|
|
case 'ジェムドロップ':
|
|
koreanMakers.push('잼드롭');
|
|
break;
|
|
case 'アレス':
|
|
koreanMakers.push('아레스');
|
|
break;
|
|
case '日本コロムビア':
|
|
koreanMakers.push('일본콜롬비아');
|
|
break;
|
|
case 'クリプトン・フューチャー・メディア':
|
|
koreanMakers.push('크립톤 퓨처 미디어');
|
|
break;
|
|
case 'イマジニア':
|
|
koreanMakers.push('이매지니아');
|
|
break;
|
|
default:
|
|
koreanMakers.push(makers[i]);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return koreanMakers.join(', ');
|
|
}
|
|
|
|
// 태그 변환 함수
|
|
function convertTags(tags) {
|
|
if (!tags) return '';
|
|
if (filterState.language !== 'ko') return tags;
|
|
|
|
const tagList = tags.split(',');
|
|
const koreanTags = [];
|
|
|
|
for (let i = 0; i < tagList.length; i++) {
|
|
switch (tagList[i].trim()) {
|
|
case 'パーティー':
|
|
koreanTags.push('파티');
|
|
break;
|
|
case 'シューティング':
|
|
koreanTags.push('슈팅');
|
|
break;
|
|
case '格闘':
|
|
koreanTags.push('격투');
|
|
break;
|
|
case 'パズル':
|
|
koreanTags.push('퍼즐');
|
|
break;
|
|
case 'レース':
|
|
koreanTags.push('레이스');
|
|
break;
|
|
case 'ロールプレイング':
|
|
koreanTags.push('롤플레잉');
|
|
break;
|
|
case 'アクション':
|
|
koreanTags.push('액션');
|
|
break;
|
|
case 'ストラテジー':
|
|
koreanTags.push('전략');
|
|
break;
|
|
case 'ドット絵':
|
|
koreanTags.push('도트 그림');
|
|
break;
|
|
case 'サバイバル':
|
|
koreanTags.push('서바이벌');
|
|
break;
|
|
case 'リズムに合わせて':
|
|
koreanTags.push('리듬에 맞춰');
|
|
break;
|
|
case 'カードゲーム':
|
|
koreanTags.push('카드 게임');
|
|
break;
|
|
case '囲碁・将棋':
|
|
koreanTags.push('바둑・쇼기');
|
|
break;
|
|
case 'ボードゲーム':
|
|
koreanTags.push('보드 게임');
|
|
break;
|
|
case 'サッカー':
|
|
koreanTags.push('축구');
|
|
break;
|
|
case 'テーブルゲーム':
|
|
koreanTags.push('테이블 게임');
|
|
break;
|
|
case 'トランプ':
|
|
koreanTags.push('트럼프');
|
|
break;
|
|
case '麻雀':
|
|
koreanTags.push('마작');
|
|
break;
|
|
case 'トレーニング':
|
|
koreanTags.push('트레이닝');
|
|
break;
|
|
case '視点切りかえ':
|
|
koreanTags.push('관점의 전환');
|
|
break;
|
|
case 'あそびが盛りだくさん':
|
|
koreanTags.push('다양한 즐길거리');
|
|
break;
|
|
case '新しいエリアを切りひらく':
|
|
koreanTags.push('새로운 지역을 개척한다');
|
|
break;
|
|
case 'あそぶたびにマップが新しい':
|
|
koreanTags.push('즐길때마다 새로운 맵');
|
|
break;
|
|
case '大人数でバトル':
|
|
koreanTags.push('많은 인원과 배틀');
|
|
break;
|
|
case '最速タイムにチャレンジ':
|
|
koreanTags.push('가장 빠른 시간에 도전');
|
|
break;
|
|
case 'シミュレーション':
|
|
koreanTags.push('시뮬레이션');
|
|
break;
|
|
case 'スポーツ':
|
|
koreanTags.push('스포츠');
|
|
break;
|
|
case 'バレーボール':
|
|
koreanTags.push('배구');
|
|
break;
|
|
case 'ゴルフ':
|
|
koreanTags.push('골프');
|
|
break;
|
|
case 'バスケットボール':
|
|
koreanTags.push('농구');
|
|
break;
|
|
case 'テニス':
|
|
koreanTags.push('테니스');
|
|
break;
|
|
case '野球':
|
|
koreanTags.push('야구');
|
|
break;
|
|
case 'ボクシング':
|
|
koreanTags.push('복싱');
|
|
break;
|
|
case '経営シミュレーション':
|
|
koreanTags.push('경영 시뮬레이션');
|
|
break;
|
|
case 'コミュニケーション':
|
|
koreanTags.push('커뮤니케이션');
|
|
break;
|
|
case '音楽ゲーム':
|
|
koreanTags.push('음악 게임');
|
|
break;
|
|
case '学習・教育':
|
|
koreanTags.push('학습・교육');
|
|
break;
|
|
case 'ツール':
|
|
koreanTags.push('도구');
|
|
break;
|
|
case 'アドベンチャー':
|
|
koreanTags.push('모험');
|
|
break;
|
|
case '失敗したら最初から':
|
|
koreanTags.push('실패하면 처음부터');
|
|
break;
|
|
case 'なぞ解き':
|
|
koreanTags.push('수수께끼');
|
|
break;
|
|
case 'せまる敵から守りぬく':
|
|
koreanTags.push('다가오는 적으로부터 지킨다');
|
|
break;
|
|
case 'すばやい判断がきめ手':
|
|
koreanTags.push('재빠른 판단이 관건');
|
|
break;
|
|
case 'つくれる・あそべる':
|
|
koreanTags.push('만들고 즐긴다');
|
|
break;
|
|
case 'テキストアドベンチャー':
|
|
koreanTags.push('텍스트 어드벤처');
|
|
break;
|
|
case '恋愛':
|
|
koreanTags.push('연애');
|
|
break;
|
|
case '最高スコアにチャレンジ':
|
|
koreanTags.push('최고 점수에 도전');
|
|
break;
|
|
case '世界を自由にかけ回る':
|
|
koreanTags.push('세계를 자유롭게 돌아다니다');
|
|
break;
|
|
case '落下に注意':
|
|
koreanTags.push('낙하주의');
|
|
break;
|
|
case '目的はあなた次第':
|
|
koreanTags.push('목적은 당신이 선택');
|
|
break;
|
|
case '戦うたびに強くなる':
|
|
koreanTags.push('싸울수록 성장');
|
|
break;
|
|
case '手足や体と連動して操作':
|
|
koreanTags.push('몸과 연동하여 조작');
|
|
break;
|
|
case 'オンラインで対戦':
|
|
koreanTags.push('온라인 대전');
|
|
break;
|
|
case 'オンラインで協力':
|
|
koreanTags.push('온라인 협력');
|
|
break;
|
|
case 'キャラクターカスタマイズ':
|
|
koreanTags.push('캐릭터 커스터마이즈');
|
|
break;
|
|
case '3人称視点':
|
|
koreanTags.push('3인칭 시점');
|
|
break;
|
|
case 'Toy-Conが使える':
|
|
koreanTags.push('Toy-Con을 사용할 수 있다');
|
|
break;
|
|
case '難易度が選べる':
|
|
koreanTags.push('난이도 선택 가능');
|
|
break;
|
|
case 'キャラクターボイス':
|
|
koreanTags.push('케릭터 음성');
|
|
break;
|
|
case 'オンラインでフレンドと':
|
|
koreanTags.push('친구와 온라인으로');
|
|
break;
|
|
case '1台の本体でいっしょにあそべる':
|
|
koreanTags.push('한 대의 본체에서 함께 놀 수 있다');
|
|
break;
|
|
case 'ともだちや家族と集まって':
|
|
koreanTags.push('친구와 가족과 함께');
|
|
break;
|
|
case 'オンラインランキング':
|
|
koreanTags.push('온라인 랭킹');
|
|
break;
|
|
case '本体を持ちよってあそべる':
|
|
koreanTags.push('게임기를 들고 플레이');
|
|
break;
|
|
case 'クロスプラットフォームプレイ対応':
|
|
koreanTags.push('크로스 플랫폼 플레이 대응');
|
|
break;
|
|
default:
|
|
koreanTags.push(tagList[i]);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return koreanTags.join(', ');
|
|
}
|
|
|
|
// 출시일 변환 함수
|
|
function convertReleaseDate(releaseDate) {
|
|
if (!releaseDate) return '';
|
|
if (filterState.language !== 'ko') return releaseDate;
|
|
|
|
const date = new Date(releaseDate.replace(/年|月/g, '/').replace('日', ''));
|
|
const year = date.getFullYear();
|
|
const month = date.getMonth() + 1;
|
|
const day = date.getDate();
|
|
|
|
return `${year}년 ${month}월 ${day}일`;
|
|
}
|
|
|
|
// 국가 변환 함수
|
|
function convertCountry(country) {
|
|
if (!country) return '';
|
|
|
|
switch (country) {
|
|
case 'JPN':
|
|
return filterState.language === 'ko' ? '일본판' : '日本版';
|
|
case 'KOR':
|
|
return filterState.language === 'ko' ? '한국판' : '韓国版';
|
|
default:
|
|
return country;
|
|
}
|
|
}
|
|
|
|
function formatGameData(game) {
|
|
const suggestedPrice = game.sale?.suggestedPrice;
|
|
const priceRange = game.sale?.priceRange;
|
|
|
|
return {
|
|
...game,
|
|
formattedTitle: filterState.language === 'ko' ? game.koTitle || game.title : game.title,
|
|
formattedMaker: convertMaker(game.maker),
|
|
formattedLanguage: convertLanguage(game.language),
|
|
formattedTags: convertTags(game.tags),
|
|
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),
|
|
formattedItemCondition: formatItemCondition(game.itemCondition),
|
|
formattedEdition: formatEdition(game.country),
|
|
formattedKoreanSupport: formatKoreanSupport(game.language),
|
|
hasItemCondition: Boolean(formatItemCondition(game.itemCondition)),
|
|
hasKoreanSupport: hasKoreanLanguage(game.language),
|
|
};
|
|
}
|
|
|
|
function formatItemCondition(itemCondition) {
|
|
if (!itemCondition || itemCondition === '-') return '';
|
|
|
|
const labels = {
|
|
ko: {
|
|
SEALED: '미개봉',
|
|
OPENED: '개봉',
|
|
},
|
|
ja: {
|
|
SEALED: '未開封',
|
|
OPENED: '開封済',
|
|
},
|
|
};
|
|
|
|
return labels[filterState.language][itemCondition] || itemCondition;
|
|
}
|
|
|
|
function formatEdition(country) {
|
|
if (country === 'JPN') {
|
|
return filterState.language === 'ko' ? '🇯🇵 일본판' : '🇯🇵 日本版';
|
|
}
|
|
|
|
if (country === 'KOR') {
|
|
return filterState.language === 'ko' ? '🇰🇷 한국 정발판' : '🇰🇷 韓国版';
|
|
}
|
|
|
|
return country || '';
|
|
}
|
|
|
|
function hasKoreanLanguage(language) {
|
|
return Boolean(language?.includes('韓国語') || language?.includes('한국어'));
|
|
}
|
|
|
|
function formatKoreanSupport(language) {
|
|
const isSupported = hasKoreanLanguage(language);
|
|
|
|
if (filterState.language === 'ko') {
|
|
return isSupported ? '🇰🇷 한국어 지원' : '❌ 한국어 미지원';
|
|
}
|
|
|
|
return isSupported ? '🇰🇷 韓国語対応' : '❌ 韓国語非対応';
|
|
}
|
|
|
|
function formatKRW(value) {
|
|
if (typeof value !== 'number') return uiTexts[filterState.language].pricePending;
|
|
return `${value.toLocaleString('ko-KR')}원`;
|
|
}
|
|
|
|
function getSuggestedPriceValue(game) {
|
|
const suggestedPrice = game.sale?.suggestedPrice;
|
|
return typeof suggestedPrice === 'number' ? suggestedPrice : null;
|
|
}
|
|
|
|
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) {
|
|
case 'sold':
|
|
return 'bg-red-100 text-red-800';
|
|
default:
|
|
return 'bg-green-100 text-green-800';
|
|
}
|
|
}
|
|
|
|
function getItemConditionClass(itemCondition) {
|
|
return 'bg-amber-100 text-amber-800';
|
|
}
|
|
|
|
function getEditionClass(country) {
|
|
return country === 'JPN' ? 'bg-red-100 text-red-800' : 'bg-indigo-100 text-indigo-800';
|
|
}
|
|
|
|
function getKoreanSupportClass(hasKoreanSupport) {
|
|
return hasKoreanSupport ? 'bg-emerald-100 text-emerald-800' : 'bg-gray-100 text-gray-600';
|
|
}
|
|
|
|
function renderInfoBadges(game, justifyClass = '') {
|
|
return `
|
|
<div class="flex flex-wrap items-center gap-1.5 ${justifyClass}">
|
|
${
|
|
game.hasItemCondition
|
|
? `<span class="rounded-full px-2 text-xs font-semibold leading-5 ${getItemConditionClass(
|
|
game.itemCondition,
|
|
)}">
|
|
${game.formattedItemCondition}
|
|
</span>`
|
|
: ''
|
|
}
|
|
<span class="rounded-full px-2 text-xs font-semibold leading-5 ${getEditionClass(
|
|
game.country,
|
|
)}">
|
|
${game.formattedEdition}
|
|
</span>
|
|
<span class="rounded-full px-2 text-xs font-semibold leading-5 ${getKoreanSupportClass(
|
|
game.hasKoreanSupport,
|
|
)}">
|
|
${game.formattedKoreanSupport}
|
|
</span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function createGameRow(game, index) {
|
|
const tr = document.createElement('tr');
|
|
tr.className = `cursor-pointer hover:bg-indigo-50 ${index % 2 === 0 ? '' : 'bg-gray-50'}`;
|
|
|
|
// 클릭 시 nsw-detail.html로 이동 (no 쿼리 포함)
|
|
tr.addEventListener('click', () => {
|
|
window.location.href = `nsw-detail.html?no=${encodeURIComponent(game.no)}`;
|
|
});
|
|
|
|
tr.innerHTML = `
|
|
<td class="py-4 pl-4 pr-3 text-sm">
|
|
<div class="flex items-start">
|
|
<div class="h-10 w-10 flex-shrink-0">
|
|
<img class="h-10 w-10 rounded-xl object-cover" src="${game.thumbnail}" alt="" />
|
|
</div>
|
|
<div class="ml-4 min-w-0 flex-1">
|
|
<div class="font-medium leading-5 text-gray-900 flex items-center gap-1">
|
|
${game.sale?.priceVerified ? `${VERIFIED_PRICE_MARK} ` : ''}<span>${game.formattedTitle}</span>
|
|
</div>
|
|
<div class="mt-2">
|
|
${renderInfoBadges(game)}
|
|
</div>
|
|
<div class="mt-1 max-w-lg text-xs leading-5 text-gray-500">
|
|
${game.formattedConfidenceDescription}
|
|
</div>
|
|
<div class="mt-3 space-y-2 sm:hidden">
|
|
<div class="flex flex-wrap items-center gap-x-3 gap-y-1">
|
|
<span class="text-base font-semibold leading-6 text-gray-900">
|
|
${game.formattedSuggestedPrice || '-'}
|
|
</span>
|
|
<span class="rounded-full px-2 text-xs font-semibold leading-5 ${getSaleStatusClass(
|
|
game.status,
|
|
)}">
|
|
${game.formattedSaleStatus}
|
|
</span>
|
|
</div>
|
|
<div class="text-xs leading-5 text-gray-500">
|
|
${game.formattedPricingBasis}
|
|
</div>
|
|
${
|
|
SHOW_RECOMMENDED_PRICE_RANGE_BY_DEFAULT
|
|
? `<div class="text-xs leading-5 text-gray-500">
|
|
${filterState.language === 'ko' ? '가격 범위' : '価格帯'}:
|
|
${game.formattedPriceRange || '-'}
|
|
</div>`
|
|
: ''
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="hidden w-56 px-3 py-4 text-sm text-gray-500 sm:table-cell">
|
|
<div class="font-semibold leading-5 text-gray-900">
|
|
${game.formattedSuggestedPrice || '-'}
|
|
</div>
|
|
<div class="mt-1 text-xs text-gray-400">${game.formattedPricingBasis}</div>
|
|
</td>
|
|
<td class="hidden w-28 px-3 py-4 text-sm text-gray-500 mx-auto sm:table-cell">
|
|
<div class="flex justify-center whitespace-nowrap rounded-full px-2 text-xs font-semibold leading-5 ${getSaleStatusClass(
|
|
game.status,
|
|
)}">
|
|
${game.formattedSaleStatus}
|
|
</div>
|
|
</td>
|
|
`;
|
|
|
|
return tr;
|
|
}
|
|
|
|
// 필터 초기화 함수
|
|
function resetFilters() {
|
|
// 모든 체크박스 해제
|
|
document.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
|
|
checkbox.checked = false;
|
|
});
|
|
|
|
// 필터 상태 초기화
|
|
filterState.filters = {
|
|
language: [],
|
|
status: [],
|
|
country: [],
|
|
cero: [],
|
|
};
|
|
filterState.searchText = '';
|
|
document.querySelectorAll('[data-search-input]').forEach(input => {
|
|
input.value = '';
|
|
});
|
|
|
|
// 게임 목록 다시 렌더링
|
|
renderGames();
|
|
}
|
|
|
|
// 필터 초기화 버튼 이벤트 리스너 설정
|
|
document.getElementById('resetFilters')?.addEventListener('click', resetFilters);
|
|
|
|
// 필터 체크박스 이벤트 리스너 설정
|
|
function setupFilterListeners() {
|
|
const searchInputs = [
|
|
document.getElementById('search-input'),
|
|
document.getElementById('mobile-search-input'),
|
|
].filter(Boolean);
|
|
const searchForms = [document.getElementById('searchForm')].filter(Boolean);
|
|
const resetSearchButtons = [
|
|
document.getElementById('reset-search'),
|
|
document.getElementById('mobile-reset-search'),
|
|
].filter(Boolean);
|
|
|
|
function updateSearchText(value) {
|
|
filterState.searchText = value.trim().toLowerCase();
|
|
searchInputs.forEach(input => {
|
|
if (input.value !== value) input.value = value;
|
|
});
|
|
renderGames();
|
|
}
|
|
|
|
window.updateNswSearch = value => {
|
|
updateSearchText(value || '');
|
|
};
|
|
|
|
window.clearNswSearch = () => {
|
|
updateSearchText('');
|
|
};
|
|
|
|
function updateSearchFromInput(input, options = {}) {
|
|
window.setTimeout(() => {
|
|
updateSearchText(input.value);
|
|
if (options.blur) input.blur();
|
|
}, 0);
|
|
}
|
|
|
|
function isSearchInput(target) {
|
|
return target instanceof HTMLInputElement && target.matches('[data-search-input]');
|
|
}
|
|
|
|
document.addEventListener(
|
|
'submit',
|
|
event => {
|
|
if (!event.target.querySelector?.('[data-search-input]')) return;
|
|
event.preventDefault();
|
|
if (event.stopImmediatePropagation) event.stopImmediatePropagation();
|
|
event.stopPropagation();
|
|
const searchInput = event.target.querySelector('[data-search-input]');
|
|
if (searchInput) updateSearchFromInput(searchInput, { blur: true });
|
|
},
|
|
true
|
|
);
|
|
|
|
document.addEventListener(
|
|
'input',
|
|
event => {
|
|
if (!isSearchInput(event.target)) return;
|
|
updateSearchText(event.target.value);
|
|
},
|
|
true
|
|
);
|
|
|
|
document.addEventListener(
|
|
'change',
|
|
event => {
|
|
if (!isSearchInput(event.target)) return;
|
|
updateSearchFromInput(event.target);
|
|
},
|
|
true
|
|
);
|
|
|
|
document.addEventListener(
|
|
'focusout',
|
|
event => {
|
|
if (!isSearchInput(event.target)) return;
|
|
updateSearchFromInput(event.target);
|
|
},
|
|
true
|
|
);
|
|
|
|
searchForms.forEach(form => {
|
|
form.addEventListener(
|
|
'submit',
|
|
event => {
|
|
event.preventDefault();
|
|
if (event.stopImmediatePropagation) event.stopImmediatePropagation();
|
|
event.stopPropagation();
|
|
const searchInput = form.querySelector('[data-search-input]');
|
|
if (searchInput) updateSearchFromInput(searchInput, { blur: true });
|
|
},
|
|
true
|
|
);
|
|
});
|
|
|
|
searchInputs.forEach(input => {
|
|
let isComposing = false;
|
|
|
|
input.addEventListener('compositionstart', () => {
|
|
isComposing = true;
|
|
});
|
|
|
|
input.addEventListener('blur', event => {
|
|
updateSearchFromInput(event.target);
|
|
});
|
|
|
|
input.addEventListener('compositionend', event => {
|
|
isComposing = false;
|
|
updateSearchFromInput(event.target);
|
|
});
|
|
|
|
input.addEventListener('keydown', event => {
|
|
if (event.key !== 'Enter') return;
|
|
if (isComposing || event.isComposing || event.keyCode === 229) return;
|
|
event.preventDefault();
|
|
updateSearchFromInput(event.target, { blur: true });
|
|
});
|
|
|
|
input.addEventListener('keyup', event => {
|
|
if (isComposing || event.isComposing || event.keyCode === 229) return;
|
|
updateSearchFromInput(event.target, { blur: event.key === 'Enter' });
|
|
});
|
|
});
|
|
|
|
resetSearchButtons.forEach(button => {
|
|
function clearSearch(event) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
updateSearchText('');
|
|
}
|
|
|
|
button.addEventListener('pointerdown', clearSearch);
|
|
button.addEventListener('touchstart', clearSearch);
|
|
button.addEventListener('click', clearSearch);
|
|
});
|
|
|
|
// 언어 필터
|
|
document.querySelectorAll('input[name="language-filter"]').forEach(checkbox => {
|
|
checkbox.addEventListener('change', () => {
|
|
updateFilters('language');
|
|
renderGames();
|
|
});
|
|
});
|
|
|
|
// 판매 상태 필터
|
|
document.querySelectorAll('input[name="status-filter"]').forEach(checkbox => {
|
|
checkbox.addEventListener('change', () => {
|
|
updateFilters('status');
|
|
renderGames();
|
|
});
|
|
});
|
|
|
|
// 국가 필터
|
|
document.querySelectorAll('input[name="country-filter"]').forEach(checkbox => {
|
|
checkbox.addEventListener('change', () => {
|
|
updateFilters('country');
|
|
renderGames();
|
|
});
|
|
});
|
|
|
|
// CERO 필터
|
|
document.querySelectorAll('input[name="cero-filter"]').forEach(checkbox => {
|
|
checkbox.addEventListener('change', () => {
|
|
updateFilters('cero');
|
|
renderGames();
|
|
});
|
|
});
|
|
}
|
|
|
|
// 필터 상태 업데이트
|
|
function updateFilters(type) {
|
|
const checkboxes = document.querySelectorAll(`input[name="${type}-filter"]:checked`);
|
|
filterState.filters[type] = Array.from(checkboxes).map(checkbox => checkbox.value);
|
|
}
|
|
|
|
// 필터링 함수
|
|
function filterGames(games) {
|
|
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) {
|
|
const hasKorean = game.language?.includes('韓国語');
|
|
const hasKoreanFilter = filterState.filters.language.includes('koreanSupport');
|
|
const hasNotSupportedFilter = filterState.filters.language.includes('koreanNotSupport');
|
|
|
|
// 한국어 지원만 체크된 경우 한국어 지원 게임만 표시
|
|
if (hasKoreanFilter && !hasNotSupportedFilter) {
|
|
if (!hasKorean) return false;
|
|
}
|
|
|
|
// 한국어 미지원만 체크된 경우 한국어 미지원 게임만 표시
|
|
if (!hasKoreanFilter && hasNotSupportedFilter) {
|
|
if (hasKorean) return false;
|
|
}
|
|
}
|
|
|
|
if (
|
|
!SHOW_SOLD_BY_DEFAULT &&
|
|
filterState.filters.status.length === 0 &&
|
|
game.status === 'sold'
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
if (
|
|
filterState.filters.status.length > 0 &&
|
|
!filterState.filters.status.includes(game.status)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
// 국가 필터
|
|
if (
|
|
filterState.filters.country.length > 0 &&
|
|
!filterState.filters.country.includes(game.country)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
// CERO 필터
|
|
if (filterState.filters.cero.length > 0 && !filterState.filters.cero.includes(game.cero)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
// 정렬 함수
|
|
function sortGames(games) {
|
|
switch (filterState.sortBy) {
|
|
case 'sortByDateDesc':
|
|
return [...games].sort((a, b) => {
|
|
const aDate = new Date(a.release.replace(/年|月/g, '/').replace(/日/g, '')).toUTCString();
|
|
const bDate = new Date(b.release.replace(/年|月/g, '/').replace(/日/g, '')).toUTCString();
|
|
const dateDiff = new Date(bDate) - new Date(aDate);
|
|
return dateDiff === 0 ? compareTitles(a, b) : dateDiff;
|
|
});
|
|
case 'sortByDate':
|
|
return [...games].sort((a, b) => {
|
|
const aDate = new Date(a.release.replace(/年|月/g, '/').replace(/日/g, '')).toUTCString();
|
|
const bDate = new Date(b.release.replace(/年|月/g, '/').replace(/日/g, '')).toUTCString();
|
|
const dateDiff = new Date(aDate) - new Date(bDate);
|
|
return dateDiff === 0 ? compareTitles(a, b) : dateDiff;
|
|
});
|
|
case 'sortByPriceDesc':
|
|
return [...games].sort((a, b) => {
|
|
const aPrice = getSuggestedPriceValue(a);
|
|
const bPrice = getSuggestedPriceValue(b);
|
|
if (aPrice === null && bPrice === null) return compareTitles(a, b);
|
|
if (aPrice === null) return 1;
|
|
if (bPrice === null) return -1;
|
|
const priceDiff = bPrice - aPrice;
|
|
return priceDiff === 0 ? compareTitles(a, b) : priceDiff;
|
|
});
|
|
case 'sortByPrice':
|
|
return [...games].sort((a, b) => {
|
|
const aPrice = getSuggestedPriceValue(a);
|
|
const bPrice = getSuggestedPriceValue(b);
|
|
if (aPrice === null && bPrice === null) return compareTitles(a, b);
|
|
if (aPrice === null) return 1;
|
|
if (bPrice === null) return -1;
|
|
const priceDiff = aPrice - bPrice;
|
|
return priceDiff === 0 ? compareTitles(a, b) : priceDiff;
|
|
});
|
|
case 'sortByRandom':
|
|
return [...games].sort(() => Math.random() - 0.5);
|
|
default:
|
|
return games;
|
|
}
|
|
}
|
|
|
|
function compareTitles(a, b) {
|
|
return a.formattedTitle.localeCompare(b.formattedTitle, filterState.language === 'ko' ? 'ko' : 'ja');
|
|
}
|
|
|
|
// 정렬 UI 초기화
|
|
function setupSortUI() {
|
|
const sortButton = document.getElementById('sort-button');
|
|
const sortOptions = document.getElementById('sort-options');
|
|
const sortLabel = document.getElementById('sort-label');
|
|
|
|
// 정렬 버튼 클릭 이벤트
|
|
sortButton.addEventListener('click', () => {
|
|
sortOptions.classList.toggle('hidden');
|
|
const isExpanded = sortOptions.classList.contains('hidden') ? 'false' : 'true';
|
|
sortButton.setAttribute('aria-expanded', isExpanded);
|
|
});
|
|
|
|
// 정렬 옵션 클릭 이벤트
|
|
sortOptions.querySelectorAll('li').forEach(option => {
|
|
option.addEventListener('click', () => {
|
|
const value = option.getAttribute('data-value');
|
|
const name = uiTexts[filterState.language].sortOptions[value];
|
|
|
|
// 선택된 옵션 업데이트
|
|
sortOptions.querySelectorAll('li').forEach(li => {
|
|
li.querySelector('span').classList.remove('font-semibold');
|
|
li.querySelector('svg')?.parentElement?.classList.add('hidden');
|
|
});
|
|
|
|
option.querySelector('span').classList.add('font-semibold');
|
|
const checkIcon = option.querySelector('svg')?.parentElement;
|
|
if (checkIcon) checkIcon.classList.remove('hidden');
|
|
|
|
// 정렬 상태 업데이트
|
|
filterState.sortBy = value;
|
|
sortButton.querySelector('span').textContent = name;
|
|
sortOptions.classList.add('hidden');
|
|
sortButton.setAttribute('aria-expanded', 'false');
|
|
|
|
// 게임 목록 다시 렌더링
|
|
renderGames();
|
|
});
|
|
});
|
|
|
|
// 외부 클릭 시 드롭다운 닫기
|
|
document.addEventListener('click', e => {
|
|
if (!sortButton.contains(e.target) && !sortOptions.contains(e.target)) {
|
|
sortOptions.classList.add('hidden');
|
|
sortButton.setAttribute('aria-expanded', 'false');
|
|
}
|
|
});
|
|
|
|
// 정렬 옵션 텍스트 업데이트
|
|
function updateSortOptionsText() {
|
|
const texts = uiTexts[filterState.language].sortOptions;
|
|
sortLabel.textContent = filterState.language === 'ko' ? '정렬' : '並び替え';
|
|
sortButton.querySelector('span').textContent = texts[filterState.sortBy];
|
|
|
|
sortOptions.querySelectorAll('li').forEach(option => {
|
|
const value = option.getAttribute('data-value');
|
|
option.querySelector('span').textContent = texts[value];
|
|
});
|
|
}
|
|
|
|
// 언어 변경 시 정렬 옵션 텍스트 업데이트
|
|
languageRadios.forEach(radio => {
|
|
radio.addEventListener('change', e => {
|
|
filterState.language = e.target.value;
|
|
updateSortOptionsText();
|
|
});
|
|
});
|
|
|
|
// 초기 정렬 옵션 텍스트 설정
|
|
updateSortOptionsText();
|
|
}
|
|
|
|
// 게임 렌더링 함수 수정
|
|
function renderGames() {
|
|
loading.classList.remove('hidden');
|
|
|
|
// 데이터 포맷팅
|
|
const formattedGames = NSW_DB.map(formatGameData);
|
|
|
|
// 필터링 적용
|
|
const filteredGames = filterGames(formattedGames);
|
|
|
|
// 정렬 적용
|
|
const sortedGames = sortGames(filteredGames);
|
|
|
|
// 게임 개수 업데이트
|
|
gameCount.textContent = `${sortedGames.length} ${uiTexts[filterState.language].count}`;
|
|
|
|
// 테이블 내용 업데이트
|
|
gameList.innerHTML = '';
|
|
sortedGames.forEach((game, index) => {
|
|
gameList.appendChild(createGameRow(game, index));
|
|
});
|
|
|
|
loading.classList.add('hidden');
|
|
}
|
|
|
|
// 초기화
|
|
setupFilterListeners();
|
|
setupSortUI();
|
|
updateUITexts();
|
|
renderGames();
|
|
});
|