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

174
docs/api-integration.md Normal file
View File

@@ -0,0 +1,174 @@
# 옆 프로젝트 API 연동 가이드
UADECK v4부터 덱 데이터는 **공유 코드 + REST API**로 제공합니다.
URL 쿼리(`?cards=`) 방식은 사용하지 않습니다.
## 공식 엔드포인트
```http
GET https://uniare.sori.studio/api/v1/decks/:shareCode
```
### 응답 예시
```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": 1,
"displayName": "닉네임",
"avatarUrl": null
},
"createdAt": "2026-06-19T12:00:00Z",
"updatedAt": "2026-06-19T12:00:00Z",
"viewCount": 42,
"isPublic": true
}
```
## shareCode 얻는 방법
| 방법 | 예시 |
|---|---|
| UADECK 공유 버튼 | `https://uniare.sori.studio/d/x7Kp9m2Q` |
| URL 파라미터 | `cardList.html?d=x7Kp9m2Q` |
| 코드만 | `x7Kp9m2Q` |
## 클라이언트 라이브러리
프로젝트에 포함된 파일:
```
integrations/uadeck-deck-client.js
```
### 브라우저
```html
<script src="https://uniare.sori.studio/integrations/uadeck-deck-client.js"></script>
<script>
const client = new UaDeckDeckClient({
baseUrl: 'https://uniare.sori.studio/api/v1',
imageBase: 'https://uniare.sori.studio',
});
// 공유 링크에서 덱 로드
const deck = await client.fetchFromUrl('https://uniare.sori.studio/d/x7Kp9m2Q');
deck.cards.forEach((card) => {
console.log(card.number, card.count, card.imageUrl);
});
</script>
```
### Node.js / ESM
```javascript
import { UaDeckDeckClient } from './integrations/uadeck-deck-client.js';
const client = new UaDeckDeckClient({
baseUrl: 'https://uniare.sori.studio/api/v1',
});
const deck = await client.fetchDeck('x7Kp9m2Q');
```
### fetch 직접 사용
```javascript
const res = await fetch('https://uniare.sori.studio/api/v1/decks/x7Kp9m2Q');
const deck = await res.json();
if (!res.ok) throw new Error(deck.error);
```
## 이미지 URL 규칙
API의 `imgSrc`는 상대 경로입니다. 전체 URL:
```
https://uniare.sori.studio/images{imgSrc}
```
예: `imgSrc: "/ua36/UA36BT_MCR-1-052.png"`
→ `https://uniare.sori.studio/images/ua36/UA36BT_MCR-1-052.png`
`UaDeckDeckClient`는 `card.imageUrl`에 자동 변환합니다.
## CORS
다른 도메인에서 브라우저 `fetch`를 쓰려면 UADECK 서버 `.env`의 `CORS_ORIGINS`에 해당 origin을 추가하세요.
```
CORS_ORIGINS=https://your-app.sori.studio,https://uniare.sori.studio
```
서버 간 호출(Node, Python 등)은 CORS가 필요 없습니다.
## 마이그레이션 (기존 ?cards= 코드)
**Before (폐기):**
```javascript
const params = new URLSearchParams(location.search);
const cards = JSON.parse(decodeURIComponent(params.get('cards')));
```
**After:**
```javascript
const code = UaDeckDeckClient.parseShareCode(location.href)
|| new URLSearchParams(location.search).get('d');
const client = new UaDeckDeckClient({ baseUrl: '/api/v1' });
const deck = await client.fetchDeck(code);
const cards = deck.cards;
```
## 프리셋(메타 덱) 목록
```http
GET /api/v1/decks/presets
```
```json
{
"presets": [
{
"share_code": "a7f3b2c1",
"series": "ua36",
"series_name": "마크로스",
"name": "공식 블루 템플릿",
"view_count": 120
}
]
}
```
프리셋도 동일하게 `GET /decks/:shareCode`로 상세 조회합니다.
## 에러 코드
| HTTP | 의미 |
|---|---|
| 404 | shareCode 없음 |
| 403 | 비공개 덱 |
| 410 | 만료된 임시 공유 (비회원 30일) |

View File

