Update used sale filters and guide

This commit is contained in:
2026-08-03 18:50:30 +09:00
parent 11ec940e48
commit ef0b48dc16
6 changed files with 215 additions and 44 deletions

View File

@@ -1357,11 +1357,11 @@ export const NSW_RESALE_DB = {
"price": 6578,
"sale": {
"currency": "KRW",
"basisPrice": 5200,
"basisPrice": 52000,
"suggestedPrice": 25000
},
"itemCondition": "",
"status": "available"
"status": "sold"
},
{
"no": 66,
@@ -1532,10 +1532,10 @@ export const NSW_RESALE_DB = {
"sale": {
"currency": "KRW",
"basisPrice": 5332,
"suggestedPrice": "none"
"suggestedPrice": 35000
},
"itemCondition": "",
"status": "available"
"status": "sold"
},
{
"no": 73,
@@ -1618,11 +1618,11 @@ export const NSW_RESALE_DB = {
"price": 7480,
"sale": {
"currency": "KRW",
"basisPrice": 2892,
"suggestedPrice": "none"
"basisPrice": 28920,
"suggestedPrice": 35000
},
"itemCondition": "",
"status": "available"
"status": "sold"
},
{
"no": 76,
@@ -4672,7 +4672,7 @@ export const NSW_RESALE_DB = {
{
"no": 200,
"title": "NieR:Automata The End of YoRHa Edition",
"koTitle": "NieR:Automata The End of YoRHa Edition",
"koTitle": "니어 오토마타 더 엔드 오브 요르하 에디션",
"image": "/images/nsw/0192_A.jpg",
"thumbnail": "/images/nsw/0192_B.jpg",
"platform": "Nintendo Switch",
@@ -4692,11 +4692,11 @@ export const NSW_RESALE_DB = {
"price": 5280,
"sale": {
"currency": "KRW",
"basisPrice": 4980,
"suggestedPrice": "none"
"basisPrice": 49800,
"suggestedPrice": 70000
},
"itemCondition": "",
"status": "available"
"status": "sold"
},
{
"no": 201,
@@ -5571,7 +5571,7 @@ export const NSW_RESALE_DB = {
{
"no": 233,
"title": "Fate/Samurai Remnant",
"koTitle": "Fate/Samurai Remnant",
"koTitle": "페이트 사무라이 렘런트",
"image": "/images/nsw/0233_A.jpg",
"thumbnail": "/images/nsw/0233_B.webp",
"platform": "Nintendo Switch",
@@ -5591,11 +5591,11 @@ export const NSW_RESALE_DB = {
"price": 9680,
"sale": {
"currency": "KRW",
"basisPrice": 7280,
"suggestedPrice": "none"
"basisPrice": 72800,
"suggestedPrice": 45000
},
"itemCondition": "",
"status": "available"
"status": "sold"
},
{
"no": 234,
@@ -6142,11 +6142,11 @@ export const NSW_RESALE_DB = {
"price": 8778,
"sale": {
"currency": "KRW",
"basisPrice": 6898.1,
"suggestedPrice": "none"
"basisPrice": 68980,
"suggestedPrice": 25000
},
"itemCondition": "",
"status": "available"
"status": "sold"
},
{
"no": 254,

74
docs/resale-data-guide.md Normal file
View File

@@ -0,0 +1,74 @@
# 중고 판매 DB 사용법
## 수정 위치
- 데이터 파일: `db/nsw.resale.db.js`
- 각 게임은 `items` 배열의 객체 하나입니다.
- 판매 상태는 `status`, 제품 상태/컨디션 메모는 `itemCondition`, 판매가는 `sale.suggestedPrice`를 수정합니다.
## 판매 상태(status)
| DB 값 | 한글 표시 | 일본어 표시 | 의미 | 목록 기본 표시 |
| --- | --- | --- | --- | --- |
| `available` | 판매 가능 | 販売可 | 아직 판매 가능한 항목 | 기본 표시 |
| `sold` | 판매완료 | 販売済み | 판매가 끝난 항목 | 손님 화면에서는 숨김 |
상세 화면에서는 `available`이 `판매중`, `sold`가 `판매완료`로 표시됩니다.
목록 첫 화면은 손님용 화면이라 판매 가능한 항목만 보여주고 `Sale Status` 필터는 숨깁니다. 관리자 확인이 필요할 때는 타이틀 오른쪽의 넓은 빈 공간을 10초 안에 5번 클릭하면 `Sale Status` 필터가 열립니다. 이때 `판매 가능`, `판매완료`를 직접 체크해서 볼 수 있습니다.
## 사이드바 기본값
| 필터 | 기본 체크 | 이유 |
| --- | --- | --- |
| Language | `한국어 지원`, `한국어 미지원` 모두 체크 | 두 항목을 모두 보여주는 기본값 |
| Sale Status | 손님 화면에서는 숨김 | 기본적으로 판매 가능한 제품만 노출 |
| Edition | `Korea Edition`, `Japan Edition` 모두 체크 | 두 판본을 모두 보여주는 기본값 |
| CERO | `A`, `B`, `C`, `D`, `Z`, `교육・데이터베이스` 모두 체크 | DB에 있는 전체 등급을 보여주는 기본값 |
표 아래의 `판매가 합계`는 현재 필터링되어 화면에 보이는 항목 중 `sale.suggestedPrice`가 숫자인 항목만 더합니다. `"none"`인 항목은 0원으로 계산합니다.
## 제품 상태/컨디션(itemCondition)
| DB 값 | 한글 표시 | 일본어 표시 | 의미 |
| --- | --- | --- | --- |
| `""` | 표시 안 함 | 표시 안 함 | 별도 상태 메모 없음 |
| `SEALED` | 미개봉 | 未開封 | 미개봉 패키지 |
| `OPENED` | 개봉 | 開封済 | 개봉한 패키지 |
| 자유 입력 예: `미개봉 새제품` | 입력한 문구 그대로 | 입력한 문구 그대로 | 더 구체적인 상태 메모 |
현재 DB는 `itemCondition`을 자유 입력 메모로도 사용할 수 있습니다. 짧고 일관된 표기가 필요하면 `SEALED`, `OPENED`를 쓰고, 특이사항이 있으면 `미개봉 새제품`, `초회 한정판`, `케이스 흠집 있음`처럼 직접 적습니다.
## 판매가(sale.suggestedPrice)
| DB 값 | 화면 표시 | 의미 |
| --- | --- | --- |
| `"none"` | `-` | 아직 판매가 미입력 |
| 숫자 예: `70000` | `70,000원` | 판매 등록에 사용할 원화 가격 |
숫자를 넣을 때는 따옴표 없이 입력합니다.
## 예시
```js
{
"no": 200,
"koTitle": "니어 오토마타 더 엔드 오브 요르하 에디션",
"sale": {
"currency": "KRW",
"basisPrice": 70000,
"suggestedPrice": "none"
},
"itemCondition": "",
"status": "sold"
}
```
위 예시는 판매완료 항목입니다. 목록 첫 화면에서는 손님용 기본값 때문에 숨겨지고, 관리자용 `Sale Status` 필터를 연 뒤 `판매완료`를 선택하면 보입니다.
## 변경 후 확인
1. `status`를 `sold`로 바꾸면 판매완료 처리됩니다.
2. 목록 기본 화면에서 안 보이는 것은 정상입니다. 손님용 기본값은 판매 가능한 항목만 표시합니다.
3. 타이틀 오른쪽 빈 공간을 10초 안에 5번 클릭해 `Sale Status` 필터를 연 뒤 `판매완료`를 선택해 항목이 나오는지 확인합니다.
4. 상세 주소 `nsw-detail.html?no=게임번호`로 들어가 `판매완료` 배지가 보이는지 확인합니다.

View File

@@ -9,8 +9,16 @@
<div class="bg-white">
<div>
<main class="mx-auto w-full px-4 sm:px-6 lg:px-8">
<div class="flex items-baseline justify-between border-b border-gray-200 pt-8 pb-6">
<a href="./index.html"><h1 class="text-4xl font-bold tracking-tight text-gray-900">Title</h1></a>
<div class="flex min-h-24 items-baseline justify-between border-b border-gray-200 pt-8 pb-6">
<div class="flex flex-1 items-stretch">
<a href="./index.html"><h1 class="text-4xl font-bold tracking-tight text-gray-900">Title</h1></a>
<button
type="button"
id="admin-filter-unlock"
class="ml-4 min-h-12 flex-1 cursor-default opacity-0"
tabindex="-1"
aria-hidden="true"></button>
</div>
<div class="flex items-center"></div>
</div>
@@ -179,6 +187,7 @@
name="language-filter"
type="checkbox"
value="koreanSupport"
checked
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600" />
<label
for="korean-support"
@@ -192,6 +201,7 @@
name="language-filter"
type="checkbox"
value="koreanNotSupport"
checked
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600" />
<label
for="korean-not-support"
@@ -204,7 +214,7 @@
</div>
<!-- 판매 상태 필터 -->
<div class="space-y-2">
<div id="saleStatusFilter" class="hidden space-y-2">
<h3 class="text-base font-medium text-gray-900">Sale Status</h3>
<div class="space-y-2">
<div class="flex items-center">
@@ -213,6 +223,7 @@
name="status-filter"
type="checkbox"
value="available"
checked
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600" />
<label for="available" class="ml-2 text-sm text-gray-900">
판매 가능
@@ -240,6 +251,7 @@
name="country-filter"
type="checkbox"
value="KOR"
checked
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600" />
<label for="kor" class="ml-2 text-sm text-gray-900">
Korea Edition
@@ -251,6 +263,7 @@
name="country-filter"
type="checkbox"
value="JPN"
checked
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600" />
<label for="jpn" class="ml-2 text-sm text-gray-900">
Japan Edition
@@ -269,6 +282,7 @@
name="cero-filter"
type="checkbox"
value="A"
checked
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600" />
<label for="cero-a" class="ml-2 text-sm text-gray-900">A</label>
</div>
@@ -278,6 +292,7 @@
name="cero-filter"
type="checkbox"
value="B"
checked
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600" />
<label for="cero-b" class="ml-2 text-sm text-gray-900">B</label>
</div>
@@ -287,6 +302,7 @@
name="cero-filter"
type="checkbox"
value="C"
checked
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600" />
<label for="cero-c" class="ml-2 text-sm text-gray-900">C</label>
</div>
@@ -296,9 +312,32 @@
name="cero-filter"
type="checkbox"
value="D"
checked
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600" />
<label for="cero-d" class="ml-2 text-sm text-gray-900">D</label>
</div>
<div class="flex items-center">
<input
id="cero-z"
name="cero-filter"
type="checkbox"
value="Z"
checked
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600" />
<label for="cero-z" class="ml-2 text-sm text-gray-900">Z</label>
</div>
<div class="flex items-center">
<input
id="cero-education"
name="cero-filter"
type="checkbox"
value="教育・データベース"
checked
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600" />
<label for="cero-education" class="ml-2 text-sm text-gray-900">
교육・데이터베이스
</label>
</div>
</div>
</div>
</div>
@@ -481,6 +520,21 @@
<tbody
id="gameList"
class="divide-y divide-gray-200 bg-white"></tbody>
<tfoot class="bg-gray-50">
<tr>
<td
class="py-3 pl-4 pr-3 text-sm font-semibold text-gray-900">
<span>판매가 합계</span>
<span id="saleTotalMobile" class="ml-2 sm:hidden">0원</span>
</td>
<td
id="saleTotal"
class="hidden w-56 px-3 py-3 text-sm font-semibold text-gray-900 sm:table-cell">
0원
</td>
<td class="hidden w-28 px-3 py-3 sm:table-cell"></td>
</tr>
</tfoot>
</table>
<div id="loading" class="hidden">Loading...</div>
</div>
@@ -495,6 +549,6 @@
</main>
</div>
</div>
<script type="module" src="./script/nsw.js?v=20260521-badges-v2"></script>
<script type="module" src="./script/nsw.js?v=20260803-status-v4"></script>
</body>
</html>

View File

@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>게임 상세 정보</title>
<link href="./style/tailwind.css" rel="stylesheet" />
<script type="module" src="script/nsw-detail.js"></script>
<script type="module" src="script/nsw-detail.js?v=20260803-status-v4"></script>
</head>
<body class="bg-white">
<div aria-hidden="true" class="relative">

View File

@@ -1,4 +1,4 @@
import NSW_DB from '../db/nsw.resale.db.js';
import NSW_DB from '../db/nsw.resale.db.js?v=20260803-status-v4';
const SHOW_RECOMMENDED_PRICE_RANGE_BY_DEFAULT = false;

View File

@@ -1,9 +1,14 @@
import NSW_DB from '../db/nsw.resale.db.js?v=20260521-badges-v2';
import NSW_DB from '../db/nsw.resale.db.js?v=20260803-status-v4';
window.NSW_APP_VERSION = '20260521-badges-v2';
window.NSW_APP_VERSION = '20260803-status-v4';
const SHOW_SOLD_BY_DEFAULT = false;
const SHOW_RECOMMENDED_PRICE_RANGE_BY_DEFAULT = false;
const DEFAULT_FILTERS = {
language: ['koreanSupport', 'koreanNotSupport'],
status: ['available'],
country: ['KOR', 'JPN'],
cero: ['A', 'B', 'C', 'D', 'Z', '教育・データベース'],
};
/** @type {string} 가격 확인 완료 표시 */
const VERIFIED_PRICE_MARK = '⭐';
@@ -11,8 +16,12 @@ const VERIFIED_PRICE_MARK = '⭐';
document.addEventListener('DOMContentLoaded', () => {
const gameList = document.getElementById('gameList');
const gameCount = document.getElementById('gameCount');
const saleTotal = document.getElementById('saleTotal');
const saleTotalMobile = document.getElementById('saleTotalMobile');
const loading = document.getElementById('loading');
const languageRadios = document.querySelectorAll('input[name="language"]');
const saleStatusFilter = document.getElementById('saleStatusFilter');
let isAdminFilterUnlocked = false;
// 언어별 UI 텍스트
const uiTexts = {
@@ -23,7 +32,7 @@ document.addEventListener('DOMContentLoaded', () => {
korean: '한국어',
japanese: '일본어',
count: '개수',
listIntro: '우선 이런 게임들이 있습니다. 가격은 순차적으로 작성중입니다.',
listIntro: '',
pricePending: '-',
loading: '로딩중...',
tableHeaders: {
@@ -51,7 +60,7 @@ document.addEventListener('DOMContentLoaded', () => {
korean: '韓国語',
japanese: '日本語',
count: '件数',
listIntro: 'まずは所持タイトルを掲載しています。価格は順次入力中です。',
listIntro: '',
pricePending: '-',
loading: '読み込み中...',
tableHeaders: {
@@ -78,10 +87,10 @@ document.addEventListener('DOMContentLoaded', () => {
const filterState = {
language: 'ko',
filters: {
language: [],
status: [],
country: [],
cero: [],
language: [...DEFAULT_FILTERS.language],
status: [...DEFAULT_FILTERS.status],
country: [...DEFAULT_FILTERS.country],
cero: [...DEFAULT_FILTERS.cero],
},
searchText: '',
sortBy: 'sortByRandom',
@@ -836,12 +845,14 @@ document.addEventListener('DOMContentLoaded', () => {
checkbox.checked = false;
});
applyDefaultFilterCheckboxes();
// 필터 상태 초기화
filterState.filters = {
language: [],
status: [],
country: [],
cero: [],
language: [...DEFAULT_FILTERS.language],
status: [...DEFAULT_FILTERS.status],
country: [...DEFAULT_FILTERS.country],
cero: [...DEFAULT_FILTERS.cero],
};
filterState.searchText = '';
document.querySelectorAll('[data-search-input]').forEach(input => {
@@ -855,6 +866,33 @@ document.addEventListener('DOMContentLoaded', () => {
// 필터 초기화 버튼 이벤트 리스너 설정
document.getElementById('resetFilters')?.addEventListener('click', resetFilters);
function applyDefaultFilterCheckboxes() {
Object.entries(DEFAULT_FILTERS).forEach(([type, values]) => {
document.querySelectorAll(`input[name="${type}-filter"]`).forEach(checkbox => {
checkbox.checked = values.includes(checkbox.value);
});
});
}
function setupAdminFilterUnlock() {
const unlockTarget = document.getElementById('admin-filter-unlock');
if (!unlockTarget || !saleStatusFilter) return;
let clickCount = 0;
let lastClickAt = 0;
unlockTarget.addEventListener('click', () => {
const now = Date.now();
clickCount = now - lastClickAt > 10000 ? 1 : clickCount + 1;
lastClickAt = now;
if (clickCount < 5) return;
isAdminFilterUnlocked = true;
saleStatusFilter.classList.remove('hidden');
clickCount = 0;
});
}
// 필터 체크박스 이벤트 리스너 설정
function setupFilterListeners() {
const searchInputs = [
@@ -1066,17 +1104,13 @@ document.addEventListener('DOMContentLoaded', () => {
}
if (
!SHOW_SOLD_BY_DEFAULT &&
filterState.filters.status.length === 0 &&
game.status === 'sold'
filterState.filters.status.length > 0 &&
!filterState.filters.status.includes(game.status)
) {
return false;
}
if (
filterState.filters.status.length > 0 &&
!filterState.filters.status.includes(game.status)
) {
if (!isAdminFilterUnlocked && game.status !== 'available') {
return false;
}
@@ -1232,6 +1266,13 @@ document.addEventListener('DOMContentLoaded', () => {
// 게임 개수 업데이트
gameCount.textContent = `${sortedGames.length} ${uiTexts[filterState.language].count}`;
const totalSuggestedPrice = sortedGames.reduce((sum, game) => {
const suggestedPrice = getSuggestedPriceValue(game);
return sum + (suggestedPrice || 0);
}, 0);
const formattedTotal = formatKRW(totalSuggestedPrice);
if (saleTotal) saleTotal.textContent = formattedTotal;
if (saleTotalMobile) saleTotalMobile.textContent = formattedTotal;
// 테이블 내용 업데이트
gameList.innerHTML = '';
@@ -1243,6 +1284,8 @@ document.addEventListener('DOMContentLoaded', () => {
}
// 초기화
applyDefaultFilterCheckboxes();
setupAdminFilterUnlock();
setupFilterListeners();
setupSortUI();
updateUITexts();