Compare commits

14 Commits

Author SHA1 Message Date
11ec940e48 Refine resale sorting and cache version 2026-05-21 12:32:42 +09:00
707da3b402 Refine resale list badges 2026-05-21 11:42:59 +09:00
a69b655995 Update resale pricing data 2026-05-21 11:22:20 +09:00
e0318cbf91 Ignore local VS Code settings 2026-05-19 18:56:17 +09:00
2b93c8db72 Bust resale DB module cache 2026-05-19 18:53:11 +09:00
908fd1035c Fix mobile search and update resale prices 2026-05-19 18:50:37 +09:00
37926e9d7f v2026.05.19-02 가격 확인 별과 순번 사이 공백
 표시 뒤에 한 칸 띄워 목록·상세 제목 가독성을 맞췄다.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 15:22:52 +09:00
9447623ece v2026.05.19-01 가격 확인·제품 상태 필드 및 별 표시
실제 확인한 가격은 로 표시하고, 개봉/미개봉 상태를 목록·상세·DB에 반영했다.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 15:21:13 +09:00
44426a633d Hide recommended price range by default 2026-05-19 11:48:42 +09:00
b15ef36f62 Regenerate Tailwind responsive utilities 2026-05-19 11:30:15 +09:00
8bdbe73900 Fix thumbnail sizing in sale rows 2026-05-19 11:26:56 +09:00
d63e3db986 Restrict compact sale rows to mobile 2026-05-19 11:25:44 +09:00
fd8c52dbce Improve mobile used sale rows 2026-05-19 11:21:06 +09:00
7904b6b89c Convert Switch DB for used sales 2026-05-19 11:11:39 +09:00
15 changed files with 8219 additions and 21297 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
node_modules/ node_modules/
.vscode/

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,9 @@
# 의사결정 이력 # 의사결정 이력
## 2026-05-19
- 임시 추정가와 실제 확인가를 구분하기 위해 `sale.priceVerified` 불리언을 도입하고, 확인 완료 항목은 제목 앞 노란 별로 표시하도록 했다.
- 패키지 개봉 여부는 판매 상태(`status`)와 분리해 `itemCondition`(SEALED/OPENED)으로 관리하고 목록·상세에 함께 노출하도록 했다.
## 2026-03-30 ## 2026-03-30
- 게임 목록 서비스의 기준 화면을 `index.html` + `nsw-detail.html` 2개로 단순화했다. - 게임 목록 서비스의 기준 화면을 `index.html` + `nsw-detail.html` 2개로 단순화했다.
- 데이터 저장소를 별도 API 없이 정적 파일(`db/nsw.db.js`)로 유지하여 배포 복잡도를 낮췄다. - 데이터 저장소를 별도 API 없이 정적 파일(`db/nsw.db.js`)로 유지하여 배포 복잡도를 낮췄다.

View File

@@ -3,30 +3,31 @@
## 메인 목록 화면 (`/index.html`) ## 메인 목록 화면 (`/index.html`)
- 담당 파일: `index.html` - 담당 파일: `index.html`
- 연결 스크립트: `script/nsw.js` - 연결 스크립트: `script/nsw.js`
- 데이터 소스: `db/nsw.db.js` - 데이터 소스: `db/nsw.resale.db.js`
- 주요 UI: - 주요 UI:
- 언어 전환(한국어/일본어) - 언어 전환(한국어/일본어)
- 검색 입력 - 검색 입력
- 필터(언어 지원, 상태, 에디션, CERO) - 필터(언어 지원, 판매 상태, 에디션, CERO)
- 정렬 드롭다운 - 정렬 드롭다운
- 게임 목록 테이블 - 게임 목록 테이블(가격 확인 별, 판매가, 판매 상태, 제품 상태, 지역)
- 사용자 동작: - 사용자 동작:
- 목록 행 클릭 시 `nsw-detail.html?no={게임번호}`로 이동 - 목록 행 클릭 시 `nsw-detail.html?no={게임번호}`로 이동
## 상세 화면 (`/nsw-detail.html`) ## 상세 화면 (`/nsw-detail.html`)
- 담당 파일: `nsw-detail.html` - 담당 파일: `nsw-detail.html`
- 연결 스크립트: `script/nsw-detail.js` - 연결 스크립트: `script/nsw-detail.js`
- 데이터 소스: `db/nsw.db.js` - 데이터 소스: `db/nsw.resale.db.js`
- 주요 UI: - 주요 UI:
- 상단 대표 이미지 - 상단 대표 이미지
- 게임 기본 정보(용량, 플레이 모드, 메이커, 언어, 등급, 출시일) - 게임 기본 정보(용량, 플레이 모드, 메이커, 언어, 등급, 출시일)
- 매 정보(구매일, 구매처, 가격, 주문번호, 추가 콘텐츠) - 매 정보(판매가, 가격 기준, 제품 상태, 가격 확인 여부, 기준일)
- 사용자 동작: - 사용자 동작:
- URL의 `no` 값으로 게임을 조회 - URL의 `no` 값으로 게임을 조회
- 해당 번호가 없으면 알림 후 목록 페이지로 리다이렉트 - 해당 번호가 없으면 알림 후 목록 페이지로 리다이렉트
## 데이터 전용 파일 ## 데이터 전용 파일
- `db/nsw.db.js`: 닌텐도 스위치 게임 데이터 본문 - `db/nsw.resale.db.js`: 닌텐도 스위치 중고 판매 목록·가격 데이터
- `db/nsw.db.js`: 레거시 보유 목록 데이터
- `db/nsw-sale.db.js`: 세일 관련 데이터(현재 화면 연결 없음) - `db/nsw-sale.db.js`: 세일 관련 데이터(현재 화면 연결 없음)
- `db/amiibo.db.js`: 아미보 관련 데이터(현재 화면 연결 없음) - `db/amiibo.db.js`: 아미보 관련 데이터(현재 화면 연결 없음)

View File