@@ -5,12 +5,12 @@
- 특히 이 저장소는 정적 HTML/JS 프로젝트이면서 카드 데이터와 이미지 자산 비중이 크기 때문에, "코드 수정"보다 "데이터-번역-이미지 일치"가 더 중요하다.
## 현재 기준 릴리즈
- 현재 작업 기준 버전: `v3.1.0`
- 현재 작업 기준 버전: `v4.0.0`
- 이번 릴리즈 핵심 반영:
- `ua51` 시리즈 추가
- `ua49` 누락 병렬 이미지 보강
- 다국어 시리즈명 추가
- 버전 표기 및 문서 체계 정리
- 회원·인증·덱 API (`api/`, Docker)
- 공유 코드 기반 덱 공유 (`/d/:code`, `GET /api/v1/decks/:shareCode`)
- 관리자 화면 (`admin.html`)
- `?cards=` URL 방식 폐기
## 다음 작업자에게 반드시 전달할 것
- 이 프로젝트는 빌드 시스템 중심 앱이 아니라 `index.html`, `cardList.html`, `result.html` 3개 페이지가 직접 동작하는 정적 사이트다.

View File

@@ -24,6 +24,7 @@
## CSS 규칙
- 공통 UI는 가능한 한 Tailwind 유틸리티 클래스를 우선 사용한다.
- `style/output.css`에 포함되지 않은 유틸리티 클래스는 적용되지 않으므로, 페이지 특화 스타일은 인라인 `<style>` 또는 전용 CSS 파일(`style/auth-pages.css` 등)에 명시한다.
- 페이지 특화 보정 스타일만 인라인 `<style>` 또는 전용 CSS 파일에 둔다.
- 캡처 안정성처럼 이유가 분명한 예외 스타일은 주석으로 목적을 남긴다.

192
docs/deploy.md Normal file
View File

