v4.0.0: 회원·덱 API, 인증 UI, 프로필 및 레이아웃 통일

Express/SQLite API와 Docker 구성을 추가하고 로그인·덱 저장·공개 덱·관리자·프로필 화면을 구현했다. 결과 화면 저장 UX, 헤더 프로필, 애드센스용 app-shell 레이아웃(1280px) 및 문서를 반영했다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-22 16:59:36 +09:00
parent f8c954f24b
commit dbf02241e2
56 changed files with 6732 additions and 183 deletions

View File

@@ -1,9 +1,10 @@
# 기술 명세
## 1. 프로젝트 개요
- 프로젝트명: `UADECK V3.1.0 - Union Arena Deck Builder`
- 형태: 정적 HTML/CSS/JavaScript 기반 덱 빌더
- 패키지 의존성: `tailwindcss`, `@tailwindcss/cli`
- 프로젝트명: `UADECK V4.0.0 - Union Arena Deck Builder`
- 형태: 정적 HTML/CSS/JavaScript 프론트 + Docker API 백엔드
- 패키지 의존성 (프론트): `tailwindcss`, `@tailwindcss/cli`
- 패키지 의존성 (API): `express`, `better-sqlite3`, `bcryptjs`, `jsonwebtoken`, `cors`
- 빌드 산출 스타일: `style/output.css`
## 2. 화면 구조
@@ -85,8 +86,10 @@
## 6. 상태 저장
- `localStorage.lang`: 현재 언어
- `localStorage.selectedCards`: 선택 카드 목록(JSON 문자열을 `encodeURIComponent` 처리)
- `localStorage.selectedSeries`: 일부 흐름에서 사용하는 선택 시리즈 값
- `localStorage.selectedCards`: 편집 중인 덱 (임시, `encodeURIComponent` JSON)
- `localStorage.selectedSeries`: 현재 시리즈 키
- `localStorage.uadeck_token` / `uadeck_user`: 로그인 세션
- 덱 공유·영구 저장: API + DB (`GET /api/v1/decks/:shareCode`)
## 7. 스타일 및 자산
- Tailwind 유틸리티 클래스와 페이지별 인라인 스타일을 혼용한다.
@@ -99,3 +102,157 @@
- `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/` 라우팅