v2026.05.19-01 가격 확인·제품 상태 필드 및 별 표시
실제 확인한 가격은 ⭐로 표시하고, 개봉/미개봉 상태를 목록·상세·DB에 반영했다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1066
db/nsw.resale.db.js
1066
db/nsw.resale.db.js
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,9 @@
|
||||
# 의사결정 이력
|
||||
|
||||
## 2026-05-19
|
||||
- 임시 추정가와 실제 확인가를 구분하기 위해 `sale.priceVerified` 불리언을 도입하고, 확인 완료 항목은 제목 앞 노란 별로 표시하도록 했다.
|
||||
- 패키지 개봉 여부는 판매 상태(`status`)와 분리해 `itemCondition`(SEALED/OPENED)으로 관리하고 목록·상세에 함께 노출하도록 했다.
|
||||
|
||||
## 2026-03-30
|
||||
- 게임 목록 서비스의 기준 화면을 `index.html` + `nsw-detail.html` 2개로 단순화했다.
|
||||
- 데이터 저장소를 별도 API 없이 정적 파일(`db/nsw.db.js`)로 유지하여 배포 복잡도를 낮췄다.
|
||||
|
||||
13
docs/map.md
13
docs/map.md
@@ -3,30 +3,31 @@
|
||||
## 메인 목록 화면 (`/index.html`)
|
||||
- 담당 파일: `index.html`
|
||||
- 연결 스크립트: `script/nsw.js`
|
||||
- 데이터 소스: `db/nsw.db.js`
|
||||
- 데이터 소스: `db/nsw.resale.db.js`
|
||||
- 주요 UI:
|
||||
- 언어 전환(한국어/일본어)
|
||||
- 검색 입력
|
||||
- 필터(언어 지원, 상태, 에디션, CERO)
|
||||
- 필터(언어 지원, 판매 상태, 에디션, CERO)
|
||||
- 정렬 드롭다운
|
||||
- 게임 목록 테이블
|
||||
- 게임 목록 테이블(가격 확인 별, 판매가, 판매 상태, 제품 상태, 지역)
|
||||
- 사용자 동작:
|
||||
- 목록 행 클릭 시 `nsw-detail.html?no={게임번호}`로 이동
|
||||
|
||||
## 상세 화면 (`/nsw-detail.html`)
|
||||
- 담당 파일: `nsw-detail.html`
|
||||
- 연결 스크립트: `script/nsw-detail.js`
|
||||
- 데이터 소스: `db/nsw.db.js`
|
||||
- 데이터 소스: `db/nsw.resale.db.js`
|
||||
- 주요 UI:
|
||||
- 상단 대표 이미지
|
||||
- 게임 기본 정보(용량, 플레이 모드, 메이커, 언어, 등급, 출시일)
|
||||
- 구매 정보(구매일, 구매처, 가격, 주문번호, 추가 콘텐츠)
|
||||
- 판매 정보(판매가, 가격 기준, 제품 상태, 가격 확인 여부, 기준일)
|
||||
- 사용자 동작:
|
||||
- URL의 `no` 값으로 게임을 조회
|
||||
- 해당 번호가 없으면 알림 후 목록 페이지로 리다이렉트
|
||||
|
||||
## 데이터 전용 파일
|
||||
- `db/nsw.db.js`: 닌텐도 스위치 게임 데이터 본문
|
||||
- `db/nsw.resale.db.js`: 닌텐도 스위치 중고 판매 목록·가격 데이터
|
||||
- `db/nsw.db.js`: 레거시 보유 목록 데이터
|
||||
- `db/nsw-sale.db.js`: 세일 관련 데이터(현재 화면 연결 없음)
|
||||
- `db/amiibo.db.js`: 아미보 관련 데이터(현재 화면 연결 없음)
|
||||
|
||||
|
||||
28
docs/spec.md
28
docs/spec.md
@@ -7,18 +7,18 @@
|
||||
|
||||
## 화면/라우팅
|
||||
- 목록 화면: `index.html`
|
||||
- 주요 기능: 검색, 필터(언어/상태/국가/CERO), 정렬, 게임 개수 표시
|
||||
- 데이터 소스: `db/nsw.db.js`
|
||||
- 주요 기능: 검색, 필터(언어/판매상태/국가/CERO), 정렬, 게임 개수 표시, 가격 확인 별·제품 상태 표시
|
||||
- 데이터 소스: `db/nsw.resale.db.js` (`export default` → `items` 배열)
|
||||
- 스크립트: `script/nsw.js`
|
||||
- 상세 화면: `nsw-detail.html`
|
||||
- 주요 기능: 게임 기본 정보/구매 정보 상세 표시
|
||||
- 주요 기능: 게임 기본 정보/판매 정보 상세 표시
|
||||
- 진입 방식: `nsw-detail.html?no={gameNo}`
|
||||
- 데이터 소스: `db/nsw.db.js`
|
||||
- 데이터 소스: `db/nsw.resale.db.js`
|
||||
- 스크립트: `script/nsw-detail.js`
|
||||
|
||||
## 데이터 구조
|
||||
- 파일: `db/nsw.db.js`
|
||||
- 형태: `export default []` 배열
|
||||
## 데이터 구조 (중고 판매 DB)
|
||||
- 파일: `db/nsw.resale.db.js`
|
||||
- 형태: `export const NSW_RESALE_DB = { metadata, items }`, `export default items`
|
||||
- 주요 필드:
|
||||
- `no` (number): 게임 고유 순번
|
||||
- `title` (string): 일본어/원문 타이틀
|
||||
@@ -38,15 +38,15 @@
|
||||
- `release` (string|null): 출시일(예: `2017年7月20日`)
|
||||
- `tags` (string|null): 장르/특성 태그
|
||||
- `extension` (string[]|null): 추가 콘텐츠 목록
|
||||
- `status` (string): `package` | `download` | `expansion` | `sold`
|
||||
- `status` (string): `available` | `sold` (판매 가능/판매완료)
|
||||
- `itemCondition` (string|null): `SEALED`(미개봉) | `OPENED`(개봉) | `null`(미정)
|
||||
- `country` (string): `KOR` | `JPN`
|
||||
- `cero` (string|null): CERO 등급
|
||||
- `purchaseInformation` (object|null):
|
||||
- `date` (string|null)
|
||||
- `store` (string|null)
|
||||
- `purchase` (number|null)
|
||||
- `monetary` (string|null)
|
||||
- `orderNumber` (string|null)
|
||||
- `sale` (object):
|
||||
- `suggestedPrice` (number): 추천 판매가(KRW)
|
||||
- `priceRange` (object): `{ min, max }`
|
||||
- `pricingBasis`, `confidence`, `checkedAt`, `memo` 등
|
||||
- `priceVerified` (boolean): 실제 시세 확인 완료 여부. `true`이면 목록·상세 제목 앞 노란 별 표시
|
||||
|
||||
## 필터/정렬 동작
|
||||
- 필터:
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
- 레거시 페이지/리소스(아미보, 세일 관련) 제거 후 문서와 실제 구조 정합성 점검이 필요하다.
|
||||
|
||||
## 다음 작업
|
||||
- `sale.priceVerified: true` 및 `itemCondition` 값을 타이틀별로 실제 데이터에 맞게 채운다.
|
||||
- 목록/상세 페이지의 경로 및 텍스트 다국어 키를 재검증한다.
|
||||
- 필터 조합(언어+상태+국가+CERO) 테스트 케이스를 작성한다.
|
||||
- 데이터 스키마 검증 스크립트를 추가해 누락 필드와 오탈자를 자동 확인한다.
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# 작업 이력
|
||||
|
||||
## v2026.05.19-01
|
||||
- `db/nsw.resale.db.js` 가격 확인 플래그 `sale.priceVerified` 추가
|
||||
- `db/nsw.resale.db.js` 제품 상태 `itemCondition`(SEALED/OPENED) enum·필드 추가
|
||||
- `script/nsw.js` 가격 확인 타이틀 노란 별 표시, 목록 제품 상태 컬럼 반영
|
||||
- `script/nsw-detail.js` 상세 화면 별 표시·제품 상태·가격 확인 문구 반영
|
||||
- `index.html` 테이블 제품 상태 열 헤더 반영
|
||||
|
||||
## v2026.03.30-04
|
||||
- 커밋 제목 규칙 추가: `vYYYY.MM.DD-번호 내용`
|
||||
- `docs/convention.md` 커밋 제목 형식 가이드 반영
|
||||
|
||||
@@ -470,8 +470,8 @@
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="hidden w-56 px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell">
|
||||
Role
|
||||
class="hidden w-28 px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell">
|
||||
제품 상태
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
|
||||
@@ -2,6 +2,9 @@ import NSW_DB from '../db/nsw.resale.db.js';
|
||||
|
||||
const SHOW_RECOMMENDED_PRICE_RANGE_BY_DEFAULT = false;
|
||||
|
||||
/** @type {string} 가격 확인 완료 표시 */
|
||||
const VERIFIED_PRICE_MARK = '⭐';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// URL에서 게임 번호 가져오기
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
@@ -33,10 +36,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
cero: '심의 등급',
|
||||
iarc: '심의 등급',
|
||||
releaseDate: '출시일',
|
||||
suggestedPrice: '추천 판매가',
|
||||
suggestedPrice: '판매가',
|
||||
priceRange: '판매가 범위',
|
||||
pricingBasis: '가격 참고 기준',
|
||||
checkedAt: '기준일',
|
||||
itemCondition: '제품 상태',
|
||||
priceVerified: '가격 확인',
|
||||
extension: '추가 콘텐츠',
|
||||
none: '없음',
|
||||
supported: '대응',
|
||||
@@ -61,6 +66,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
priceRange: '価格帯',
|
||||
pricingBasis: '価格参考基準',
|
||||
checkedAt: '確認日',
|
||||
itemCondition: '商品状態',
|
||||
priceVerified: '価格確認',
|
||||
extension: '追加コンテンツ',
|
||||
none: 'なし',
|
||||
supported: '対応',
|
||||
@@ -94,6 +101,30 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
return labels[language][pricingBasis] || '';
|
||||
}
|
||||
|
||||
function formatItemCondition(itemCondition) {
|
||||
if (!itemCondition) {
|
||||
return language === 'ko' ? '미정' : '未設定';
|
||||
}
|
||||
|
||||
const labels = {
|
||||
ko: {
|
||||
SEALED: '미개봉',
|
||||
OPENED: '개봉',
|
||||
},
|
||||
ja: {
|
||||
SEALED: '未開封',
|
||||
OPENED: '開封済',
|
||||
},
|
||||
};
|
||||
|
||||
return labels[language][itemCondition] || itemCondition;
|
||||
}
|
||||
|
||||
function formatPriceVerified(priceVerified) {
|
||||
if (!priceVerified) return '';
|
||||
return language === 'ko' ? '실제 시세 확인 완료' : '実勢価格確認済み';
|
||||
}
|
||||
|
||||
function convertLanguage(value) {
|
||||
if (!value || language !== 'ko') return value;
|
||||
|
||||
@@ -214,8 +245,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// 기본 정보 설정
|
||||
document.getElementById('gameImage').src = window.innerWidth < 640 ? game.thumbnail : game.image;
|
||||
document.getElementById('gameTitle').textContent =
|
||||
language === 'ko' ? game.koTitle || game.title : game.title;
|
||||
const displayTitle = language === 'ko' ? game.koTitle || game.title : game.title;
|
||||
const gameTitleEl = document.getElementById('gameTitle');
|
||||
gameTitleEl.className =
|
||||
'text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl flex items-center justify-center gap-2';
|
||||
gameTitleEl.innerHTML = `${game.sale?.priceVerified ? VERIFIED_PRICE_MARK : ''}<span>${game.no}. ${displayTitle}</span>`;
|
||||
document.getElementById('gameTags').textContent = convertTags(game.tags);
|
||||
document.getElementById('infoTitle').textContent = currentTexts.infoTitle;
|
||||
document.getElementById('purchaseTitle').textContent = currentTexts.purchaseTitle;
|
||||
@@ -294,6 +328,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
? [{ value: priceRange, label: currentTexts.priceRange }]
|
||||
: []),
|
||||
{ value: formatPricingBasis(game.sale.pricingBasis), label: currentTexts.pricingBasis },
|
||||
{ value: formatItemCondition(game.itemCondition), label: currentTexts.itemCondition },
|
||||
{ value: formatPriceVerified(game.sale.priceVerified), label: currentTexts.priceVerified },
|
||||
{ value: game.sale.checkedAt, label: currentTexts.checkedAt },
|
||||
];
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@ import NSW_DB from '../db/nsw.resale.db.js';
|
||||
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');
|
||||
@@ -24,6 +27,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
info: '판매가',
|
||||
status: '판매 상태',
|
||||
role: '가격 범위',
|
||||
itemCondition: '제품 상태',
|
||||
location: '지역',
|
||||
},
|
||||
sortOptions: {
|
||||
@@ -54,6 +58,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
info: '販売価格',
|
||||
status: '販売状態',
|
||||
role: '価格帯',
|
||||
itemCondition: '商品状態',
|
||||
location: '地域',
|
||||
},
|
||||
sortOptions: {
|
||||
@@ -104,10 +109,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
headers[0].textContent = texts.tableHeaders.title;
|
||||
headers[1].textContent = texts.tableHeaders.info;
|
||||
headers[2].textContent = texts.tableHeaders.status;
|
||||
headers[3].textContent = texts.tableHeaders.role;
|
||||
headers[3].textContent = SHOW_RECOMMENDED_PRICE_RANGE_BY_DEFAULT
|
||||
? texts.tableHeaders.role
|
||||
: texts.tableHeaders.itemCondition;
|
||||
headers[4].textContent = texts.tableHeaders.location;
|
||||
headers[3].classList.toggle('sm:table-cell', SHOW_RECOMMENDED_PRICE_RANGE_BY_DEFAULT);
|
||||
headers[3].classList.toggle('sm:hidden', !SHOW_RECOMMENDED_PRICE_RANGE_BY_DEFAULT);
|
||||
headers[3].classList.add('sm:table-cell');
|
||||
headers[3].classList.remove('sm:hidden');
|
||||
|
||||
// 로딩 텍스트 업데이트
|
||||
loading.textContent = texts.loading;
|
||||
@@ -597,9 +604,29 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
formattedPricingBasis: formatPricingBasis(game.sale?.pricingBasis),
|
||||
formattedConfidenceDescription: formatConfidenceDescription(game.sale?.confidence),
|
||||
formattedSaleStatus: formatSaleStatus(game.status),
|
||||
formattedItemCondition: formatItemCondition(game.itemCondition),
|
||||
};
|
||||
}
|
||||
|
||||
function formatItemCondition(itemCondition) {
|
||||
if (!itemCondition) {
|
||||
return filterState.language === 'ko' ? '미정' : '未設定';
|
||||
}
|
||||
|
||||
const labels = {
|
||||
ko: {
|
||||
SEALED: '미개봉',
|
||||
OPENED: '개봉',
|
||||
},
|
||||
ja: {
|
||||
SEALED: '未開封',
|
||||
OPENED: '開封済',
|
||||
},
|
||||
};
|
||||
|
||||
return labels[filterState.language][itemCondition] || itemCondition;
|
||||
}
|
||||
|
||||
function formatKRW(value) {
|
||||
if (typeof value !== 'number') return '';
|
||||
return `${value.toLocaleString('ko-KR')}원`;
|
||||
@@ -667,6 +694,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
}
|
||||
|
||||
function getItemConditionClass(itemCondition) {
|
||||
switch (itemCondition) {
|
||||
case 'SEALED':
|
||||
return 'bg-amber-100 text-amber-800';
|
||||
case 'OPENED':
|
||||
return 'bg-slate-100 text-slate-700';
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-500';
|
||||
}
|
||||
}
|
||||
|
||||
function getCountryClass(country) {
|
||||
return country === 'JPN'
|
||||
? 'text-red-600 hover:text-red-900'
|
||||
@@ -689,8 +727,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
<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">
|
||||
${game.no}. ${game.formattedTitle}
|
||||
<div class="font-medium leading-5 text-gray-900 flex items-center gap-1">
|
||||
${game.sale?.priceVerified ? VERIFIED_PRICE_MARK : ''}<span>${game.no}. ${game.formattedTitle}</span>
|
||||
</div>
|
||||
<div class="mt-1 max-w-lg text-xs leading-5 text-gray-500">
|
||||
${game.formattedConfidenceDescription}
|
||||
@@ -705,6 +743,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
)}">
|
||||
${game.formattedSaleStatus}
|
||||
</span>
|
||||
<span class="rounded-full px-2 text-xs font-semibold leading-5 ${getItemConditionClass(
|
||||
game.itemCondition,
|
||||
)}">
|
||||
${game.formattedItemCondition}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs leading-5 text-gray-500">
|
||||
${game.formattedPricingBasis}
|
||||
@@ -740,7 +783,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
<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 class="hidden w-28 px-3 py-4 text-sm text-gray-500 sm:table-cell">
|
||||
<div class="flex justify-center whitespace-nowrap rounded-full px-2 text-xs font-semibold leading-5 ${getItemConditionClass(
|
||||
game.itemCondition,
|
||||
)}">
|
||||
${game.formattedItemCondition}
|
||||
</div>
|
||||
</td>`
|
||||
}
|
||||
<td class="hidden py-4 pl-3 pr-4 text-right text-sm font-medium sm:table-cell">
|
||||
<div class="${getCountryClass(game.country)}">
|
||||
|
||||
Reference in New Issue
Block a user