Files
uniare.sori.studio/docs/spec.md
zenn a281d731a1 v4.0.0: UNIARE 리브랜딩·레이아웃·덱 UX 개선
제품명·헤더 로고 이미지 통일, 덱 이름 변경·조회 수 제거, 썸네일 홀드 미리보기를 누르는 동안만 표시하도록 수정하고 Tailwind CSS를 재빌드했다.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 11:19:07 +09:00

259 lines
9.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 기술 명세
## 1. 프로젝트 개요
- 프로젝트명: `UNIARE - Union Arena Deck Builder` (표시명 UNIARE, 내부 API 식별자 일부는 `uadeck` 레거시 유지)
- 형태: 정적 HTML/CSS/JavaScript 프론트 + Docker API 백엔드
- 패키지 의존성 (프론트): `tailwindcss`, `@tailwindcss/cli`
- 패키지 의존성 (API): `express`, `better-sqlite3`, `bcryptjs`, `jsonwebtoken`, `cors`
- 빌드 산출 스타일: `style/output.css`
## 2. 화면 구조
### `/index.html`
- 시리즈 선택 랜딩 페이지다.
- `i18n/translations.<lang>.json`을 직접 fetch하여 시리즈명과 소개 문구를 렌더링한다.
- 사용자가 시리즈를 선택하면 `cardList.html?series=<key>&seriesName=<name>&lang=<lang>`로 이동한다.
### `/cardList.html`
- 특정 시리즈의 카드 목록을 보여주고 덱을 편집한다.
- 쿼리스트링의 `series` 값을 기준으로 `./datas/${seriesKey}.js`를 동적 import 한다.
- 필터 기능:
- 검색어
- 색상(`All`, `Red`, `Blue`, `Green`, `Yellow`, `Purple`)
- 시리즈 하위 그룹
- 타입(`Basic`, `AP`, `Parallel`)
- 트리거(`All`, `Special`, `Color`, `Final`)
- 카드 수량 증감, 키 카드 지정, 초기화, 상세 오버레이, 결과 페이지 이동을 담당한다.
### `/result.html`
- 선택한 카드 목록을 미리보기하고 이미지로 저장한다.
- 스타일 모드:
- `type1`: 키 카드 강조형
- `type2`: 클래식 5열형
- `html-to-image` CDN을 사용해 덱 이미지를 PNG로 저장한다.
## 3. 데이터 구조
### 카드 데이터 파일
- 위치: `datas/*.js`
- 파일 수: 현재 `52개`
- 각 파일은 `export const cardList = [...]` 형태를 사용한다.
### 카드 객체 스키마
```js
{
imgSrc: '/ua24/UA24BT_SHY-1-001.png',
Number: 'UA24BT/SHY-1-001',
count: 0,
key: false,
color: 'Red',
parallel: false,
rarity: 'C',
trigger: null,
}
```
### 필드 의미
- `imgSrc`: 원본 상대 식별자. 실제 렌더링 시 `images/${seriesKey}/${fileName}` 형태로 재구성한다.
- `Number`: 카드 번호. 동일 카드 판본 묶음과 필터 기준으로 사용한다.
- `count`: 현재 덱에 담긴 수량.
- `key`: 키 카드 여부.
- `color`: 카드 색상 또는 일부 특수 분류 값.
- `parallel`: 패러렐 여부.
- `rarity`: 희귀도 표기.
- `trigger`: `null`, `COLOR`, `FINAL`, `SPECIAL` 등 트리거 정보.
- `maxCount`: 일부 카드만 선택적으로 사용하며 기본값은 `4`로 동작한다.
## 4. 이미지 규칙
- 기본 위치: `images/<seriesKey>/`
- 저장 정책: `images/**`는 `Git LFS` 대상으로 관리한다.
- 렌더링 규칙:
- 카드 목록 화면: `images/${seriesKey}/${fileName}`
- 결과 화면: `/images/${seriesKey}/${fileName}`
- 폴백 이미지: `/images/uapr/comingsoon.png`
- 현재 용량 현황:
- `images/` 약 `1.7G`
- 이미지 파일 약 `9,370개`
## 5. 다국어
- 지원 언어: `ko`, `ja`, `en`
- 번역 파일:
- `i18n/translations.ko.json`
- `i18n/translations.ja.json`
- `i18n/translations.en.json`
- `index.html`은 번역 JSON을 직접 읽는다.
- 공통 텍스트 치환은 `i18n/i18n.js`의 `data-i18n` 기반 로직을 사용한다.
## 6. 상태 저장
- `localStorage.lang`: 현재 언어
- `localStorage.selectedCards`: 편집 중인 덱 (임시, `encodeURIComponent` JSON)
- `localStorage.selectedSeries`: 현재 시리즈 키
- `localStorage.uadeck_token` / `uadeck_user`: 로그인 세션
- 덱 공유·영구 저장: API + DB (`GET /api/v1/decks/:shareCode`)
## 7. 스타일 및 자산
- Tailwind 유틸리티 클래스와 페이지별 인라인 스타일을 혼용한다.
- 공통 스타일 파일:
- `style/output.css`
- `style/style.css`
- `style/navigation.css`
## 8. 알려진 제약
- `cardList.html`과 `result.html`의 스크립트가 페이지 내부에 크게 포함되어 있어 모듈 분리 전까지는 변경 영향 범위가 넓다.
- `script/navigation.js`의 일부 이동 대상 페이지는 현재 저장소에 존재하지 않는다.
- 이미지가 Git 일반 객체로 계속 누적되면 저장소 히스토리 크기가 지속 증가한다.
---
## 9. v4.0.0 계획 — 회원·덱 API·공유
> 상태: **v4.0.0 1차 구현 완료** (2026-06-19)
### 9.1 아키텍처 개요
- 프론트: 기존 정적 HTML/JS 유지. 헤더에 프로필·인증 UI 추가.
- 백엔드: Docker 컨테이너 API 서버 (NAS 배포).
- 카드 DB: `datas/*.js` + `images/` 정적 유지 (변경 없음).
- 사용자·덱 데이터: API + SQLite(또는 PostgreSQL) DB.
- 외부 연동(옆 프로젝트): `GET /api/v1/decks/:shareCode` 단일 계약.
### 9.2 덱 공유 방식 (확정)
기존 `?cards=<JSON>` URL 방식은 **폐기**한다. 공유는 공유 코드 + API만 사용한다.
| 종류 | `share_type` | 코드 생성 | URL 예시 | 설명 |
|---|---|---|---|---|
| 개인 덱 | `user` | nanoid 8자 (저장마다 고유) | `/d/x7Kp9m2Q` | 같은 카드 구성이라도 저장자마다 URL이 다름 |
| 메타 덱·프리셋 | `preset` | 내용 해시 8자 (동일 구성 = 동일 URL) | `/d/a7f3b2c1` | 관리자 등록 공식 템플릿, 중복 저장 없음 |
브라우저 진입:
- `/d/:shareCode` → `cardList.html?d=:shareCode` 리다이렉트 또는 동일 페이지에서 API 로드
- `cardList.html`은 `?d=` 파라미터가 있으면 API에서 덱 복원
### 9.3 DB 저장 vs API 응답
**DB에 저장하는 최소 덱 본문 (`cards_json`):**
```json
[
{ "number": "UA36BT/MCR-1-052", "count": 4, "key": true },
{ "number": "EX14BT/MCR-2-011", "count": 2, "key": false }
]
```
**API 응답 시 서버가 `datas/<series>.js`를 lookup하여 풀 카드 정보를 합성한다.**
```json
{
"v": 1,
"shareCode": "x7Kp9m2Q",
"shareType": "user",
"series": "ua36",
"seriesName": "마크로스",
"name": "블루 aggro",
"totalCards": 50,
"cards": [
{
"number": "UA36BT/MCR-1-052",
"count": 4,
"key": true,
"imgSrc": "/ua36/UA36BT_MCR-1-052.png",
"color": "Blue",
"rarity": "SR",
"trigger": null,
"parallel": false
}
],
"author": {
"id": 12,
"displayName": "닉네임",
"avatarUrl": "/uploads/avatars/12.png"
},
"createdAt": "2026-06-19T12:00:00Z",
"updatedAt": "2026-06-19T14:30:00Z",
"viewCount": 42,
"isPublic": true
}
```
비회원 임시 공유: `author`는 `null`, `expiresAt` 필드로 만료 관리.
### 9.4 DB 스키마 (초안)
#### `users`
| 컬럼 | 타입 | 설명 |
|---|---|---|
| `id` | INTEGER PK | |
| `email` | TEXT UNIQUE | 로그인 ID |
| `username` | TEXT UNIQUE | 표시용 닉네임 |
| `password_hash` | TEXT | bcrypt |
| `display_name` | TEXT | 프로필 이름 |
| `avatar_url` | TEXT | 프로필 썸네일 |
| `default_deck_public` | INTEGER | 덱 저장 기본 공개 여부 (1=공개, 0=비공개) |
| `skip_deck_public_prompt` | INTEGER | 저장 시 공개 확인 생략 (1=생략) |
| `role` | TEXT | `user` \| `admin` |
| `is_active` | INTEGER | 0=비활성, 1=활성 |
| `created_at` | TEXT | 가입일 |
| `updated_at` | TEXT | |
#### `decks`
| 컬럼 | 타입 | 설명 |
|---|---|---|
| `id` | INTEGER PK | |
| `share_code` | TEXT UNIQUE | URL에 쓰이는 8자 코드 |
| `share_type` | TEXT | `user` \| `preset` |
| `content_hash` | TEXT | preset 타입용. user 타입은 null 가능 |
| `user_id` | INTEGER FK | null=비회원 임시 공유 |
| `series` | TEXT | 예: `ua36` |
| `name` | TEXT | 덱 이름 |
| `cards_json` | TEXT | `[{number,count,key}]` JSON |
| `is_public` | INTEGER | 0=비공개, 1=공개 |
| `view_count` | INTEGER | 조회수 |
| `expires_at` | TEXT | null=만료 없음 |
| `created_at` | TEXT | 생성일 |
| `updated_at` | TEXT | 최종 수정일 |
덱 1건 용량 추정: 카드 50장 × 약 60바이트 ≈ 3KB. 수만 건이어도 DB 부담은 경미하다.
### 9.5 API 엔드포인트 (초안)
#### 인증
| Method | Path | 설명 |
|---|---|---|
| POST | `/api/v1/auth/register` | 회원가입 |
| POST | `/api/v1/auth/login` | 로그인 → JWT |
| GET | `/api/v1/auth/me` | 내 정보 |
| PUT | `/api/v1/auth/me` | 프로필 수정 (이메일, 사용자명, 아바타, `defaultDeckPublic`, `skipDeckPublicPrompt`) |
| POST | `/api/v1/auth/me/password` | 비밀번호 변경 |
| GET | `/api/v1/auth/avatar/:userId` | 프로필 이미지 (PNG) |
#### 덱
| Method | Path | 설명 |
|---|---|---|
| GET | `/api/v1/decks/:shareCode` | 덱 조회 (외부 연동 공식 계약) |
| GET | `/api/v1/decks` | 내 덱 목록 (인증 필요) |
| POST | `/api/v1/decks` | 덱 저장·공유 코드 발급 |
| PUT | `/api/v1/decks/:shareCode` | 덱 수정 (소유자만) |
| DELETE | `/api/v1/decks/:shareCode` | 덱 삭제 (소유자만) |
#### 관리자
| Method | Path | 설명 |
|---|---|---|
| GET | `/api/v1/admin/users` | 회원 목록 |
| GET | `/api/v1/admin/users/:id` | 회원 상세 |
| PUT | `/api/v1/admin/users/:id` | 회원 정보·역할·활성 수정 |
| POST | `/api/v1/admin/users/:id/reset-password` | 비밀번호 초기화 |
| POST | `/api/v1/admin/decks/preset` | 메타 덱(프리셋) 등록 |
### 9.6 프론트 변경 (예정)
- `script/auth-header.js`: 헤더 프로필 드롭다운 (로그인/회원가입, 내 덱 관리)
- `login.html`, `signup.html`, `deckHistory.html`, `admin.html` 신규
- `cardList.html`: `?cards=` 제거, `?d=` API 로드로 전환
- `localStorage.selectedCards`: 편집 중 임시 저장만 (공유와 분리)
### 9.7 배포
- `docker-compose.yml`: API만 (기존 웹서버 사용 시)
- `docker-compose.prod.yml`: Nginx + API 풀스택 (NAS 권장)
- `docs/deploy.md`: NAS 배포 상세 가이드
- `docs/api-integration.md`: 옆 프로젝트 연동 가이드
- `integrations/uadeck-deck-client.js`: 연동용 JS 클라이언트
- `nginx/default.conf`: `/api/`, `/d/` 라우팅