@@ -7,18 +7,18 @@
## 화면/라우팅 ## 화면/라우팅
- 목록 화면: `index.html` - 목록 화면: `index.html`
- 주요 기능: 검색, 필터(언어/상태/국가/CERO), 정렬, 게임 개수 표시 - 주요 기능: 검색, 필터(언어/판매상태/국가/CERO), 정렬, 게임 개수 표시, 판매 정보 배지 표시
- 데이터 소스: `db/nsw.db.js` - 데이터 소스: `db/nsw.resale.db.js` (`export default``items` 배열)
- 스크립트: `script/nsw.js` - 스크립트: `script/nsw.js`
- 상세 화면: `nsw-detail.html` - 상세 화면: `nsw-detail.html`
- 주요 기능: 게임 기본 정보/매 정보 상세 표시 - 주요 기능: 게임 기본 정보/매 정보 상세 표시
- 진입 방식: `nsw-detail.html?no={gameNo}` - 진입 방식: `nsw-detail.html?no={gameNo}`
- 데이터 소스: `db/nsw.db.js` - 데이터 소스: `db/nsw.resale.db.js`
- 스크립트: `script/nsw-detail.js` - 스크립트: `script/nsw-detail.js`
## 데이터 구조 ## 데이터 구조 (중고 판매 DB)
- 파일: `db/nsw.db.js` - 파일: `db/nsw.resale.db.js`
- 형태: `export default []` 배열 - 형태: `export const NSW_RESALE_DB = { metadata, items }`, `export default items`
- 주요 필드: - 주요 필드:
- `no` (number): 게임 고유 순번 - `no` (number): 게임 고유 순번
- `title` (string): 일본어/원문 타이틀 - `title` (string): 일본어/원문 타이틀
@@ -38,15 +38,13 @@
- `release` (string|null): 출시일(예: `2017年7月20日`) - `release` (string|null): 출시일(예: `2017年7月20日`)
- `tags` (string|null): 장르/특성 태그 - `tags` (string|null): 장르/특성 태그
- `extension` (string[]|null): 추가 콘텐츠 목록 - `extension` (string[]|null): 추가 콘텐츠 목록
- `status` (string): `package` | `download` | `expansion` | `sold` - `status` (string): `available` | `sold` (판매 가능/판매완료)
- `itemCondition` (string|null): 판매자가 직접 작성하는 제품 상태. 예: `미개봉 새제품`, `초회 한정판`
- `country` (string): `KOR` | `JPN` - `country` (string): `KOR` | `JPN`
- `cero` (string|null): CERO 등급 - `cero` (string|null): CERO 등급
- `purchaseInformation` (object|null): - `sale` (object):
- `date` (string|null) - `currency` (string): `KRW`
- `store` (string|null) - `suggestedPrice` (`number` | `"none"`): 수기 입력된 판매가(KRW). 미입력 항목은 `"none"`.
- `purchase` (number|null)
- `monetary` (string|null)
- `orderNumber` (string|null)
## 필터/정렬 동작 ## 필터/정렬 동작
- 필터: - 필터:

View File

@@ -4,6 +4,8 @@
- 레거시 페이지/리소스(아미보, 세일 관련) 제거 후 문서와 실제 구조 정합성 점검이 필요하다. - 레거시 페이지/리소스(아미보, 세일 관련) 제거 후 문서와 실제 구조 정합성 점검이 필요하다.
## 다음 작업 ## 다음 작업
- `sale.suggestedPrice`를 타이틀별로 수기 확인해 숫자 가격으로 순차 입력한다.
- `itemCondition` 값을 타이틀별로 실제 데이터에 맞게 채운다.
- 목록/상세 페이지의 경로 및 텍스트 다국어 키를 재검증한다. - 목록/상세 페이지의 경로 및 텍스트 다국어 키를 재검증한다.
- 필터 조합(언어+상태+국가+CERO) 테스트 케이스를 작성한다. - 필터 조합(언어+상태+국가+CERO) 테스트 케이스를 작성한다.
- 데이터 스키마 검증 스크립트를 추가해 누락 필드와 오탈자를 자동 확인한다. - 데이터 스키마 검증 스크립트를 추가해 누락 필드와 오탈자를 자동 확인한다.

View File

@@ -1,5 +1,20 @@
# 작업 이력 # 작업 이력
## v2026.05.21-01
- `db/nsw.resale.db.js` 기존 추정 판매가·가격 근거 필드를 제거하고 `sale.suggestedPrice: "none"` 상태로 초기화
- `script/nsw.js`, `script/nsw-detail.js` 가격 미입력 항목에 “가격은 순차적으로 작성중입니다.” 안내 표시
- 가격 정렬 시 숫자 가격이 있는 항목만 우선 정렬하고 미입력 항목은 뒤로 배치
## v2026.05.19-02
- `script/nsw.js`, `script/nsw-detail.js` 가격 확인 별(⭐)과 순번 사이 공백 추가
## 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 ## v2026.03.30-04
- 커밋 제목 규칙 추가: `vYYYY.MM.DD-번호 내용` - 커밋 제목 규칙 추가: `vYYYY.MM.DD-번호 내용`
- `docs/convention.md` 커밋 제목 형식 가이드 반영 - `docs/convention.md` 커밋 제목 형식 가이드 반영

View File