@@ -0,0 +1,192 @@
# NAS 배포 가이드 (UADECK v4)
## 1. 사전 준비
- Ugreen NAS에 Docker(또는 Container Manager) 설치
- 프로젝트를 NAS에 클론 또는 동기화
- `Git LFS`로 이미지 자산 pull 완료 확인
## 2. 환경변수 설정
```bash
cp .env.example .env
```
| 변수 | 설명 |
|---|---|
| `JWT_SECRET` | 32자 이상 랜덤 문자열 (필수) |
| `ADMIN_EMAIL` | 최초 관리자 이메일 |
| `ADMIN_PASSWORD` | 최초 관리자 비밀번호 (8자+) |
| `WEB_PORT` | 웹 노출 포트 (기본 `8080`) |
| `CORS_ORIGINS` | 옆 프로젝트 도메인 포함, 쉼표 구분 |
예시:
```env
JWT_SECRET=your-random-secret-at-least-32-chars
ADMIN_EMAIL=admin@uniare.sori.studio
ADMIN_PASSWORD=your-secure-password
WEB_PORT=8080
CORS_ORIGINS=https://uniare.sori.studio,https://your-other-app.sori.studio,http://localhost:8080,http://localhost:5500,http://127.0.0.1:5500
```
## 3. 로컬 테스트
> **한 줄 요약:** 프로젝트 루트에서 `npm start` → 브라우저 **http://localhost:8080**
회원가입·로그인·덱 저장은 **API 서버**가 떠 있어야 합니다.
HTML만 Live Server 등으로 열면 저장·로그인이 **405** 로 실패합니다.
### 빠른 시작 (권장)
```bash
npm start # 백그라운드 실행 (터미널 점유 안 함)
# 브라우저: http://localhost:8080
npm run logs # 로그 보기 (종료: Ctrl+C)
npm stop # 서버 종료
```
| 명령 | 설명 |
|---|---|
| `npm start` | Docker로 웹+API 백그라운드 기동 (**일상 개발용**) |
| `npm stop` | 서버 종료 |
| `npm run logs` | API·Nginx 로그 스트리밍 |
| `npm run dev` | 포그라운드 실행 (로그 붙어 있음, `Ctrl+C`로 **서버도 종료**) |
| `npm run api:dev` | API만 Node로 실행 (Live Server 병행 시) |
### 저장이 안 될 때
1. `curl http://localhost:8080/api/v1/health` → `{"ok":true,...}` 가 나와야 함
2. 안 나오면 `npm start` 다시 실행
3. `npm run dev` 후 `Healthy`에서 멈춘 것처럼 보여도 **정상**이지만, `Ctrl+C`하면 서버가 꺼짐 → **`npm start` 사용 권장**
4. Live Server(`:5500`)로 HTML만 연 경우 → 아래 방법 B 또는 **반드시 `:8080` 사용**
### 방법 A — Docker (`npm start`와 동일)
```bash
npm start
```
브라우저: **http://localhost:8080** (Live Server 포트가 아님)
### 방법 B — API만 로컬 실행 (Live Server / Cursor 미리보기 사용 시)
터미널 1 — API:
```bash
cd api && npm install # 최초 1회
cd .. && npm run api:dev
```
터미널 2 — HTML은 Live Server 등으로 열기 (예: `:5500`)
`.env`의 `CORS_ORIGINS`에 Live Server 포트를 추가하세요:
```env
CORS_ORIGINS=...,http://localhost:5500,http://127.0.0.1:5500
```
확인:
```bash
curl http://localhost:3000/api/v1/health
# → {"ok":true,"version":"4.0.0"}
```
### 405 오류가 나올 때
| 원인 | 해결 |
|---|---|
| API 미실행 | `npm start` (또는 `npm run dev`) |
| Live Server로 HTML만 연 경우 | POST `/api/v1/...`가 정적 서버로 가서 405 발생 → 방법 A 또는 B 사용 |
| Docker 미기동 | `docker ps`로 `uadeck-api`, `uadeck-web` 확인 |
## 4. Docker 기동 (NAS / 프로덕션)
```bash
# 프로덕션 스택 (Nginx + API)
docker compose -f docker-compose.prod.yml up -d --build
# API만 단독 (기존 웹서버가 정적 파일 서빙 중일 때)
docker compose up -d --build
```
확인:
```bash
curl http://localhost:8080/api/v1/health
# → {"ok":true,"version":"4.0.0"}
```
## 5. 기존 리버스 프록시 연동
이미 NAS에서 Nginx Proxy Manager, Traefik, Caddy 등을 쓰는 경우:
### 옵션 A — `uadeck-web` 포트만 프록시 (권장)
```
https://uniare.sori.studio → http://NAS_IP:8080
```
`uadeck-web` 컨테이너가 `/api/`와 정적 파일을 모두 처리합니다.
### 옵션 B — 기존 정적 호스팅 + API만 Docker
1. `docker compose up -d` (API만, 포트 3000)
2. 기존 웹서버에 `nginx/uadeck.conf` 규칙 추가:
```nginx
location ~ ^/d/([A-Za-z0-9]+)$ {
return 302 /cardList.html?d=$1;
}
location /api/ {
proxy_pass http://127.0.0.1:3000;
# proxy_set_header ... (uadeck.conf 참고)
}
```
## 6. 최초 관리자 로그인
1. `https://uniare.sori.studio/login.html` 접속
2. `.env`의 `ADMIN_EMAIL` 계정으로 로그인
- API 최초 기동 시 해당 이메일이 없으면 `ADMIN_PASSWORD`로 계정이 자동 생성된다.
- **이미 `ADMIN_EMAIL`로 회원가입한 경우** API 재시작 시 해당 계정이 `admin`으로 승격된다. (가입 시 설정한 비밀번호 그대로 사용)
3. 프로필 메뉴 → **관리자** → `/admin.html`
> 관리자 메뉴가 안 보이면 **로그아웃 후 다시 로그인**하세요. 브라우저에 이전 `role`이 캐시되어 있을 수 있습니다.
## 7. 운영 명령
```bash
# 로그
docker compose -f docker-compose.prod.yml logs -f uadeck-api
# 재시작
docker compose -f docker-compose.prod.yml restart
# DB 백업 (볼륨 내 SQLite)
docker cp uadeck-api:/data/uadeck.db ./backup/uadeck-$(date +%Y%m%d).db
```
## 8. 카드/이미지 업데이트
`datas/`, `images/`, HTML 파일은 **볼륨 마운트**이므로:
1. Git pull (또는 파일 동기화)
2. **재빌드 불필요** — 브라우저 새로고침만으로 반영
API 컨테이너는 `datas/`를 read-only 마운트하므로 새 시리즈 파일 추가 후 API 재시작만 하면 됩니다:
```bash
docker compose -f docker-compose.prod.yml restart uadeck-api
```
## 9. 트러블슈팅
| 증상 | 확인 |
|---|---|
| 로그인 안 됨 | `JWT_SECRET` 설정, API health 확인 |
| 공유 링크 404 | Nginx `/d/` 리다이렉트 규칙 |
| CORS 오류 | `CORS_ORIGINS`에 옆 프로젝트 도메인 추가 |
| 덱 카드 메타 누락 | `datas/<series>.js` 존재 여부, API `DATAS_PATH` |

