Compare commits

15 Commits

Author SHA1 Message Date
ef0b48dc16 Update used sale filters and guide 2026-08-03 18:50:30 +09:00
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
16 changed files with 8402 additions and 21309 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
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
- 게임 목록 서비스의 기준 화면을 `index.html` + `nsw-detail.html` 2개로 단순화했다.
- 데이터 저장소를 별도 API 없이 정적 파일(`db/nsw.db.js`)로 유지하여 배포 복잡도를 낮췄다.

View File

@@ -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`: 아미보 관련 데이터(현재 화면 연결 없음)

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

@@ -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,13 @@
- `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): 판매자가 직접 작성하는 제품 상태. 예: `미개봉 새제품`, `초회 한정판`
- `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):
- `currency` (string): `KRW`
- `suggestedPrice` (`number` | `"none"`): 수기 입력된 판매가(KRW). 미입력 항목은 `"none"`.
## 필터/정렬 동작
- 필터:

View File

@@ -4,6 +4,8 @@
- 레거시 페이지/리소스(아미보, 세일 관련) 제거 후 문서와 실제 구조 정합성 점검이 필요하다.
## 다음 작업
- `sale.suggestedPrice`를 타이틀별로 수기 확인해 숫자 가격으로 순차 입력한다.
- `itemCondition` 값을 타이틀별로 실제 데이터에 맞게 채운다.
- 목록/상세 페이지의 경로 및 텍스트 다국어 키를 재검증한다.
- 필터 조합(언어+상태+국가+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
- 커밋 제목 규칙 추가: `vYYYY.MM.DD-번호 내용`
- `docs/convention.md` 커밋 제목 형식 가이드 반영

View File

@@ -9,12 +9,60 @@
<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>
<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="lg:block">
@@ -78,7 +126,7 @@
<div class="flex items-center justify-between">
<div class="w-full">
<form id="searchForm" class="mb-4">
<form id="searchForm" class="mb-4" onsubmit="return false">
<label
for="search-input"
class="block text-sm font-medium leading-6 text-gray-900">
@@ -91,13 +139,18 @@
</span>
<input
type="text"
name="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=" " />
<div
class="cursor-pointer absolute inset-y-0 right-0 flex items-center pr-3"
id="reset-search">
class="cursor-pointer absolute inset-y-0 right-0 z-10 flex items-center pr-3"
id="reset-search"
onclick="window.clearNswSearch && window.clearNswSearch()">
<svg
class="h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
@@ -134,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"
@@ -147,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"
@@ -158,39 +213,20 @@
</div>
</div>
<!-- 상태 필터 -->
<div class="space-y-2">
<h3 class="text-base font-medium text-gray-900">Status</h3>
<!-- 판매 상태 필터 -->
<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">
<input
id="package"
id="available"
name="status-filter"
type="checkbox"
value="package"
value="available"
checked
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>
</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 for="available" class="ml-2 text-sm text-gray-900">
판매 가능
</label>
</div>
<div class="flex items-center">
@@ -200,7 +236,7 @@
type="checkbox"
value="sold"
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>
@@ -215,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
@@ -226,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
@@ -244,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>
@@ -253,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>
@@ -262,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>
@@ -271,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>
@@ -283,11 +347,14 @@
<div class="lg:col-span-3">
<div class="sm:px-6 lg:px-8">
<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">
개수:
<span id="gameCount">0</span>
</p>
<p id="priceProgressNotice" class="mt-2 max-w-md text-sm text-gray-500 sm:mt-0">
우선 이런 게임들이 있습니다. 가격은 순차적으로 작성중입니다.
</p>
<div class="sort">
<div class="flex items-center gap-2">
<label id="sort-label" class="block text-sm/6 font-medium text-gray-900">
@@ -300,7 +367,7 @@
id="sort-button"
aria-expanded="false"
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
class="w-4 h-4 transform transition-transform duration-200"
fill="none"
@@ -320,46 +387,6 @@
tabindex="-1"
role="listbox"
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
class="relative cursor-pointer hover:bg-gray-100 select-none py-2 pl-3 pr-9 text-gray-900"
role="option"
@@ -403,8 +430,8 @@
<li
class="relative cursor-pointer hover:bg-gray-100 select-none py-2 pl-3 pr-9 text-gray-900"
role="option"
data-value="sortByPurchaseDateDesc">
<span class="block truncate">구매일 최신순</span>
data-value="sortByPriceDesc">
<span class="block truncate">판매가 높은순</span>
<span
class="absolute inset-y-0 right-0 flex items-center pr-4 text-indigo-600 hidden">
<svg
@@ -423,8 +450,8 @@
<li
class="relative cursor-pointer hover:bg-gray-100 select-none py-2 pl-3 pr-9 text-gray-900"
role="option"
data-value="sortByPurchaseDate">
<span class="block truncate">구매일 과거순</span>
data-value="sortByPrice">
<span class="block truncate">판매가 낮은순</span>
<span
class="absolute inset-y-0 right-0 flex items-center pr-4 text-indigo-600 hidden">
<svg
@@ -444,9 +471,9 @@
class="relative cursor-pointer hover:bg-gray-100 select-none py-2 pl-3 pr-9 text-gray-900"
role="option"
data-value="sortByRandom">
<span class="block truncate">무작위</span>
<span class="block truncate font-semibold">무작위</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
class="size-5"
viewBox="0 0 20 20"
@@ -480,29 +507,34 @@
</th>
<th
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
</th>
<th
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
</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>
</thead>
<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>
@@ -517,6 +549,6 @@
</main>
</div>
</div>
<script type="module" src="./script/nsw.js"></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">
@@ -33,7 +33,7 @@
<div class="flex justify-between px-4 py-5 sm:px-6">
<div>
<h3 id="purchaseTitle" class="text-base font-semibold leading-6 text-gray-900">
구매 정보
판매 정보
</h3>
<p id="purchaseGameTitle" class="mt-1 max-w-2xl text-sm text-gray-500"></p>
</div>
@@ -45,7 +45,7 @@
</div>
</div>
<div id="purchaseInfo" class="border-t border-gray-200 px-4 py-5 sm:px-6">
<!-- 구매 정보가 여기에 동적으로 추가됩니다 -->
<!-- 판매 정보가 여기에 동적으로 추가됩니다 -->
</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?v=20260803-status-v4';
const SHOW_RECOMMENDED_PRICE_RANGE_BY_DEFAULT = false;
/** @type {string} 가격 확인 완료 표시 */
const VERIFIED_PRICE_MARK = '⭐';
document.addEventListener('DOMContentLoaded', () => {
// URL에서 게임 번호 가져오기
@@ -18,7 +23,7 @@ document.addEventListener('DOMContentLoaded', () => {
const texts = {
ko: {
infoTitle: '게임 정보',
purchaseTitle: '구매 정보',
purchaseTitle: '판매 정보',
requiredCapacity: '필요한 용량',
playMode: '플레이 모드',
playUser: '플레이 인원',
@@ -31,10 +36,13 @@ document.addEventListener('DOMContentLoaded', () => {
cero: '심의 등급',
iarc: '심의 등급',
releaseDate: '출시일',
purchaseDate: '구매일',
store: '구매처',
price: '구매 가격',
orderNumber: '주문번호',
suggestedPrice: '판매가',
pricePending: '-',
priceRange: '판매가 범위',
pricingBasis: '가격 참고 기준',
checkedAt: '기준일',
itemCondition: '제품 상태',
priceVerified: '가격 확인',
extension: '추가 콘텐츠',
none: '없음',
supported: '대응',
@@ -42,7 +50,7 @@ document.addEventListener('DOMContentLoaded', () => {
},
ja: {
infoTitle: 'Infomation',
purchaseTitle: '購入情報',
purchaseTitle: '販売情報',
requiredCapacity: '必要な容量',
playMode: 'プレイモード',
playUser: 'プレイ人数',
@@ -55,10 +63,13 @@ document.addEventListener('DOMContentLoaded', () => {
cero: 'CERO',
iarc: 'IARC',
releaseDate: '配信日',
purchaseDate: '購入日',
store: '購入先',
price: '購入価格',
orderNumber: '注文番号',
suggestedPrice: '推奨販売価格',
pricePending: '-',
priceRange: '価格帯',
pricingBasis: '価格参考基準',
checkedAt: '確認日',
itemCondition: '商品状態',
priceVerified: '価格確認',
extension: '追加コンテンツ',
none: 'なし',
supported: '対応',
@@ -68,11 +79,178 @@ document.addEventListener('DOMContentLoaded', () => {
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('gameTitle').textContent =
language === 'ko' ? game.koTitle || game.title : game.title;
document.getElementById('gameTags').textContent = game.tags;
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>${displayTitle}</span>`;
document.getElementById('gameTags').textContent = convertTags(game.tags);
document.getElementById('infoTitle').textContent = currentTexts.infoTitle;
document.getElementById('purchaseTitle').textContent = currentTexts.purchaseTitle;
document.getElementById('purchaseGameTitle').textContent =
@@ -80,14 +258,18 @@ document.addEventListener('DOMContentLoaded', () => {
// 게임 상태 설정
const statusClass = {
package: 'bg-green-100 text-green-800',
available: 'bg-green-100 text-green-800',
download: 'bg-yellow-100 text-yellow-800',
expansion: 'bg-blue-100 text-blue-800',
sold: 'bg-red-100 text-red-800',
}[game.status];
document.getElementById(
'gameStatus',
).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 =
@@ -98,11 +280,11 @@ document.addEventListener('DOMContentLoaded', () => {
document.getElementById('gameCountry').textContent =
language === 'ko'
? game.country === 'JPN'
? '일본판'
: '한국판'
? '🇯🇵 일본판'
: '🇰🇷 한국 정발판'
: game.country === 'JPN'
? '日本版'
: '韓国版';
? '🇯🇵 日本版'
: '🇰🇷 韓国版';
// 게임 정보 설정
const gameInfo = document.getElementById('gameInfo');
@@ -116,41 +298,51 @@ document.addEventListener('DOMContentLoaded', () => {
{ key: 'language', label: currentTexts.language },
{ key: 'cero', label: currentTexts.cero },
{ key: 'iarc', label: currentTexts.iarc },
{ key: 'releaseDate', label: currentTexts.releaseDate },
{ key: 'release', label: currentTexts.releaseDate },
];
infoItems.forEach(item => {
if (game[item.key]) {
const value = getGameInfoValue(item.key);
if (value) {
const div = document.createElement('div');
div.className = 'border-t border-gray-200 pt-4';
div.innerHTML = `
<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);
}
});
// 구매 정보 설정
// 판매 정보 설정
const purchaseInfo = document.getElementById('purchaseInfo');
if (game.purchaseInformation) {
const purchaseItems = [
{ key: 'date', label: currentTexts.purchaseDate },
{ key: 'store', label: currentTexts.store },
{ key: 'price', label: currentTexts.price },
{ key: 'orderNumber', label: currentTexts.orderNumber },
if (game.sale) {
const priceRange =
game.sale.priceRange?.min && game.sale.priceRange?.max
? `${formatKRW(game.sale.priceRange.min)} ~ ${formatKRW(game.sale.priceRange.max)}`
: '';
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');
dl.className = 'grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2';
purchaseItems.forEach(item => {
if (game.purchaseInformation[item.key]) {
saleItems.forEach(item => {
if (item.value) {
const div = document.createElement('div');
div.className = 'sm:col-span-1';
div.innerHTML = `
<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);
}

View File

@@ -1,35 +1,50 @@
import NSW_DB from '../db/nsw.db.js';
import NSW_DB from '../db/nsw.resale.db.js?v=20260803-status-v4';
window.NSW_APP_VERSION = '20260803-status-v4';
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 = '⭐';
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 = {
ko: {
title: 'Switch DB',
title: 'Switch 패키지 판매 목록',
languageSelect: '언어 선택',
languageDescription: '선택한 언어로 표시됩니다',
korean: '한국어',
japanese: '일본어',
count: '개수',
listIntro: '',
pricePending: '-',
loading: '로딩중...',
tableHeaders: {
title: '제목',
info: '정보',
status: '상태',
role: '역할',
location: '지역'
info: '판매가',
status: '판매 상태',
},
sortOptions: {
sortByNoDesc: '순번 최신순',
sortByNo: '순번 과거순',
sortByDateDesc: '발매일 최신순',
sortByDate: '발매일 과거순',
sortByPurchaseDateDesc: '구매일 최신순',
sortByPurchaseDate: '구매일 과거순',
sortByPriceDesc: '판매가 높은순',
sortByPrice: '판매가 낮은순',
sortByRandom: '무작위',
},
filter: {
@@ -39,27 +54,25 @@ document.addEventListener('DOMContentLoaded', () => {
},
},
ja: {
title: 'ゲーム一覧',
title: '中古販売リスト',
languageSelect: '言語選択',
languageDescription: '選択した言語で表示されます',
korean: '韓国語',
japanese: '日本語',
count: '件数',
listIntro: '',
pricePending: '-',
loading: '読み込み中...',
tableHeaders: {
title: 'タイトル',
info: '情報',
status: '状態',
role: '役割',
location: '地域'
info: '販売価格',
status: '販売状態',
},
sortOptions: {
sortByNoDesc: '番号降順',
sortByNo: '番号昇順',
sortByDateDesc: '発売日降順',
sortByDate: '発売日昇順',
sortByPurchaseDateDesc: '購入日降順',
sortByPurchaseDate: '購入日昇順',
sortByPriceDesc: '販売価格降順',
sortByPrice: '販売価格昇順',
sortByRandom: 'ランダム',
},
filter: {
@@ -74,25 +87,15 @@ 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],
},
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 텍스트 업데이트 함수
function updateUITexts() {
const texts = uiTexts[filterState.language];
@@ -101,38 +104,44 @@ document.addEventListener('DOMContentLoaded', () => {
document.querySelector('h1').textContent = texts.title;
// 언어 선택 섹션 업데이트
document.querySelector('label.text-base').textContent = texts.languageSelect;
document.querySelector('p.text-sm').textContent = texts.languageDescription;
document.querySelector('label[for="ko"]').textContent = texts.korean;
document.querySelector('label[for="ja"]').textContent = texts.japanese;
setTextContent('label.text-base', texts.languageSelect);
setTextContent('p.text-sm', texts.languageDescription);
setTextContent('label[for="ko"]', texts.korean);
setTextContent('label[for="ja"]', texts.japanese);
// 테이블 헤더 업데이트
const headers = document.querySelectorAll('th');
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[4].textContent = texts.tableHeaders.location;
if (headers[0]) headers[0].textContent = texts.tableHeaders.title;
if (headers[1]) headers[1].textContent = texts.tableHeaders.info;
if (headers[2]) headers[2].textContent = texts.tableHeaders.status;
// 로딩 텍스트 업데이트
loading.textContent = texts.loading;
setTextContent('#priceProgressNotice', texts.listIntro);
// 필터 텍스트 업데이트
document.getElementById('resetFilters').textContent = texts.filter.reset;
document.querySelector('label[for="korean-support"]').textContent = texts.filter.koreanSupport;
document.querySelector('label[for="korean-not-support"]').textContent =
texts.filter.koreanNotSupport;
setTextContent('#resetFilters', texts.filter.reset);
setTextContent('label[for="korean-support"]', texts.filter.koreanSupport);
setTextContent('label[for="korean-not-support"]', texts.filter.koreanNotSupport);
}
function setTextContent(selector, text) {
const element = document.querySelector(selector);
if (element) element.textContent = text;
}
// 언어 변경 이벤트 리스너
languageRadios.forEach(radio => {
radio.addEventListener('change', e => {
filterState.language = e.target.value;
localStorage.setItem('language', filterState.language);
updateUITexts();
renderGames();
});
});
localStorage.setItem('language', filterState.language);
// 언어 변환 함수
function convertLanguage(language) {
if (!language) return '';
@@ -581,34 +590,183 @@ document.addEventListener('DOMContentLoaded', () => {
}
function formatGameData(game) {
const suggestedPrice = game.sale?.suggestedPrice;
const priceRange = game.sale?.priceRange;
return {
...game,
formattedTitle: filterState.language === 'ko' ? game.koTitle || game.title : game.title,
formattedReleaseDate: convertReleaseDate(game.release),
formattedMaker: convertMaker(game.maker),
formattedLanguage: convertLanguage(game.language),
formattedTags: convertTags(game.tags),
formattedCountry: convertCountry(game.country),
formattedSuggestedPrice: formatKRW(suggestedPrice),
formattedPriceRange:
priceRange?.min && priceRange?.max
? `${formatKRW(priceRange.min)} ~ ${formatKRW(priceRange.max)}`
: '',
formattedPricingBasis: formatPricingBasis(game.sale?.pricingBasis),
formattedConfidenceDescription: formatConfidenceDescription(game.sale?.confidence),
formattedSaleStatus: formatSaleStatus(game.status),
formattedItemCondition: formatItemCondition(game.itemCondition),
formattedEdition: formatEdition(game.country),
formattedKoreanSupport: formatKoreanSupport(game.language),
hasItemCondition: Boolean(formatItemCondition(game.itemCondition)),
hasKoreanSupport: hasKoreanLanguage(game.language),
};
}
function 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) {
case 'package':
return 'bg-green-100 text-green-800';
case 'download':
return 'bg-yellow-100 text-yellow-800';
case 'expansion':
return 'bg-blue-100 text-blue-800';
case 'sold':
return 'bg-red-100 text-red-800';
default:
return '';
return 'bg-green-100 text-green-800';
}
}
function getCountryClass(country) {
return country === 'JPN'
? 'text-red-600 hover:text-red-900'
: 'text-indigo-600 hover:text-indigo-900';
function getItemConditionClass(itemCondition) {
return 'bg-amber-100 text-amber-800';
}
function getEditionClass(country) {
return country === 'JPN' ? 'bg-red-100 text-red-800' : 'bg-indigo-100 text-indigo-800';
}
function getKoreanSupportClass(hasKoreanSupport) {
return hasKoreanSupport ? 'bg-emerald-100 text-emerald-800' : 'bg-gray-100 text-gray-600';
}
function renderInfoBadges(game, justifyClass = '') {
return `
<div class="flex flex-wrap items-center gap-1.5 ${justifyClass}">
${
game.hasItemCondition
? `<span class="rounded-full px-2 text-xs font-semibold leading-5 ${getItemConditionClass(
game.itemCondition,
)}">
${game.formattedItemCondition}
</span>`
: ''
}
<span class="rounded-full px-2 text-xs font-semibold leading-5 ${getEditionClass(
game.country,
)}">
${game.formattedEdition}
</span>
<span class="rounded-full px-2 text-xs font-semibold leading-5 ${getKoreanSupportClass(
game.hasKoreanSupport,
)}">
${game.formattedKoreanSupport}
</span>
</div>
`;
}
function createGameRow(game, index) {
@@ -622,40 +780,57 @@ document.addEventListener('DOMContentLoaded', () => {
tr.innerHTML = `
<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">
<img class="h-10 w-10 rounded-xl object-cover" src="${game.thumbnail}" alt="" />
</div>
<div class="ml-4">
<div class="font-medium text-gray-900 ${
game.status === 'sold' ? 'text-red-600 line-through' : ''
}">
${game.no}. ${game.formattedTitle}
<div class="ml-4 min-w-0 flex-1">
<div class="font-medium leading-5 text-gray-900 flex items-center gap-1">
${game.sale?.priceVerified ? `${VERIFIED_PRICE_MARK} ` : ''}<span>${game.formattedTitle}</span>
</div>
<div class="mt-2">
${renderInfoBadges(game)}
</div>
<div class="mt-1 max-w-lg text-xs leading-5 text-gray-500">
${game.formattedConfidenceDescription}
</div>
<div class="mt-3 space-y-2 sm:hidden">
<div class="flex flex-wrap items-center gap-x-3 gap-y-1">
<span class="text-base font-semibold leading-6 text-gray-900">
${game.formattedSuggestedPrice || '-'}
</span>
<span class="rounded-full px-2 text-xs font-semibold leading-5 ${getSaleStatusClass(
game.status,
)}">
${game.formattedSaleStatus}
</span>
</div>
<div class="text-xs leading-5 text-gray-500">
${game.formattedPricingBasis}
</div>
${
SHOW_RECOMMENDED_PRICE_RANGE_BY_DEFAULT
? `<div class="text-xs leading-5 text-gray-500">
${filterState.language === 'ko' ? '가격 범위' : '価格帯'}:
${game.formattedPriceRange || '-'}
</div>`
: ''
}
</div>
<div class="text-gray-500">${game.formattedReleaseDate}</div>
</div>
</div>
</td>
<td class="hidden w-3/12 px-3 py-4 text-sm text-gray-500 lg:table-cell">
<div class="text-gray-900">${game.formattedMaker}</div>
<div class="text-gray-500">${game.formattedLanguage}</div>
<td class="hidden w-56 px-3 py-4 text-sm text-gray-500 sm:table-cell">
<div class="font-semibold leading-5 text-gray-900">
${game.formattedSuggestedPrice || '-'}
</div>
<div class="mt-1 text-xs text-gray-400">${game.formattedPricingBasis}</div>
</td>
<td class="px-3 py-4 text-sm text-gray-500 mx-auto">
<div class="flex justify-center rounded-full px-2 text-xs font-semibold leading-5 ${getStatusClass(
<td class="hidden w-28 px-3 py-4 text-sm text-gray-500 mx-auto sm:table-cell">
<div class="flex justify-center whitespace-nowrap rounded-full px-2 text-xs font-semibold leading-5 ${getSaleStatusClass(
game.status,
)}">
${game.status}
</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}
${game.formattedSaleStatus}
</div>
</td>
`;
@@ -670,23 +845,188 @@ 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 => {
input.value = '';
});
// 게임 목록 다시 렌더링
renderGames();
}
// 필터 초기화 버튼 이벤트 리스너 설정
document.getElementById('resetFilters').addEventListener('click', resetFilters);
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 = [
document.getElementById('search-input'),
document.getElementById('mobile-search-input'),
].filter(Boolean);
const searchForms = [document.getElementById('searchForm')].filter(Boolean);
const resetSearchButtons = [
document.getElementById('reset-search'),
document.getElementById('mobile-reset-search'),
].filter(Boolean);
function updateSearchText(value) {
filterState.searchText = value.trim().toLowerCase();
searchInputs.forEach(input => {
if (input.value !== value) input.value = value;
});
renderGames();
}
window.updateNswSearch = value => {
updateSearchText(value || '');
};
window.clearNswSearch = () => {
updateSearchText('');
};
function updateSearchFromInput(input, options = {}) {
window.setTimeout(() => {
updateSearchText(input.value);
if (options.blur) input.blur();
}, 0);
}
function isSearchInput(target) {
return target instanceof HTMLInputElement && target.matches('[data-search-input]');
}
document.addEventListener(
'submit',
event => {
if (!event.target.querySelector?.('[data-search-input]')) return;
event.preventDefault();
if (event.stopImmediatePropagation) event.stopImmediatePropagation();
event.stopPropagation();
const searchInput = event.target.querySelector('[data-search-input]');
if (searchInput) updateSearchFromInput(searchInput, { blur: true });
},
true
);
document.addEventListener(
'input',
event => {
if (!isSearchInput(event.target)) return;
updateSearchText(event.target.value);
},
true
);
document.addEventListener(
'change',
event => {
if (!isSearchInput(event.target)) return;
updateSearchFromInput(event.target);
},
true
);
document.addEventListener(
'focusout',
event => {
if (!isSearchInput(event.target)) return;
updateSearchFromInput(event.target);
},
true
);
searchForms.forEach(form => {
form.addEventListener(
'submit',
event => {
event.preventDefault();
if (event.stopImmediatePropagation) event.stopImmediatePropagation();
event.stopPropagation();
const searchInput = form.querySelector('[data-search-input]');
if (searchInput) updateSearchFromInput(searchInput, { blur: true });
},
true
);
});
searchInputs.forEach(input => {
let isComposing = false;
input.addEventListener('compositionstart', () => {
isComposing = true;
});
input.addEventListener('blur', event => {
updateSearchFromInput(event.target);
});
input.addEventListener('compositionend', event => {
isComposing = false;
updateSearchFromInput(event.target);
});
input.addEventListener('keydown', event => {
if (event.key !== 'Enter') return;
if (isComposing || event.isComposing || event.keyCode === 229) return;
event.preventDefault();
updateSearchFromInput(event.target, { blur: true });
});
input.addEventListener('keyup', event => {
if (isComposing || event.isComposing || event.keyCode === 229) return;
updateSearchFromInput(event.target, { blur: event.key === 'Enter' });
});
});
resetSearchButtons.forEach(button => {
function clearSearch(event) {
event.preventDefault();
event.stopPropagation();
updateSearchText('');
}
button.addEventListener('pointerdown', clearSearch);
button.addEventListener('touchstart', clearSearch);
button.addEventListener('click', clearSearch);
});
// 언어 필터
document.querySelectorAll('input[name="language-filter"]').forEach(checkbox => {
checkbox.addEventListener('change', () => {
@@ -695,7 +1035,7 @@ document.addEventListener('DOMContentLoaded', () => {
});
});
// 상태 필터
// 판매 상태 필터
document.querySelectorAll('input[name="status-filter"]').forEach(checkbox => {
checkbox.addEventListener('change', () => {
updateFilters('status');
@@ -729,48 +1069,49 @@ document.addEventListener('DOMContentLoaded', () => {
// 필터링 함수
function filterGames(games) {
return games.filter(game => {
if (filterState.searchText) {
const searchableText = [
game.title,
game.koTitle,
game.maker,
game.tags,
game.country,
game.status,
String(game.sale?.suggestedPrice || ''),
]
.filter(Boolean)
.join(' ')
.toLowerCase();
if (!searchableText.includes(filterState.searchText)) return false;
}
// 언어 필터
if (filterState.filters.language.length > 0) {
const hasKorean = game.language.includes('韓国語');
const hasKorean = game.language?.includes('韓国語');
const hasKoreanFilter = filterState.filters.language.includes('koreanSupport');
const hasNotSupportedFilter = filterState.filters.language.includes('koreanNotSupport');
// 한국어 지원과 미지원이 모두 체크된 경우 모든 게임 표시
if (hasKoreanFilter && hasNotSupportedFilter) {
return true;
}
// 한국어 지원만 체크된 경우 한국어 지원 게임만 표시
if (hasKoreanFilter && !hasNotSupportedFilter) {
return hasKorean;
if (!hasKorean) return false;
}
// 한국어 미지원만 체크된 경우 한국어 미지원 게임만 표시
if (!hasKoreanFilter && hasNotSupportedFilter) {
return !hasKorean;
if (hasKorean) return false;
}
// 아무것도 체크되지 않은 경우 필터링하지 않음
return true;
}
// 상태 필터
if (filterState.filters.status.length > 0) {
const hasExtensionFilter = filterState.filters.status.includes('extension');
const hasOtherStatusFilters =
filterState.filters.status.filter(status => status !== 'extension').length > 0;
if (
filterState.filters.status.length > 0 &&
!filterState.filters.status.includes(game.status)
) {
return false;
}
// extension 필터가 체크된 경우
if (hasExtensionFilter) {
// extension 값이 null이 아닌 게임만 표시
if (game.extension === null) return false;
}
// 다른 상태 필터가 체크된 경우
if (hasOtherStatusFilters) {
const otherStatuses = filterState.filters.status.filter(status => status !== 'extension');
if (!otherStatuses.includes(game.status)) return false;
}
if (!isAdminFilterUnlocked && game.status !== 'available') {
return false;
}
// 국가 필터
@@ -793,43 +1134,39 @@ document.addEventListener('DOMContentLoaded', () => {
// 정렬 함수
function sortGames(games) {
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':
return [...games].sort((a, b) => {
const aDate = new Date(a.release.replace(/年|月/g, '/').replace(/日/g, '')).toUTCString();
const bDate = new Date(b.release.replace(/年|月/g, '/').replace(/日/g, '')).toUTCString();
const dateDiff = new Date(bDate) - new Date(aDate);
return dateDiff === 0 ? b.no - a.no : dateDiff;
return dateDiff === 0 ? compareTitles(a, b) : dateDiff;
});
case 'sortByDate':
return [...games].sort((a, b) => {
const aDate = new Date(a.release.replace(/年|月/g, '/').replace(/日/g, '')).toUTCString();
const bDate = new Date(b.release.replace(/年|月/g, '/').replace(/日/g, '')).toUTCString();
const dateDiff = new Date(aDate) - new Date(bDate);
return dateDiff === 0 ? b.no - a.no : dateDiff;
return dateDiff === 0 ? compareTitles(a, b) : dateDiff;
});
case 'sortByPurchaseDateDesc':
case 'sortByPriceDesc':
return [...games].sort((a, b) => {
if (!a.purchaseInformation?.date || !b.purchaseInformation?.date) {
return !a.purchaseInformation?.date ? 1 : -1;
}
const aDate = new Date(a.purchaseInformation.date.replace(/\./g, '/')).toUTCString();
const bDate = new Date(b.purchaseInformation.date.replace(/\./g, '/')).toUTCString();
const dateDiff = new Date(bDate) - new Date(aDate);
return dateDiff === 0 ? b.no - a.no : dateDiff;
const aPrice = getSuggestedPriceValue(a);
const bPrice = getSuggestedPriceValue(b);
if (aPrice === null && bPrice === null) return compareTitles(a, b);
if (aPrice === null) return 1;
if (bPrice === null) return -1;
const priceDiff = bPrice - aPrice;
return priceDiff === 0 ? compareTitles(a, b) : priceDiff;
});
case 'sortByPurchaseDate':
case 'sortByPrice':
return [...games].sort((a, b) => {
if (!a.purchaseInformation?.date || !b.purchaseInformation?.date) {
return !a.purchaseInformation?.date ? 1 : -1;
}
const aDate = new Date(a.purchaseInformation.date.replace(/\./g, '/')).toUTCString();
const bDate = new Date(b.purchaseInformation.date.replace(/\./g, '/')).toUTCString();
const dateDiff = new Date(aDate) - new Date(bDate);
return dateDiff === 0 ? b.no - a.no : dateDiff;
const aPrice = getSuggestedPriceValue(a);
const bPrice = getSuggestedPriceValue(b);
if (aPrice === null && bPrice === null) return compareTitles(a, b);
if (aPrice === null) return 1;
if (bPrice === null) return -1;
const priceDiff = aPrice - bPrice;
return priceDiff === 0 ? compareTitles(a, b) : priceDiff;
});
case 'sortByRandom':
return [...games].sort(() => Math.random() - 0.5);
@@ -838,6 +1175,10 @@ document.addEventListener('DOMContentLoaded', () => {
}
}
function compareTitles(a, b) {
return a.formattedTitle.localeCompare(b.formattedTitle, filterState.language === 'ko' ? 'ko' : 'ja');
}
// 정렬 UI 초기화
function setupSortUI() {
const sortButton = document.getElementById('sort-button');
@@ -925,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 = '';
@@ -936,6 +1284,8 @@ document.addEventListener('DOMContentLoaded', () => {
}
// 초기화
applyDefaultFilterCheckboxes();
setupAdminFilterUnlock();
setupFilterListeners();
setupSortUI();
updateUITexts();

View File

@@ -15,17 +15,12 @@
--color-yellow-800: oklch(47.6% 0.114 61.907);
--color-green-100: oklch(96.2% 0.044 156.743);
--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-800: oklch(42.4% 0.199 265.638);
--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-600: oklch(51.1% 0.262 276.966);
--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-gray-50: oklch(98.5% 0.002 247.839);
--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-400: oklch(70.7% 0.022 261.325);
--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-800: oklch(27.8% 0.033 256.848);
--color-gray-900: oklch(21% 0.034 264.665);
--color-black: #000;
--color-white: #fff;
--spacing: 0.25rem;
--container-xs: 20rem;
--container-lg: 32rem;
--container-2xl: 42rem;
--container-4xl: 56rem;
--container-7xl: 80rem;
@@ -61,9 +54,6 @@
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--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-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
--default-font-family: var(--font-sans);
@@ -240,24 +230,6 @@
.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 {
margin-inline: calc(var(--spacing) * -4);
}
@@ -276,6 +248,9 @@
.mt-2 {
margin-top: calc(var(--spacing) * 2);
}
.mt-3 {
margin-top: calc(var(--spacing) * 3);
}
.mt-4 {
margin-top: calc(var(--spacing) * 4);
}
@@ -288,9 +263,6 @@
.mt-16 {
margin-top: calc(var(--spacing) * 16);
}
.mr-1 {
margin-right: calc(var(--spacing) * 1);
}
.mb-4 {
margin-bottom: calc(var(--spacing) * 4);
}
@@ -303,6 +275,12 @@
.ml-4 {
margin-left: calc(var(--spacing) * 4);
}
.line-clamp-2 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.block {
display: block;
}
@@ -349,9 +327,6 @@
.max-h-96 {
max-height: calc(var(--spacing) * 96);
}
.w-3\/12 {
width: calc(3/12 * 100%);
}
.w-4 {
width: calc(var(--spacing) * 4);
}
@@ -361,12 +336,18 @@
.w-10 {
width: calc(var(--spacing) * 10);
}
.w-24 {
width: calc(var(--spacing) * 24);
.w-28 {
width: calc(var(--spacing) * 28);
}
.w-32 {
width: calc(var(--spacing) * 32);
}
.w-40 {
width: calc(var(--spacing) * 40);
}
.w-56 {
width: calc(var(--spacing) * 56);
}
.w-\[calc\(100\%-3rem\)\] {
width: calc(100% - 3rem);
}
@@ -379,6 +360,9 @@
.max-w-7xl {
max-width: var(--container-7xl);
}
.max-w-lg {
max-width: var(--container-lg);
}
.min-w-0 {
min-width: calc(var(--spacing) * 0);
}
@@ -391,21 +375,12 @@
.flex-shrink-0 {
flex-shrink: 0;
}
.flex-grow {
flex-grow: 1;
}
.rotate-180 {
rotate: 180deg;
}
.transform {
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;
}
.resize {
resize: both;
}
.grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
@@ -421,6 +396,9 @@
.items-center {
align-items: center;
}
.items-start {
align-items: flex-start;
}
.justify-between {
justify-content: space-between;
}
@@ -444,6 +422,9 @@
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 {
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)));
}
}
.gap-y-1 {
row-gap: calc(var(--spacing) * 1);
}
.gap-y-2 {
row-gap: calc(var(--spacing) * 2);
}
@@ -553,27 +537,18 @@
.bg-gray-50 {
background-color: var(--color-gray-50);
}
.bg-gray-100 {
background-color: var(--color-gray-100);
}
.bg-green-100 {
background-color: var(--color-green-100);
}
.bg-indigo-600 {
background-color: var(--color-indigo-600);
}
.bg-purple-100 {
background-color: var(--color-purple-100);
}
.bg-red-100 {
background-color: var(--color-red-100);
}
.bg-slate-50 {
background-color: var(--color-slate-50);
}
.bg-teal-100 {
background-color: var(--color-teal-100);
}
.bg-white {
background-color: var(--color-white);
}
@@ -606,9 +581,6 @@
.px-4 {
padding-inline: calc(var(--spacing) * 4);
}
.py-0\.5 {
padding-block: calc(var(--spacing) * 0.5);
}
.py-1 {
padding-block: calc(var(--spacing) * 1);
}
@@ -726,12 +698,12 @@
--tw-tracking: var(--tracking-tight);
letter-spacing: var(--tracking-tight);
}
.whitespace-nowrap {
white-space: nowrap;
}
.text-blue-800 {
color: var(--color-blue-800);
}
.text-gray-300 {
color: var(--color-gray-300);
}
.text-gray-400 {
color: var(--color-gray-400);
}
@@ -750,27 +722,18 @@
.text-indigo-600 {
color: var(--color-indigo-600);
}
.text-purple-800 {
color: var(--color-purple-800);
}
.text-red-600 {
color: var(--color-red-600);
}
.text-red-800 {
color: var(--color-red-800);
}
.text-teal-800 {
color: var(--color-teal-800);
}
.text-white {
color: var(--color-white);
}
.text-yellow-800 {
color: var(--color-yellow-800);
}
.line-through {
text-decoration-line: line-through;
}
.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));
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: 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-property: transform, translate, scale, rotate;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
@@ -970,6 +928,11 @@
margin-top: calc(var(--spacing) * 0);
}
}
.sm\:hidden {
@media (width >= 40rem) {
display: none;
}
}
.sm\:table-cell {
@media (width >= 40rem) {
display: table-cell;
@@ -1069,11 +1032,6 @@
display: none;
}
}
.lg\:table-cell {
@media (width >= 64rem) {
display: table-cell;
}
}
.lg\:max-h-none {
@media (width >= 64rem) {
max-height: none;