@@ -15,6 +15,46 @@
</div> </div>
<section aria-labelledby="products-heading" class="pt-6 pb-12"> <section aria-labelledby="products-heading" class="pt-6 pb-12">
<div id="mobileSearchForm" class="mb-4 lg:hidden">
<label
for="mobile-search-input"
class="block text-sm font-medium leading-6 text-gray-900">
직접 검색
</label>
<div class="relative mt-2 flex rounded-md shadow-sm">
<span
class="inline-flex items-center rounded-l-md border border-r-0 border-gray-300 px-3 text-gray-500 sm:text-sm">
게임명
</span>
<input
type="text"
id="mobile-search-input"
data-search-input
enterkeyhint="search"
autocomplete="off"
oninput="window.updateNswSearch && window.updateNswSearch(this.value)"
onchange="window.updateNswSearch && window.updateNswSearch(this.value)"
class="block w-full min-w-0 flex-1 rounded-none rounded-r-md px-2 py-2 pr-9 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
placeholder=" " />
<button
type="button"
class="absolute inset-y-0 right-0 z-10 flex items-center pr-3"
id="mobile-reset-search"
aria-label="검색어 지우기"
onclick="window.clearNswSearch && window.clearNswSearch()">
<svg
class="h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="currentColor">
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z"
clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
<div class="grid grid-cols-1 gap-x-8 gap-y-2 lg:grid-cols-4"> <div class="grid grid-cols-1 gap-x-8 gap-y-2 lg:grid-cols-4">
<!-- 필터 섹션 --> <!-- 필터 섹션 -->
<div class="lg:block"> <div class="lg:block">
@@ -78,7 +118,7 @@
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="w-full"> <div class="w-full">
<form id="searchForm" class="mb-4"> <form id="searchForm" class="mb-4" onsubmit="return false">
<label <label
for="search-input" for="search-input"
class="block text-sm font-medium leading-6 text-gray-900"> class="block text-sm font-medium leading-6 text-gray-900">
@@ -91,13 +131,18 @@
</span> </span>
<input <input
type="text" type="text"
name="search-input"
id="search-input" id="search-input"
class="block w-full px-2 min-w-0 flex-1 rounded-none rounded-r-md py-1.5 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" data-search-input
enterkeyhint="search"
autocomplete="off"
oninput="window.updateNswSearch && window.updateNswSearch(this.value)"
onchange="window.updateNswSearch && window.updateNswSearch(this.value)"
class="block w-full px-2 min-w-0 flex-1 rounded-none rounded-r-md py-1.5 pr-9 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
placeholder=" " /> placeholder=" " />
<div <div
class="cursor-pointer absolute inset-y-0 right-0 flex items-center pr-3" class="cursor-pointer absolute inset-y-0 right-0 z-10 flex items-center pr-3"
id="reset-search"> id="reset-search"
onclick="window.clearNswSearch && window.clearNswSearch()">
<svg <svg
class="h-5 w-5 text-gray-400" class="h-5 w-5 text-gray-400"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -158,39 +203,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 +225,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>
@@ -283,11 +308,14 @@
<div class="lg:col-span-3"> <div class="lg:col-span-3">
<div class="sm:px-6 lg:px-8"> <div class="sm:px-6 lg:px-8">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex items-center justify-between sm:flex-auto sm:-mx-6 lg:-mx-4"> <div class="flex flex-wrap items-center justify-between gap-3 sm:flex-auto sm:-mx-6 lg:-mx-4">
<p class="mt-2 sm:mt-0 text-sm text-gray-700"> <p class="mt-2 sm:mt-0 text-sm text-gray-700">
개수: 개수:
<span id="gameCount">0</span> <span id="gameCount">0</span>
</p> </p>
<p id="priceProgressNotice" class="mt-2 max-w-md text-sm text-gray-500 sm:mt-0">
우선 이런 게임들이 있습니다. 가격은 순차적으로 작성중입니다.
</p>
<div class="sort"> <div class="sort">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<label id="sort-label" class="block text-sm/6 font-medium text-gray-900"> <label id="sort-label" class="block text-sm/6 font-medium text-gray-900">
@@ -300,7 +328,7 @@
id="sort-button" id="sort-button"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true"> aria-haspopup="true">
<span class="col-start-1 row-start-1 truncate pr-6">순번 최신순</span> <span class="col-start-1 row-start-1 truncate pr-6">무작위</span>
<svg <svg
class="w-4 h-4 transform transition-transform duration-200" class="w-4 h-4 transform transition-transform duration-200"
fill="none" fill="none"
@@ -320,46 +348,6 @@
tabindex="-1" tabindex="-1"
role="listbox" role="listbox"
aria-labelledby="sort-label"> aria-labelledby="sort-label">
<li
class="relative cursor-pointer hover:bg-gray-100 select-none py-2 pl-3 pr-9 text-gray-900"
role="option"
data-value="sortByNoDesc">
<span class="block truncate font-semibold">순번 최신순</span>
<span
class="absolute inset-y-0 right-0 flex items-center pr-4 text-indigo-600">
<svg
class="size-5"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
data-slot="icon">
<path
fill-rule="evenodd"
d="M16.704 4.153a.75.75 0 0 1 .143 1.052l-8 10.5a.75.75 0 0 1-1.127.075l-4.5-4.5a.75.75 0 0 1 1.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 0 1 1.05-.143Z"
clip-rule="evenodd" />
</svg>
</span>
</li>
<li
class="relative cursor-pointer hover:bg-gray-100 select-none py-2 pl-3 pr-9 text-gray-900"
role="option"
data-value="sortByNo">
<span class="block truncate">순번 과거순</span>
<span
class="absolute inset-y-0 right-0 flex items-center pr-4 text-indigo-600 hidden">
<svg
class="size-5"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
data-slot="icon">
<path
fill-rule="evenodd"
d="M16.704 4.153a.75.75 0 0 1 .143 1.052l-8 10.5a.75.75 0 0 1-1.127.075l-4.5-4.5a.75.75 0 0 1 1.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 0 1 1.05-.143Z"
clip-rule="evenodd" />
</svg>
</span>
</li>
<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"
@@ -403,8 +391,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 +411,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
@@ -444,9 +432,9 @@
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="sortByRandom"> data-value="sortByRandom">
<span class="block truncate">무작위</span> <span class="block truncate font-semibold">무작위</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">
<svg <svg
class="size-5" class="size-5"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -480,24 +468,14 @@
</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 sm: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="hidden w-28 px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell">
Status Status
</th> </th>
<th
scope="col"
class="hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell">
Role
</th>
<th
scope="col"
class="hidden py-3.5 pl-3 pr-4 text-left text-sm font-semibold text-gray-900 sm:table-cell">
Location
</th>
</tr> </tr>
</thead> </thead>
<tbody <tbody
@@ -517,6 +495,6 @@
</main> </main>
</div> </div>
</div> </div>
<script type="module" src="./script/nsw.js"></script> <script type="module" src="./script/nsw.js?v=20260521-badges-v2"></script>
</body> </body>
</html> </html>

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,9 @@
import NSW_DB from '../db/nsw.db.js'; 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', () => { document.addEventListener('DOMContentLoaded', () => {
// URL에서 게임 번호 가져오기 // URL에서 게임 번호 가져오기
@@ -18,7 +23,7 @@ document.addEventListener('DOMContentLoaded', () => {
const texts = { const texts = {
ko: { ko: {
infoTitle: '게임 정보', infoTitle: '게임 정보',
purchaseTitle: '매 정보', purchaseTitle: '매 정보',
requiredCapacity: '필요한 용량', requiredCapacity: '필요한 용량',
playMode: '플레이 모드', playMode: '플레이 모드',
playUser: '플레이 인원', playUser: '플레이 인원',
@@ -31,10 +36,13 @@ document.addEventListener('DOMContentLoaded', () => {
cero: '심의 등급', cero: '심의 등급',
iarc: '심의 등급', iarc: '심의 등급',
releaseDate: '출시일', releaseDate: '출시일',
purchaseDate: '구매일', suggestedPrice: '판매가',
store: '구매처', pricePending: '-',
price: '구매 가격', priceRange: '판매가 범위',
orderNumber: '주문번호', pricingBasis: '가격 참고 기준',
checkedAt: '기준일',
itemCondition: '제품 상태',
priceVerified: '가격 확인',
extension: '추가 콘텐츠', extension: '추가 콘텐츠',
none: '없음', none: '없음',
supported: '대응', supported: '대응',
@@ -42,7 +50,7 @@ document.addEventListener('DOMContentLoaded', () => {
}, },
ja: { ja: {
infoTitle: 'Infomation', infoTitle: 'Infomation',
purchaseTitle: '購入情報', purchaseTitle: '販売情報',
requiredCapacity: '必要な容量', requiredCapacity: '必要な容量',
playMode: 'プレイモード', playMode: 'プレイモード',
playUser: 'プレイ人数', playUser: 'プレイ人数',
@@ -55,10 +63,13 @@ document.addEventListener('DOMContentLoaded', () => {
cero: 'CERO', cero: 'CERO',
iarc: 'IARC', iarc: 'IARC',
releaseDate: '配信日', releaseDate: '配信日',
purchaseDate: '購入日', suggestedPrice: '推奨販売価格',
store: '購入先', pricePending: '-',
price: '購入価格', priceRange: '価格',
orderNumber: '注文番号', pricingBasis: '価格参考基準',
checkedAt: '確認日',
itemCondition: '商品状態',
priceVerified: '価格確認',
extension: '追加コンテンツ', extension: '追加コンテンツ',
none: 'なし', none: 'なし',
supported: '対応', supported: '対応',
@@ -68,11 +79,178 @@ document.addEventListener('DOMContentLoaded', () => {
const currentTexts = texts[language]; const currentTexts = texts[language];
function formatKRW(value) {
if (typeof value !== 'number') return currentTexts.pricePending;
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 formatItemCondition(itemCondition) {
if (!itemCondition || itemCondition === '-') return '';
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;
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 = const displayTitle = language === 'ko' ? game.koTitle || game.title : game.title;
language === 'ko' ? game.koTitle || game.title : game.title; const gameTitleEl = document.getElementById('gameTitle');
document.getElementById('gameTags').textContent = game.tags; 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>${displayTitle}</span>`;
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 +258,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 =
@@ -98,11 +280,11 @@ document.addEventListener('DOMContentLoaded', () => {
document.getElementById('gameCountry').textContent = document.getElementById('gameCountry').textContent =
language === 'ko' language === 'ko'
? game.country === 'JPN' ? game.country === 'JPN'
? '일본판' ? '🇯🇵 일본판'
: '한국판' : '🇰🇷 한국 정발판'
: game.country === 'JPN' : game.country === 'JPN'
? '日本版' ? '🇯🇵 日本版'
: '韓国版'; : '🇰🇷 韓国版';
// 게임 정보 설정 // 게임 정보 설정
const gameInfo = document.getElementById('gameInfo'); const gameInfo = document.getElementById('gameInfo');
@@ -116,41 +298,51 @@ 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 },
...(SHOW_RECOMMENDED_PRICE_RANGE_BY_DEFAULT
? [{ 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 },
]; ];
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,12 @@
import NSW_DB from '../db/nsw.db.js'; 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', () => { document.addEventListener('DOMContentLoaded', () => {
const gameList = document.getElementById('gameList'); const gameList = document.getElementById('gameList');
@@ -9,27 +17,25 @@ document.addEventListener('DOMContentLoaded', () => {
// 언어별 UI 텍스트 // 언어별 UI 텍스트
const uiTexts = { const uiTexts = {
ko: { ko: {
title: 'Switch DB', title: 'Switch 패키지 판매 목록',
languageSelect: '언어 선택', languageSelect: '언어 선택',
languageDescription: '선택한 언어로 표시됩니다', languageDescription: '선택한 언어로 표시됩니다',
korean: '한국어', korean: '한국어',
japanese: '일본어', japanese: '일본어',
count: '개수', count: '개수',
listIntro: '우선 이런 게임들이 있습니다. 가격은 순차적으로 작성중입니다.',
pricePending: '-',
loading: '로딩중...', loading: '로딩중...',
tableHeaders: { tableHeaders: {
title: '제목', title: '제목',
info: '정보', info: '판매가',
status: '상태', status: '판매 상태',
role: '역할',
location: '지역'
}, },
sortOptions: { sortOptions: {
sortByNoDesc: '순번 최신순',
sortByNo: '순번 과거순',
sortByDateDesc: '발매일 최신순', sortByDateDesc: '발매일 최신순',
sortByDate: '발매일 과거순', sortByDate: '발매일 과거순',
sortByPurchaseDateDesc: '구매일 최신순', sortByPriceDesc: '판매가 높은순',
sortByPurchaseDate: '구매일 과거순', sortByPrice: '판매가 낮은순',
sortByRandom: '무작위', sortByRandom: '무작위',
}, },
filter: { filter: {
@@ -39,27 +45,25 @@ document.addEventListener('DOMContentLoaded', () => {
}, },
}, },
ja: { ja: {
title: 'ゲーム一覧', title: '中古販売リスト',
languageSelect: '言語選択', languageSelect: '言語選択',
languageDescription: '選択した言語で表示されます', languageDescription: '選択した言語で表示されます',
korean: '韓国語', korean: '韓国語',
japanese: '日本語', japanese: '日本語',
count: '件数', count: '件数',
listIntro: 'まずは所持タイトルを掲載しています。価格は順次入力中です。',
pricePending: '-',
loading: '読み込み中...', loading: '読み込み中...',
tableHeaders: { tableHeaders: {
title: 'タイトル', title: 'タイトル',
info: '情報', info: '販売価格',
status: '状態', status: '販売状態',
role: '役割',
location: '地域'
}, },
sortOptions: { sortOptions: {
sortByNoDesc: '番号降順',
sortByNo: '番号昇順',
sortByDateDesc: '発売日降順', sortByDateDesc: '発売日降順',
sortByDate: '発売日昇順', sortByDate: '発売日昇順',
sortByPurchaseDateDesc: '購入日降順', sortByPriceDesc: '販売価格降順',
sortByPurchaseDate: '購入日昇順', sortByPrice: '販売価格昇順',
sortByRandom: 'ランダム', sortByRandom: 'ランダム',
}, },
filter: { filter: {
@@ -79,20 +83,10 @@ document.addEventListener('DOMContentLoaded', () => {
country: [], country: [],
cero: [], cero: [],
}, },
sortBy: 'sortByNoDesc', // 기본 정렬 옵션 searchText: '',
sortBy: 'sortByRandom',
}; };
// 정렬 옵션 정의
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];
@@ -101,38 +95,44 @@ document.addEventListener('DOMContentLoaded', () => {
document.querySelector('h1').textContent = texts.title; document.querySelector('h1').textContent = texts.title;
// 언어 선택 섹션 업데이트 // 언어 선택 섹션 업데이트
document.querySelector('label.text-base').textContent = texts.languageSelect; setTextContent('label.text-base', texts.languageSelect);
document.querySelector('p.text-sm').textContent = texts.languageDescription; setTextContent('p.text-sm', texts.languageDescription);
document.querySelector('label[for="ko"]').textContent = texts.korean; setTextContent('label[for="ko"]', texts.korean);
document.querySelector('label[for="ja"]').textContent = texts.japanese; setTextContent('label[for="ja"]', texts.japanese);
// 테이블 헤더 업데이트 // 테이블 헤더 업데이트
const headers = document.querySelectorAll('th'); const headers = document.querySelectorAll('th');
headers[0].textContent = texts.tableHeaders.title; if (headers[0]) headers[0].textContent = texts.tableHeaders.title;
headers[1].textContent = texts.tableHeaders.info; if (headers[1]) headers[1].textContent = texts.tableHeaders.info;
headers[2].textContent = texts.tableHeaders.status; if (headers[2]) headers[2].textContent = texts.tableHeaders.status;
headers[3].textContent = texts.tableHeaders.role;
headers[4].textContent = texts.tableHeaders.location;
// 로딩 텍스트 업데이트 // 로딩 텍스트 업데이트
loading.textContent = texts.loading; loading.textContent = texts.loading;
setTextContent('#priceProgressNotice', texts.listIntro);
// 필터 텍스트 업데이트 // 필터 텍스트 업데이트
document.getElementById('resetFilters').textContent = texts.filter.reset; setTextContent('#resetFilters', texts.filter.reset);
document.querySelector('label[for="korean-support"]').textContent = texts.filter.koreanSupport; setTextContent('label[for="korean-support"]', texts.filter.koreanSupport);
document.querySelector('label[for="korean-not-support"]').textContent = setTextContent('label[for="korean-not-support"]', texts.filter.koreanNotSupport);
texts.filter.koreanNotSupport; }
function setTextContent(selector, text) {
const element = document.querySelector(selector);
if (element) element.textContent = text;
} }
// 언어 변경 이벤트 리스너 // 언어 변경 이벤트 리스너
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,34 +581,183 @@ 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),
formattedItemCondition: formatItemCondition(game.itemCondition),
formattedEdition: formatEdition(game.country),
formattedKoreanSupport: formatKoreanSupport(game.language),
hasItemCondition: Boolean(formatItemCondition(game.itemCondition)),
hasKoreanSupport: hasKoreanLanguage(game.language),
}; };
} }
function getStatusClass(status) { 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) { 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';
} }
} }
function getCountryClass(country) { function getItemConditionClass(itemCondition) {
return country === 'JPN' return 'bg-amber-100 text-amber-800';
? 'text-red-600 hover:text-red-900' }
: 'text-indigo-600 hover:text-indigo-900';
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) { function createGameRow(game, index) {
@@ -622,40 +771,57 @@ document.addEventListener('DOMContentLoaded', () => {
tr.innerHTML = ` tr.innerHTML = `
<td class="py-4 pl-4 pr-3 text-sm"> <td class="py-4 pl-4 pr-3 text-sm">
<div class="flex items-center"> <div class="flex items-start">
<div class="h-10 w-10 flex-shrink-0"> <div class="h-10 w-10 flex-shrink-0">
<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 min-w-0 flex-1">
<div class="font-medium text-gray-900 ${ <div class="font-medium leading-5 text-gray-900 flex items-center gap-1">
game.status === 'sold' ? 'text-red-600 line-through' : '' ${game.sale?.priceVerified ? `${VERIFIED_PRICE_MARK} ` : ''}<span>${game.formattedTitle}</span>
}"> </div>
${game.no}. ${game.formattedTitle} <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 class="text-gray-500">${game.formattedReleaseDate}</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-56 px-3 py-4 text-sm text-gray-500 sm:table-cell">
<div class="text-gray-900">${game.formattedMaker}</div> <div class="font-semibold leading-5 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="hidden w-28 px-3 py-4 text-sm text-gray-500 mx-auto sm:table-cell">
<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>
</td>
<td class="w-3/12 hidden px-3 py-4 text-sm text-gray-500 lg:table-cell">
${game.formattedTags}
</td>
<td class="hidden py-4 pl-3 pr-4 text-right text-sm font-medium sm:table-cell">
<div class="${getCountryClass(game.country)}">
${game.formattedCountry}
</div> </div>
</td> </td>
`; `;
@@ -677,16 +843,152 @@ document.addEventListener('DOMContentLoaded', () => {
country: [], country: [],
cero: [], cero: [],
}; };
filterState.searchText = '';
document.querySelectorAll('[data-search-input]').forEach(input => {
input.value = '';
});
// 게임 목록 다시 렌더링 // 게임 목록 다시 렌더링
renderGames(); renderGames();
} }
// 필터 초기화 버튼 이벤트 리스너 설정 // 필터 초기화 버튼 이벤트 리스너 설정
document.getElementById('resetFilters').addEventListener('click', resetFilters); document.getElementById('resetFilters')?.addEventListener('click', resetFilters);
// 필터 체크박스 이벤트 리스너 설정 // 필터 체크박스 이벤트 리스너 설정
function setupFilterListeners() { 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 => { document.querySelectorAll('input[name="language-filter"]').forEach(checkbox => {
checkbox.addEventListener('change', () => { checkbox.addEventListener('change', () => {
@@ -695,7 +997,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 +1031,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;
}
} }
// 국가 필터 // 국가 필터
@@ -793,43 +1100,39 @@ document.addEventListener('DOMContentLoaded', () => {
// 정렬 함수 // 정렬 함수
function sortGames(games) { function sortGames(games) {
switch (filterState.sortBy) { switch (filterState.sortBy) {
case 'sortByNoDesc':
return [...games].sort((a, b) => b.no - a.no);
case 'sortByNo':
return [...games].sort((a, b) => a.no - b.no);
case 'sortByDateDesc': case 'sortByDateDesc':
return [...games].sort((a, b) => { return [...games].sort((a, b) => {
const aDate = new Date(a.release.replace(/年|月/g, '/').replace(/日/g, '')).toUTCString(); const aDate = new Date(a.release.replace(/年|月/g, '/').replace(/日/g, '')).toUTCString();
const bDate = new Date(b.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); const dateDiff = new Date(bDate) - new Date(aDate);
return dateDiff === 0 ? b.no - a.no : dateDiff; return dateDiff === 0 ? compareTitles(a, b) : dateDiff;
}); });
case 'sortByDate': case 'sortByDate':
return [...games].sort((a, b) => { return [...games].sort((a, b) => {
const aDate = new Date(a.release.replace(/年|月/g, '/').replace(/日/g, '')).toUTCString(); const aDate = new Date(a.release.replace(/年|月/g, '/').replace(/日/g, '')).toUTCString();
const bDate = new Date(b.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); const dateDiff = new Date(aDate) - new Date(bDate);
return dateDiff === 0 ? b.no - a.no : dateDiff; return dateDiff === 0 ? compareTitles(a, b) : dateDiff;
}); });
case 'sortByPurchaseDateDesc': case 'sortByPriceDesc':
return [...games].sort((a, b) => { return [...games].sort((a, b) => {
if (!a.purchaseInformation?.date || !b.purchaseInformation?.date) { const aPrice = getSuggestedPriceValue(a);
return !a.purchaseInformation?.date ? 1 : -1; const bPrice = getSuggestedPriceValue(b);
} if (aPrice === null && bPrice === null) return compareTitles(a, b);
const aDate = new Date(a.purchaseInformation.date.replace(/\./g, '/')).toUTCString(); if (aPrice === null) return 1;
const bDate = new Date(b.purchaseInformation.date.replace(/\./g, '/')).toUTCString(); if (bPrice === null) return -1;
const dateDiff = new Date(bDate) - new Date(aDate); const priceDiff = bPrice - aPrice;
return dateDiff === 0 ? b.no - a.no : dateDiff; return priceDiff === 0 ? compareTitles(a, b) : priceDiff;
}); });
case 'sortByPurchaseDate': case 'sortByPrice':
return [...games].sort((a, b) => { return [...games].sort((a, b) => {
if (!a.purchaseInformation?.date || !b.purchaseInformation?.date) { const aPrice = getSuggestedPriceValue(a);
return !a.purchaseInformation?.date ? 1 : -1; const bPrice = getSuggestedPriceValue(b);
} if (aPrice === null && bPrice === null) return compareTitles(a, b);
const aDate = new Date(a.purchaseInformation.date.replace(/\./g, '/')).toUTCString(); if (aPrice === null) return 1;
const bDate = new Date(b.purchaseInformation.date.replace(/\./g, '/')).toUTCString(); if (bPrice === null) return -1;
const dateDiff = new Date(aDate) - new Date(bDate); const priceDiff = aPrice - bPrice;
return dateDiff === 0 ? b.no - a.no : dateDiff; return priceDiff === 0 ? compareTitles(a, b) : priceDiff;
}); });
case 'sortByRandom': case 'sortByRandom':
return [...games].sort(() => Math.random() - 0.5); return [...games].sort(() => Math.random() - 0.5);
@@ -838,6 +1141,10 @@ document.addEventListener('DOMContentLoaded', () => {
} }
} }
function compareTitles(a, b) {
return a.formattedTitle.localeCompare(b.formattedTitle, filterState.language === 'ko' ? 'ko' : 'ja');
}
// 정렬 UI 초기화 // 정렬 UI 초기화
function setupSortUI() { function setupSortUI() {
const sortButton = document.getElementById('sort-button'); const sortButton = document.getElementById('sort-button');

View File

@@ -15,17 +15,12 @@
--color-yellow-800: oklch(47.6% 0.114 61.907); --color-yellow-800: oklch(47.6% 0.114 61.907);
--color-green-100: oklch(96.2% 0.044 156.743); --color-green-100: oklch(96.2% 0.044 156.743);
--color-green-800: oklch(44.8% 0.119 151.328); --color-green-800: oklch(44.8% 0.119 151.328);
--color-teal-100: oklch(95.3% 0.051 180.801);
--color-teal-800: oklch(43.7% 0.078 188.216);
--color-blue-100: oklch(93.2% 0.032 255.585); --color-blue-100: oklch(93.2% 0.032 255.585);
--color-blue-800: oklch(42.4% 0.199 265.638); --color-blue-800: oklch(42.4% 0.199 265.638);
--color-indigo-50: oklch(96.2% 0.018 272.314); --color-indigo-50: oklch(96.2% 0.018 272.314);
--color-indigo-400: oklch(67.3% 0.182 276.935);
--color-indigo-500: oklch(58.5% 0.233 277.117); --color-indigo-500: oklch(58.5% 0.233 277.117);
--color-indigo-600: oklch(51.1% 0.262 276.966); --color-indigo-600: oklch(51.1% 0.262 276.966);
--color-indigo-900: oklch(35.9% 0.144 278.697); --color-indigo-900: oklch(35.9% 0.144 278.697);
--color-purple-100: oklch(94.6% 0.033 307.174);
--color-purple-800: oklch(43.8% 0.218 303.724);
--color-slate-50: oklch(98.4% 0.003 247.858); --color-slate-50: oklch(98.4% 0.003 247.858);
--color-gray-50: oklch(98.5% 0.002 247.839); --color-gray-50: oklch(98.5% 0.002 247.839);
--color-gray-100: oklch(96.7% 0.003 264.542); --color-gray-100: oklch(96.7% 0.003 264.542);
@@ -33,14 +28,12 @@
--color-gray-300: oklch(87.2% 0.01 258.338); --color-gray-300: oklch(87.2% 0.01 258.338);
--color-gray-400: oklch(70.7% 0.022 261.325); --color-gray-400: oklch(70.7% 0.022 261.325);
--color-gray-500: oklch(55.1% 0.027 264.364); --color-gray-500: oklch(55.1% 0.027 264.364);
--color-gray-600: oklch(44.6% 0.03 256.802);
--color-gray-700: oklch(37.3% 0.034 259.733); --color-gray-700: oklch(37.3% 0.034 259.733);
--color-gray-800: oklch(27.8% 0.033 256.848);
--color-gray-900: oklch(21% 0.034 264.665); --color-gray-900: oklch(21% 0.034 264.665);
--color-black: #000; --color-black: #000;
--color-white: #fff; --color-white: #fff;
--spacing: 0.25rem; --spacing: 0.25rem;
--container-xs: 20rem; --container-lg: 32rem;
--container-2xl: 42rem; --container-2xl: 42rem;
--container-4xl: 56rem; --container-4xl: 56rem;
--container-7xl: 80rem; --container-7xl: 80rem;
@@ -61,9 +54,6 @@
--radius-md: 0.375rem; --radius-md: 0.375rem;
--radius-lg: 0.5rem; --radius-lg: 0.5rem;
--radius-xl: 0.75rem; --radius-xl: 0.75rem;
--ease-in: cubic-bezier(0.4, 0, 1, 1);
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--default-transition-duration: 150ms; --default-transition-duration: 150ms;
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
--default-font-family: var(--font-sans); --default-font-family: var(--font-sans);
@@ -240,24 +230,6 @@
.row-start-1 { .row-start-1 {
grid-row-start: 1; grid-row-start: 1;
} }
.container {
width: 100%;
@media (width >= 40rem) {
max-width: 40rem;
}
@media (width >= 48rem) {
max-width: 48rem;
}
@media (width >= 64rem) {
max-width: 64rem;
}
@media (width >= 80rem) {
max-width: 80rem;
}
@media (width >= 96rem) {
max-width: 96rem;
}
}
.-mx-4 { .-mx-4 {
margin-inline: calc(var(--spacing) * -4); margin-inline: calc(var(--spacing) * -4);
} }
@@ -276,6 +248,9 @@
.mt-2 { .mt-2 {
margin-top: calc(var(--spacing) * 2); margin-top: calc(var(--spacing) * 2);
} }
.mt-3 {
margin-top: calc(var(--spacing) * 3);
}
.mt-4 { .mt-4 {
margin-top: calc(var(--spacing) * 4); margin-top: calc(var(--spacing) * 4);
} }
@@ -288,9 +263,6 @@
.mt-16 { .mt-16 {
margin-top: calc(var(--spacing) * 16); margin-top: calc(var(--spacing) * 16);
} }
.mr-1 {
margin-right: calc(var(--spacing) * 1);
}
.mb-4 { .mb-4 {
margin-bottom: calc(var(--spacing) * 4); margin-bottom: calc(var(--spacing) * 4);
} }
@@ -303,6 +275,12 @@
.ml-4 { .ml-4 {
margin-left: calc(var(--spacing) * 4); margin-left: calc(var(--spacing) * 4);
} }
.line-clamp-2 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.block { .block {
display: block; display: block;
} }
@@ -349,9 +327,6 @@
.max-h-96 { .max-h-96 {
max-height: calc(var(--spacing) * 96); max-height: calc(var(--spacing) * 96);
} }
.w-3\/12 {
width: calc(3/12 * 100%);
}
.w-4 { .w-4 {
width: calc(var(--spacing) * 4); width: calc(var(--spacing) * 4);
} }
@@ -361,12 +336,18 @@
.w-10 { .w-10 {
width: calc(var(--spacing) * 10); width: calc(var(--spacing) * 10);
} }
.w-24 { .w-28 {
width: calc(var(--spacing) * 24); width: calc(var(--spacing) * 28);
}
.w-32 {
width: calc(var(--spacing) * 32);
} }
.w-40 { .w-40 {
width: calc(var(--spacing) * 40); width: calc(var(--spacing) * 40);
} }
.w-56 {
width: calc(var(--spacing) * 56);
}
.w-\[calc\(100\%-3rem\)\] { .w-\[calc\(100\%-3rem\)\] {
width: calc(100% - 3rem); width: calc(100% - 3rem);
} }
@@ -379,6 +360,9 @@
.max-w-7xl { .max-w-7xl {
max-width: var(--container-7xl); max-width: var(--container-7xl);
} }
.max-w-lg {
max-width: var(--container-lg);
}
.min-w-0 { .min-w-0 {
min-width: calc(var(--spacing) * 0); min-width: calc(var(--spacing) * 0);
} }
@@ -391,21 +375,12 @@
.flex-shrink-0 { .flex-shrink-0 {
flex-shrink: 0; flex-shrink: 0;
} }
.flex-grow {
flex-grow: 1;
}
.rotate-180 {
rotate: 180deg;
}
.transform { .transform {
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
} }
.cursor-pointer { .cursor-pointer {
cursor: pointer; cursor: pointer;
} }
.resize {
resize: both;
}
.grid-cols-1 { .grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr)); grid-template-columns: repeat(1, minmax(0, 1fr));
} }
@@ -421,6 +396,9 @@
.items-center { .items-center {
align-items: center; align-items: center;
} }
.items-start {
align-items: flex-start;
}
.justify-between { .justify-between {
justify-content: space-between; justify-content: space-between;
} }
@@ -444,6 +422,9 @@
margin-block-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse))); margin-block-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)));
} }
} }
.gap-x-3 {
column-gap: calc(var(--spacing) * 3);
}
.gap-x-4 { .gap-x-4 {
column-gap: calc(var(--spacing) * 4); column-gap: calc(var(--spacing) * 4);
} }
@@ -460,6 +441,9 @@
margin-inline-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse))); margin-inline-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)));
} }
} }
.gap-y-1 {
row-gap: calc(var(--spacing) * 1);
}
.gap-y-2 { .gap-y-2 {
row-gap: calc(var(--spacing) * 2); row-gap: calc(var(--spacing) * 2);
} }
@@ -553,27 +537,18 @@
.bg-gray-50 { .bg-gray-50 {
background-color: var(--color-gray-50); background-color: var(--color-gray-50);
} }
.bg-gray-100 {
background-color: var(--color-gray-100);
}
.bg-green-100 { .bg-green-100 {
background-color: var(--color-green-100); background-color: var(--color-green-100);
} }
.bg-indigo-600 { .bg-indigo-600 {
background-color: var(--color-indigo-600); background-color: var(--color-indigo-600);
} }
.bg-purple-100 {
background-color: var(--color-purple-100);
}
.bg-red-100 { .bg-red-100 {
background-color: var(--color-red-100); background-color: var(--color-red-100);
} }
.bg-slate-50 { .bg-slate-50 {
background-color: var(--color-slate-50); background-color: var(--color-slate-50);
} }
.bg-teal-100 {
background-color: var(--color-teal-100);
}
.bg-white { .bg-white {
background-color: var(--color-white); background-color: var(--color-white);
} }
@@ -606,9 +581,6 @@
.px-4 { .px-4 {
padding-inline: calc(var(--spacing) * 4); padding-inline: calc(var(--spacing) * 4);
} }
.py-0\.5 {
padding-block: calc(var(--spacing) * 0.5);
}
.py-1 { .py-1 {
padding-block: calc(var(--spacing) * 1); padding-block: calc(var(--spacing) * 1);
} }
@@ -726,12 +698,12 @@
--tw-tracking: var(--tracking-tight); --tw-tracking: var(--tracking-tight);
letter-spacing: var(--tracking-tight); letter-spacing: var(--tracking-tight);
} }
.whitespace-nowrap {
white-space: nowrap;
}
.text-blue-800 { .text-blue-800 {
color: var(--color-blue-800); color: var(--color-blue-800);
} }
.text-gray-300 {
color: var(--color-gray-300);
}
.text-gray-400 { .text-gray-400 {
color: var(--color-gray-400); color: var(--color-gray-400);
} }
@@ -750,27 +722,18 @@
.text-indigo-600 { .text-indigo-600 {
color: var(--color-indigo-600); color: var(--color-indigo-600);
} }
.text-purple-800 {
color: var(--color-purple-800);
}
.text-red-600 { .text-red-600 {
color: var(--color-red-600); color: var(--color-red-600);
} }
.text-red-800 { .text-red-800 {
color: var(--color-red-800); color: var(--color-red-800);
} }
.text-teal-800 {
color: var(--color-teal-800);
}
.text-white { .text-white {
color: var(--color-white); color: var(--color-white);
} }
.text-yellow-800 { .text-yellow-800 {
color: var(--color-yellow-800); color: var(--color-yellow-800);
} }
.line-through {
text-decoration-line: line-through;
}
.shadow { .shadow {
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
@@ -816,11 +779,6 @@
.filter { .filter {
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
} }
.transition {
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
.transition-transform { .transition-transform {
transition-property: transform, translate, scale, rotate; transition-property: transform, translate, scale, rotate;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
@@ -970,6 +928,11 @@
margin-top: calc(var(--spacing) * 0); margin-top: calc(var(--spacing) * 0);
} }
} }
.sm\:hidden {
@media (width >= 40rem) {
display: none;
}
}
.sm\:table-cell { .sm\:table-cell {
@media (width >= 40rem) { @media (width >= 40rem) {
display: table-cell; display: table-cell;
@@ -1069,11 +1032,6 @@
display: none; display: none;
} }
} }
.lg\:table-cell {
@media (width >= 64rem) {
display: table-cell;
}
}
.lg\:max-h-none { .lg\:max-h-none {
@media (width >= 64rem) { @media (width >= 64rem) {
max-height: none; max-height: none;