View File

@@ -1,5 +1,25 @@
# 의사결정 이력
## 2026-06-19 / v4.0.0 구현 완료
- 배경: 설계 확정 후 회원·API·공유 시스템 1차 구현.
- 반영: `api/` 백엔드, Docker Compose, 인증/덱/관리자 API, 프론트 신규 페이지 및 헤더 프로필 UI.
- 배포: NAS Docker + Nginx `/api/`, `/d/` 프록시 필요.
## 2026-06-19 / v4.0.0 덱 공유·API 설계 확정
- 배경: v3는 덱을 `?cards=<JSON>` URL에 실어 공유했으나 URL이 수천 자로 비대해지고, 옆 프로젝트도 개발 중이라 계약을 새로 잡을 수 있는 시점이다.
- 결정:
- URL 쿼리 기반 덱 공유는 **폐기**하고, 공유 코드 + REST API 방식으로 전환한다.
- **개인 덱**(`share_type=user`): 저장할 때마다 nanoid 8자 고유 코드 발급. 같은 카드 구성이라도 저장자마다 URL이 다르다.
- **메타 덱·프리셋**(`share_type=preset`): 카드 구성 해시 기반 고정 코드. 동일 구성이면 항상 같은 URL.
- DB에는 `{number, count, key}`만 저장하고, API 응답 시 `datas/`에서 카드 메타를 lookup하여 합성한다.
- 작성자, 생성일, 수정일, 조회수, 공개 여부 등 메타데이터를 덱 레코드와 함께 관리한다.
- 외부 연동(옆 프로젝트) 공식 계약: `GET /api/v1/decks/:shareCode`
- 프론트는 Vue 전환 없이 정적 HTML + API 호출 + Docker 백엔드로 확장한다.
- 이유:
- URL 길이·중복 데이터 문제 해소.
- 회원 덱 소유권·수정·비공개 설정이 자연스럽다.
- DB 용량은 덱 본문 기준 건당 수 KB 수준으로 경미하다.
## 2026-04-19 / v3.1.0 릴리즈 기준 확정
- 배경: 저장소의 마지막 명시 릴리즈 기록은 `v3.0.1`인데, 메인 화면은 이미 `v3.1`을 노출하고 있어 실제 릴리즈 이력과 화면 표기가 어긋나 있었다.
- 결정:

View File

@@ -3,48 +3,51 @@
## `/`
- 진입 파일: `index.html`
- 화면 역할: 시리즈 선택 홈 화면
- 사용자 행동:
- 언어 선택
- 시리즈 카드 클릭
- 연결 파일:
- `i18n/translations.<lang>.json`: 제목, 설명, 시리즈명
- `script/navigation.js`: 공통 네비게이션 오버레이
- `style/output.css`: 공통 스타일
- 연결: `script/auth-header.js`, `script/api.js`
## `/cardList.html?series=<seriesKey>`
## `/cardList.html?series=<key>&d=<shareCode>`
- 진입 파일: `cardList.html`
- 화면 역할: 카드 검색, 필터링, 수량 선택, 키 카드 지정
- 사용자 행동:
- 검색어 입력
- 색상/타입/트리거 필터
- 카드 수량 조절
- 키 카드 지정
- 덱 결과 페이지 이동
- 연결 파일:
- `datas/<seriesKey>.js`: 현재 시리즈 카드 원본 데이터
- `images/<seriesKey>/`: 카드 이미지 원본
- `i18n/translations.<lang>.json`: 버튼/문구 번역
- `script/navigation.js`: 공통 네비게이션
- 화면 역할: 카드 검색·필터·덱 편집·저장·공유
- `?d=` 있으면 API에서 덱 복원
- 연결: `datas/<seriesKey>.js`, `script/api.js`
## `/result.html`
- 진입 파일: `result.html`
- 화면 역할: 선택한 덱 미리보기 및 이미지 저장
- 사용자 행동:
- 덱 이름 입력
- 출력 스타일 선택
- 고화질 저장 토글
- PNG 다운로드
- 연결 파일:
- `localStorage.selectedCards` 또는 `cards` 쿼리스트링: 덱 데이터 소스
- `images/<seriesKey>/`: 결과 이미지 렌더링 대상
- `i18n/translations.<lang>.json`: 결과 화면 번역
- 화면 역할: 덱 미리보기·PNG 저장·내 덱 저장
- 데이터 소스: `localStorage.selectedCards`
- 하단 고정 바: `홈으로` | `이미지로 다운로드` | `내 덱으로 저장` (공개 여부 확인 모달)
- `script/deck-prefs.js`: 덱 공개 기본값·프롬프트 설정
## 공통 보조 파일
- `script/navigation.js`: 오버레이 메뉴 생성 및 이동 처리
- `i18n/i18n.js`: `data-i18n` 기반 공통 번역 적용
- `style/style.css`, `style/navigation.css`, `style/output.css`: 공통/페이지 스타일
## `/d/<shareCode>` (Nginx 리다이렉트)
- `nginx/uadeck.conf` 규칙으로 `cardList.html?d=` 로 이동
- 옆 프로젝트 연동: `GET /api/v1/decks/<shareCode>`
## 초보자용 이해 포인트
- 이 프로젝트는 프레임워크 앱이 아니라 HTML 파일 3개가 직접 화면 역할을 나눠 갖는 구조다.
- 카드 정보는 `datas/`에, 실제 카드 이미지는 `images/`에 따로 있다.
- 화면에서 보이는 한 장의 카드는 "데이터 파일의 객체 1개 + 이미지 폴더의 파일 1개" 조합으로 만들어진다.
## `/login.html`, `/signup.html`
- 인증 화면
## `/profile.html` (내 프로필)
- 로그인 사용자: 이메일·사용자명·프로필 사진·비밀번호 변경
- 연결: `script/api.js` (`updateProfile`, `changePassword`), `script/image-utils.js`, `style/profile.css`
## `/deckHistory.html`
- 로그인 사용자의 저장 덱 목록·불러오기·삭제
- 카드 그리드 + 미리보기 썸네일 (`preview_url`)
- 시리즈 필터·검색 (`style/deck-history.css`)
## `/preset.html` (공개 덱)
- 사용자가 공개 전환한 덱 목록 (카드 그리드)
- API: `GET /api/v1/decks/presets` → `is_public=1` user 덱
## `/admin.html`
- 관리자: 회원 관리, 덱 관리 (`style/admin.css`)
## API (`api/`)
- Docker: `docker-compose.yml`
- 엔드포인트 베이스: `/api/v1`
## 공통
- `style/app-shell.css`: 헤더·본문 최대 너비 1280px (`max-w-7xl`), 데스크톱 우측 광고 레일
- `script/api.js`: API 클라이언트 (`updateProfile`, `changePassword` 포함)
- `script/auth-header.js`: 헤더 프로필 드롭다운 (내 프로필, 내 덱 관리, 공개 덱), 로그인 시 아바타 옆 사용자명 표시
- `script/image-utils.js`: 클라이언트 이미지 PNG 리사이즈 (`UaImageUtils.resizeImageToPng`)
- `style/auth-header.css`

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/` 라우팅

View File

@@ -7,12 +7,36 @@
- `cardList.html`과 `result.html` 내부 스크립트 비중이 커서 기능 확장 시 유지보수 난도가 올라갈 수 있다.
- 데이터 파일 네이밍과 샘플 파일(`datas/ua99 sample.js`, `datas/ua99 sample mini.js`) 처리 기준이 문서상 명확하지 않았다.
## v4.0.0 구현 완료 항목
- [x] Docker API 서버 + DB (`users`, `decks` 테이블)
- [x] 인증: 회원가입, 로그인, JWT, 헤더 프로필 드롭다운
- [x] 덱 API: 저장, 조회, 공유 코드 (`GET /api/v1/decks/:shareCode`)
- [x] `cardList.html`: `?d=` API 로드, 저장·공유 버튼
- [x] `deckHistory.html`, `login.html`, `signup.html`, `preset.html`
- [x] `admin.html`: 회원 목록·수정, 비밀번호 초기화
- [x] `index.html` 버전 `v4.0.0`
## v4 후속 작업
- [ ] NAS에 `docker compose -f docker-compose.prod.yml up -d` 배포
- [ ] `.env` 설정 및 기존 리버스 프록시 연동
- [ ] 옆 프로젝트에 `integrations/uadeck-deck-client.js` 적용
- [ ] `cardList.html` 인라인 스크립트 모듈 분리
- [ ] i18n 번역 키 추가 (auth, admin 문구)
## v4.0.0 최근 완료
- [x] `profile.html`: 일반 사용자 프로필 화면 (아바타·닉네임·이메일·비밀번호)
- [x] 헤더 드롭다운「내 프로필」링크
- [x] 아바타 업로드 시 PNG 리사이즈 (`script/image-utils.js`) — JPEG 등 형식 깨짐 방지
- [x] 아바타 URL `updatedAt` 캐시 무효화
- [x] 내 덱 관리: `/d/코드` 경로 표시 제거
- [x] `result.html` 하단 버튼 개편 + 공개 확인 모달 + 홈 이동
- [x] 프로필·API: 덱 저장 공개 기본값 설정
## 다음 작업 제안
- 원격 저장소에서 `Git LFS` 업로드가 정상 완료되었는지 확인한다.
- 협업자가 있다면 히스토리 재작성 이후 동기화 방법을 공지한다.
- `Git LFS` 도입 후에도 저장소 운영 비용이 크면 이미지 저장소 분리/CDN 이전을 재검토한다.
- `cardList.html`의 인라인 스크립트를 별도 JS 모듈로 분리하는 리팩터링을 검토한다.
- 존재하지 않는 페이지 링크를 정리하거나, 향후 구현 예정이면 문서에 상태를 명확히 표기한다.
## 판단 가이드
- 신규 이미지 추가 관리가 목적이면 `Git LFS` 추적만으로도 충분하다.

View File

@@ -1,5 +1,149 @@
# 업데이트 이력
## 2026-06-19 / v4.0.0
### 수행 작업
- v4.0.0 회원·덱 API·공유 시스템 1차 구현
- `api/` Express + SQLite 백엔드 (인증, 덱 CRUD, 관리자 API)
- `docker-compose.yml`, `.env.example`, `nginx/uadeck.conf` 추가
- 프론트: `script/api.js`, `script/auth-header.js`, `style/auth-header.css`
- 신규 페이지: `login.html`, `signup.html`, `deckHistory.html`, `preset.html`, `admin.html`
- `index.html` v4.0.0 + 프로필 헤더
- `cardList.html`: `?cards=` 제거, `?d=` API 로드, 저장·공유 버튼
- `result.html`: localStorage 기반, 프로필 헤더
- `docs/spec.md` §9 API·DB 명세 반영
### 배포 방법
1. `.env.example`을 `.env`로 복사 후 `JWT_SECRET`, `ADMIN_EMAIL`, `ADMIN_PASSWORD` 설정
2. `docker compose up -d --build`
3. 리버스 프록시에 `nginx/uadeck.conf` 규칙 적용 (`/api/`, `/d/`)
### 메모
- 비회원도 저장·공유 가능 (30일 만료). 회원은 영구 저장.
- 외부 연동: `GET /api/v1/decks/:shareCode`, 클라이언트 `integrations/uadeck-deck-client.js`
## 2026-06-19 / v4.0.0 덱 저장 UX
### 수행 작업
- `cardList.html` 저장·공유 버튼 제거
- `result.html` 하단 Back 제거 →「내 덱에 저장」「공유」버튼 추가
- 저장·공유 시 Preview 이미지 PNG 함께 API 저장 (`preview_image`)
- 덱 이름: 입력값 없으면 `시리즈명 YYYY-MM-DD`
- `deckHistory.html` 미리보기 썸네일 표시
- 로그아웃 시 메인(`/`)으로 이동
- `seedAdmin`: `ADMIN_EMAIL`로 먼저 가입한 경우 역할을 `admin`으로 승격 (기존에는 INSERT 실패로 관리자 미생성)
- `auth-header.js`: 프로필 메뉴 열 때 `/auth/me`로 role 재동기화 (관리자 메뉴 미표시 수정)
- 관리자 API: JWT role 대신 DB role 확인 (`createRequireAdmin`)
- `npm start` / `npm stop` / `npm run logs` 로컬 실행 명령 정리 (`package.json`)
- 관리자 덱 관리 썸네일, 회원 모달 개편 (아바타·비밀번호 분리 모달)
- 본인 계정 역할/비활성 변경 서버·UI 차단, 표시 이름 필드 제거 (사용자명 사용)
## 2026-06-19 / v4.0.0 — 공개 덱·관리자 UI
### 수행 작업
- 메타 덱: 관리자 프리셋 등록 폐기 → 사용자 `is_public` 공개 덱이 `/preset.html`(공개 덱)에 노출
- 내 덱 저장 기본 비공개, 공유 시 공개, 내 덱 관리에서 공개/비공개 전환
- 덱 이름 기본값: `YYYYMMDD_HHMMSS` (시리즈명 제외)
- `admin.html` 레이아웃 개편 (`style/admin.css`), 프리셋 등록 탭 제거
- `preset.html` 카드 그리드 + 필터 UI
## 2026-06-19 / v4.0.0 — 프로필·아바타
### 수행 작업
- **썸네일(프로필 사진) 미적용 원인**: JPEG 등을 `.png`로 그대로 저장해 브라우저 표시 실패 → 업로드 전 `canvas`로 PNG 변환 (`script/image-utils.js`)
- `profile.html` 신규: 일반 사용자가 이메일·사용자명·아바타·비밀번호 변경 (`PUT /auth/me`, `POST /auth/me/password`)
- 헤더 드롭다운에「내 프로필」추가 (`script/auth-header.js`)
- 아바타 URL에 `updatedAt` 쿼리로 캐시 무효화 (저장 후 즉시 반영)
- `admin.html` 아바타 업로드도 동일 PNG 변환 적용
- `script/api.js`: `updateProfile`, `changePassword` 추가
- `docs/spec.md` 인증 API 엔드포인트 보강
### 확인 방법
1. `npm stop && npm start` 로 API 재시작 (auth 라우트 변경 반영)
2. http://localhost:8080/profile.html 에서 사진 업로드 →「프로필 저장」
3. 헤더 썸네일 클릭 →「내 프로필」메뉴 확인
### 후속
- 헤더 프로필 버튼: 로그인 시 아바타 옆 사용자명 표시 (`auth-header.js`, `auth-header.css`)
## 2026-06-19 / v4.0.0 — 덱 저장 UX·네비게이션
### 수행 작업
- `deckHistory.html`: `/d/코드` 경로 표시 제거 (링크 복사 버튼 유지)
- `result.html` 하단 버튼: `홈으로` / `이미지로 다운로드` / `내 덱으로 저장`
- 저장 시「모두에게 공개할까요?」모달 +「다음부터 물어보지 않기」토글
- `script/deck-prefs.js`: 공개 기본값·프롬프트 스킵 (localStorage + API 동기화)
- `users` 테이블: `default_deck_public`, `skip_deck_public_prompt` 컬럼 추가
- `profile.html`: 덱 저장 설정 (기본 공개, 물어보기 on/off)
- 뒤로가기 루프 수정: `result.html` 편집 복귀 시 `history.back()`, `cardList` ← 홈 버튼
### 확인
- API 변경 반영: `npm stop && npm start` (또는 `docker compose up -d --build`)
## 2026-06-19 / v4.0.0 — 레이아웃 통일·광고 공간
### 수행 작업
- `style/app-shell.css` 추가: 헤더·본문 `80rem`(1280px, index `max-w-7xl`과 동일)
- `deckHistory.html`, `preset.html`, `profile.html` 공통 셸 적용 (페이지 이동 시 헤더 너비 점프 제거)
- 데스크톱: 본문 좌측 + 우측 300px 광고 레일(`app-ad-rail`), 모바일: 상단 리더보드 슬롯
- 프로필 폼은 콘텐츠 영역 내 `max-width: 42rem` 유지
- `auth-pages.css`, `admin.css` 너비도 80rem으로 통일
## 2026-06-19 / v4.0.0 — 덱 썸네일 수정
### 수행 작업
- Preview 캡처 시 카드 이미지 로드 대기 + `cacheBust` 추가 (`result.html`)
- 캡처 실패해도 덱 저장은 진행, 안내 메시지 표시
- `deckHistory.html`: `UaDeckApi.resolveMediaUrl()`로 미리보기 URL 보정 (Live Server 등)
- 이미지 로드 실패 시 플레이스홀더 표시
- Docker 미리보기 저장 경로를 `./api/data/previews` 호스트 바인드 마운트로 변경
- API `getPreviewUrl`: DB `preview_image` 컬럼도 참고
## 2026-06-19 / v4.0.0 UI — 내 덱 관리
### 수행 작업
- `deckHistory.html` 카드 그리드 레이아웃으로 개편 (미리보기 이미지 상단 배치)
- `style/deck-history.css` 추가: 상단 여백, 툴바, 카드 그리드 전용 스타일
- 시리즈별 필터 드롭다운 + 덱 이름·시리즈 검색 입력 추가
- Tailwind 미빌드 클래스 의존 제거
## 2026-06-19 / v4.0.0 UI — result 하단 버튼
### 수행 작업
- `result.html` 하단「내 덱에 저장」「공유」버튼이 `style/output.css`에 없는 Tailwind 클래스(`bg-slate-700`, `bg-emerald-600`)로 인해 보이지 않던 문제 수정
- 페이지 내 `<style>`에 `.result-footer-*`, `.result-btn-*` 전용 스타일 추가 (인증 페이지와 동일한 패턴)
## 2026-06-19 / v4.0.0 UI — 인증 페이지
### 수행 작업
- `style/auth-pages.css` 추가: 폼 최대 너비 28rem, 상단 여백, index와 동일한 그라데이션·폰트
- `login.html`, `signup.html` 레이아웃 개선 (Tailwind 미빌드 클래스 의존 제거)
## 2026-06-19 / v4.0.0 배포·연동 보강
### 수행 작업
- `docker-compose.prod.yml`: Nginx + API 풀스택 (정적 파일 볼륨 마운트, 재빌드 불필요)
- `nginx/default.conf`, `docs/deploy.md` NAS 배포 가이드
- `integrations/uadeck-deck-client.js` 옆 프로젝트용 클라이언트
- `docs/api-integration.md` API 연동 문서
- `admin.html` 프리셋 등록 탭 추가
## 2026-06-19 / v4.0.0
### 수행 작업
- v4 회원·덱 API·공유 방식 설계를 확정했다.
- URL 쿼리(`?cards=`) 기반 공유는 폐기하고, 공유 코드 + `GET /api/v1/decks/:shareCode` API로 전환하기로 했다.
- 개인 덱은 저장마다 고유 `share_code`, 메타 덱·프리셋은 내용 해시 기반 고정 코드를 사용한다.
- DB 스키마 초안(`users`, `decks`)과 API 엔드포인트 목록을 `docs/spec.md` §9에 반영했다.
- 의사결정 내용을 `docs/history.md`에 기록했다.
### 메모
- 구현은 이 설계를 기준으로 진행한다. 실제 코드·Docker·화면 작업은 다음 단계다.
- 옆 프로젝트는 API 계약만 맞추면 되며, 레거시 쿼리 호환은 필요 없다.
## 2026-04-19 / v3.1.0
### 수행 작업