326 lines
17 KiB
Markdown
326 lines
17 KiB
Markdown
# 기술 명세
|
|
|
|
## 프로젝트 개요
|
|
|
|
- 서비스명: `UNIARE - Union Arena Deck Builder`
|
|
- 현재 버전: `v4.0.28`
|
|
- 프론트: 정적 HTML, CSS, JavaScript
|
|
- 백엔드: Express, SQLite(`better-sqlite3`)
|
|
- 운영: Docker Compose 기반 Nginx + API
|
|
- 지원 언어: 한국어, 일본어, 영어
|
|
- 표시 브랜드는 `UNIARE`이며 `UaDeckApi`, `uadeck_token`, `uadeck.db` 등 내부 레거시 식별자는 호환을 위해 유지한다.
|
|
|
|
## 화면 기능
|
|
|
|
| 경로 | 기능 |
|
|
|---|---|
|
|
| `/` | 시리즈 선택, 언어 전환, 인증 헤더 |
|
|
| `/cardList.html?series=<key>` | 카드 검색·필터, 수량 조절, 키 카드 지정, 덱 편집 |
|
|
| `/cardList.html?d=<shareCode>` | API 공유 덱 불러오기 및 편집 |
|
|
| `/result.html` | 덱 미리보기, PNG 다운로드, 회원 덱 저장 |
|
|
| `/login.html` | 로그인 |
|
|
| `/signup.html` | 회원가입 |
|
|
| `/verify-email.html` | 가입 이메일 인증·인증 메일 재전송 |
|
|
| `/forgot-password.html` | 비밀번호 재설정 메일 요청 |
|
|
| `/reset-password.html?token=<token>` | 1회용 토큰으로 비밀번호 재설정 |
|
|
| `/profile.html` | 이메일·사용자명·아바타·비밀번호·덱 공개 기본값·회원 탈퇴 관리 |
|
|
| `/deckHistory.html` | 내 덱 검색·필터·이름 변경·공개 설정·삭제·불러오기 |
|
|
| `/preset.html` | 공개 사용자 덱 검색·필터·즐겨찾기·불러오기 |
|
|
| `/admin.html` | 회원과 덱 관리 |
|
|
| `/d/<shareCode>` | Nginx에서 `cardList.html?d=<shareCode>`로 연결 |
|
|
|
|
## 검색 노출
|
|
|
|
- 메인 검색 제목은 `UNIARE | 유니온 아레나 덱 빌더`를 사용한다.
|
|
- 메인 설명은 카드 검색, 덱 제작·저장·공유, 공개 덱과 즐겨찾기 기능을 안내한다.
|
|
- JavaScript 실행 전에도 메인 제목과 설명이 HTML에 표시된다.
|
|
- canonical, Open Graph, Twitter Card, WebSite·WebApplication 구조화 데이터를 제공한다.
|
|
- 번역 파일 로딩 실패 시 내부 오류 대신 새로고침 안내를 표시한다.
|
|
|
|
## 카드 데이터
|
|
|
|
- 데이터 위치: `datas/*.js`
|
|
- 실제 시리즈 데이터: `ua01`~`ua57` 중 현재 존재하는 파일과 `uapr.js`
|
|
- `ua40` Re:제로 EX17BT는 116개 판본을 등록하고 이미지 81개를 제공한다. 패러렐 중심 이미지 35개는 미확보 상태다.
|
|
- `ua54` 무직전생은 `UAPR/MST-1-054` UR 패러렐 판본을 포함한다.
|
|
- `ua57` 슈타인즈 게이트는 128개 판본과 이미지를 제공한다.
|
|
- 샘플 데이터: `ua99 sample.js`, `ua99 sample mini.js`
|
|
- 모듈 형식: `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,
|
|
maxCount: 4,
|
|
}
|
|
```
|
|
|
|
- `Number`가 같은 판본은 동일 카드 수량 그룹으로 처리한다.
|
|
- `parallel`은 패러렐 판본 구분에 사용한다.
|
|
- `trigger`는 `null`, `COLOR`, `FINAL`, `SPECIAL` 값을 사용한다.
|
|
- `maxCount`가 없으면 기본 최대 수량은 4장이다.
|
|
|
|
## 이미지
|
|
|
|
- 카드 이미지: `images/<seriesKey>/<fileName>`
|
|
- 폴백 이미지: `/images/uapr/comingsoon.png`
|
|
- 브랜드 자산: `images/assets/`
|
|
- `images/**`는 Git LFS로 관리한다.
|
|
- 카드 목록·덱 결과 화면에서 카드 이미지 로드 실패 시 폴백 이미지를 표시한다.
|
|
- 아바타와 덱 미리보기는 API 데이터 디렉터리에 저장하며 Git에 포함하지 않는다.
|
|
- 덱 저장 미리보기는 1200px 기준 PNG로 정규화한다.
|
|
- 덱 저장 시 목록용 축소 썸네일을 420px WebP(`<shareCode>.thumb.webp`)로 함께 생성한다. (브라우저 WebP 인코딩 미지원 시 썸네일 생략, 원본으로 폴백)
|
|
- 목록 화면(내 덱·공개 덱·관리자)은 썸네일을 우선 표시하고, 길게 눌러 확대할 때만 원본 PNG를 사용한다.
|
|
- 내 덱·공개 덱 목록은 클라이언트 무한 스크롤(청크 24개)로 렌더링한다.
|
|
- 덱 삭제·미참조 정리 시 해당 공유 코드의 원본 PNG와 썸네일 WebP를 함께 삭제한다.
|
|
- 관리자는 DB에서 참조하지 않는 미리보기 파일을 조회하고 일괄 삭제할 수 있다.
|
|
- nginx는 텍스트 자산(css/js/json/svg 등)을 gzip으로 압축 전송하고, 정적 이미지는 30일 immutable 캐시로 제공한다.
|
|
- nginx는 HTML에 `X-Content-Type-Options`/`X-Frame-Options`/`Referrer-Policy`, 정적 자산에 `X-Content-Type-Options: nosniff` 헤더를 적용한다.
|
|
- 카드 목록 화면의 카드 이미지는 `loading="lazy"`로 지연 로드한다.
|
|
- 인증 API는 IP 기준 rate limit을 적용한다. (로그인 10회/10분, 계정·이메일 쓰기 30회/10분) API는 nginx 프록시 1홉을 신뢰한다.
|
|
- API `/api/v1/health`는 `package.json` 버전을 반환한다.
|
|
|
|
## 브라우저 상태
|
|
|
|
| 키 | 용도 |
|
|
|---|---|
|
|
| `lang` | 선택 언어 |
|
|
| `selectedCards` | 편집 중인 덱 |
|
|
| `selectedSeries` | 현재 시리즈 |
|
|
| `uadeck_token` | JWT. 로그인 유지 선택 시 `localStorage`, 미선택 시 `sessionStorage` |
|
|
| `uadeck_user` | 로그인 사용자 캐시. 토큰과 같은 저장소 사용 |
|
|
| `uadeck.deckFavorites` | 비로그인 즐겨찾기 및 로그인 시 최초 이전 대상 |
|
|
|
|
## 인증 및 권한
|
|
|
|
- 비밀번호는 bcrypt 비용 12로 해시한다.
|
|
- 신규 회원은 Resend 인증 링크로 이메일 소유 확인을 완료해야 로그인할 수 있다.
|
|
- 기존 회원은 이메일 인증 기능 배포 시 인증 완료 상태로 마이그레이션한다.
|
|
- 인증 링크는 24시간, 비밀번호 재설정 링크는 30분 동안 한 번만 사용할 수 있다.
|
|
- 인증·재설정 원문 토큰은 DB에 저장하지 않고 SHA-256 해시만 저장한다.
|
|
- 인증 메일 링크와 인증 화면은 버전 쿼리로 최신 HTML·API 클라이언트를 로드하고, 캐시된 구 클라이언트에서도 직접 인증 API를 호출한다.
|
|
- 인증 폼은 요청 중 제출 버튼을 잠그고 로딩 상태를 표시하며, 메일 재요청은 화면과 API에서 1분 간격으로 제한한다.
|
|
- 가입 인증·인증 재전송·비밀번호 재설정 메일은 계정별 한국 시간 기준 월 3회까지만 발송한다.
|
|
- 월간 한도 초과 시 자동 발송을 중단하고 관리자 이메일 `zenn@sori.studio` 문의를 안내한다.
|
|
- 미인증 사용자가 로그인하면 로그인 화면에서 인증 메일을 즉시 재전송할 수 있으며, 최초 발송 포함 월 3회 제한과 관리자 문의 방법을 함께 표시한다.
|
|
- 인증 완료 안내와 비밀번호 찾기·재설정 화면은 한국어를 기본값으로 한국어·일본어·영어 번역을 제공한다.
|
|
- 사용자명은 대소문자를 구분하지 않고 중복을 차단한다.
|
|
- 인증된 이메일은 프로필에서 직접 변경할 수 없다.
|
|
- JWT 기본 만료는 7일이며 `JWT_EXPIRES`로 변경할 수 있다.
|
|
- 로그인 유지 선택 시 브라우저를 닫아도 인증 정보를 유지하고, 미선택 시 현재 브라우저 세션에서만 유지한다.
|
|
- 인증된 화면 진입 시 `/auth/me` 검증과 함께 `last_login_at`을 갱신한다.
|
|
- `user`는 자신의 프로필과 덱만 수정·삭제할 수 있다.
|
|
- 일반 회원은 프로필 최하단에서 `DELETE` 확인 입력 후 직접 탈퇴할 수 있다.
|
|
- `admin`은 회원 상태·역할·비밀번호·탈퇴와 전체 덱을 관리할 수 있다.
|
|
- 관리자는 자신의 관리자 역할을 해제하거나 자신의 계정을 비활성화할 수 없다.
|
|
- 관리자는 현재 로그인한 자신의 계정을 관리자 화면에서 탈퇴시킬 수 없다.
|
|
- 비공개 덱은 소유자와 관리자만 조회할 수 있다.
|
|
- 비회원은 덱을 서버에 저장하거나 공개 링크를 생성할 수 없다.
|
|
- 로그인 사용자의 즐겨찾기는 계정 DB에 저장되어 브라우저와 기기가 달라도 유지된다.
|
|
- 비로그인 즐겨찾기는 브라우저에 저장하고 로그인 시 유효한 덱만 계정으로 이전한다.
|
|
- 계정 탈퇴 시 사용자·인증 토큰·메일 기록·즐겨찾기·아바타를 삭제한다.
|
|
- 탈퇴 회원의 저장 덱과 썸네일은 다른 사용자가 계속 참고할 수 있도록 유지하고 `user_id`만 `null`로 변경해 작성자를 익명 처리한다.
|
|
- 같은 사용자명으로 새로 가입해도 새 사용자 ID가 부여되어 기존 익명 덱이나 개인정보와 연결되지 않는다.
|
|
|
|
## 덱 저장 정책
|
|
|
|
- 공유 코드는 저장 시 생성되는 8자 코드다.
|
|
- 덱 저장과 공개 링크 생성은 로그인한 회원만 사용할 수 있다.
|
|
- 비회원은 덱 구성과 PNG 이미지 다운로드만 사용할 수 있다.
|
|
- 기존 비회원 공유 덱은 호환을 위해 원래 만료일까지 조회할 수 있다.
|
|
- DB의 `cards_json`에는 `{imgSrc, number, count, key}`를 저장한다.
|
|
- API 응답 시 `datas/<series>.js`에서 이미지·색상·희귀도·트리거 정보를 합성한다.
|
|
- 같은 `Number`에 여러 판본이 있을 경우 API는 저장된 `imgSrc`를 기준으로 카드 메타를 복원한다.
|
|
- 공개 덱 목록은 `share_type='user'`, `is_public=1`, 미만료 조건을 사용한다.
|
|
- `preset` 타입과 관리자 프리셋 API는 레거시 호환용으로 코드에 남아 있으며 현재 공개 덱 화면에서는 사용하지 않는다.
|
|
|
|
## 데이터베이스
|
|
|
|
### `users`
|
|
|
|
| 컬럼 | 설명 |
|
|
|---|---|
|
|
| `id` | 기본 키 |
|
|
| `email`, `username` | 고유 로그인·표시 식별자 |
|
|
| `password_hash` | bcrypt 해시 |
|
|
| `display_name` | 레거시 표시명, 현재 `username`과 함께 갱신 |
|
|
| `avatar_url` | 아바타 API 경로 |
|
|
| `role` | `user` 또는 `admin` |
|
|
| `is_active` | 계정 활성 상태 |
|
|
| `default_deck_public` | 덱 저장 기본 공개 여부 |
|
|
| `skip_deck_public_prompt` | 저장 시 공개 확인 생략 여부 |
|
|
| `last_login_at` | 마지막 로그인 또는 인증 세션 확인 시각 |
|
|
| `email_verified_at` | 이메일 인증 완료 시각. 기존 회원은 마이그레이션 시 채움 |
|
|
| `created_at`, `updated_at` | 생성·수정 시각 |
|
|
|
|
### `auth_tokens`
|
|
|
|
| 컬럼 | 설명 |
|
|
|---|---|
|
|
| `user_id` | 인증 대상 사용자 |
|
|
| `type` | `verify_email` 또는 `reset_password` |
|
|
| `token_hash` | 원문 토큰의 SHA-256 해시 |
|
|
| `expires_at`, `used_at` | 만료·사용 시각 |
|
|
| `created_at` | 발급 시각 |
|
|
|
|
### `email_send_logs`
|
|
|
|
| 컬럼 | 설명 |
|
|
|---|---|
|
|
| `user_id` | 발송 대상 사용자 |
|
|
| `type` | `verify_email` 또는 `reset_password` |
|
|
| `month_key` | 한국 시간 기준 `YYYY-MM` 월 |
|
|
| `status` | 동시 요청 방지용 `reserved`, 발송 완료 `sent` |
|
|
| `created_at`, `sent_at` | 예약·발송 시각 |
|
|
|
|
### `decks`
|
|
|
|
| 컬럼 | 설명 |
|
|
|---|---|
|
|
| `share_code` | 고유 공유 코드 |
|
|
| `share_type` | `user` 또는 레거시 `preset` |
|
|
| `content_hash` | 레거시 프리셋 중복 판별 |
|
|
| `user_id` | 소유 회원, 과거 비회원 덱은 `null` |
|
|
| `series`, `series_name` | 시리즈 키와 표시명 |
|
|
| `name` | 덱 이름 |
|
|
| `cards_json` | 최소 카드 구성 JSON |
|
|
| `is_public` | 공개 여부 |
|
|
| `view_count` | 상세 조회 횟수 |
|
|
| `expires_at` | 과거 비회원 공유 덱의 만료 시각 |
|
|
| `preview_image` | 저장된 미리보기 파일 정보 |
|
|
| `created_at`, `updated_at` | 생성·수정 시각 |
|
|
|
|
### `deck_favorites`
|
|
|
|
| 컬럼 | 설명 |
|
|
|---|---|
|
|
| `user_id` | 즐겨찾기 사용자, 사용자 삭제 시 함께 삭제 |
|
|
| `share_code` | 즐겨찾기 덱, 덱 삭제 시 함께 삭제 |
|
|
| `created_at` | 즐겨찾기 등록 시각 |
|
|
|
|
## API
|
|
|
|
### 공통
|
|
|
|
| Method | Path | 권한 | 기능 |
|
|
|---|---|---|---|
|
|
| GET | `/api/v1/health` | 공개 | API 상태와 버전 |
|
|
| GET | `/api/stats` | 공개 또는 토큰 | 외부 대시보드용 운영 지표 |
|
|
| GET | `/api/v1/stats` | 공개 또는 토큰 | 외부 대시보드용 운영 지표 |
|
|
|
|
`/api/stats`와 `/api/v1/stats`는 같은 응답을 반환한다. `.env`의 `STATS_API_TOKEN`이 비어 있으면 공개 집계 API로 동작하고, 값이 있으면 `Authorization: Bearer <token>` 또는 `X-Stats-Token: <token>` 헤더가 필요하다. 응답에는 개인정보 없이 집계값만 포함한다.
|
|
|
|
```json
|
|
{
|
|
"status": "online",
|
|
"ok": true,
|
|
"version": "4.0.27",
|
|
"lastUpdated": "2026-08-12T00:00:00.000Z",
|
|
"members": {
|
|
"total": 0,
|
|
"active": 0,
|
|
"inactive": 0,
|
|
"verified": 0,
|
|
"unverified": 0,
|
|
"created24h": 0,
|
|
"latestCreatedAt": null,
|
|
"latestLoginAt": null
|
|
},
|
|
"decks": {
|
|
"total": 0,
|
|
"userTotal": 0,
|
|
"presetTotal": 0,
|
|
"publicTotal": 0,
|
|
"privateTotal": 0,
|
|
"anonymousTotal": 0,
|
|
"created24h": 0,
|
|
"updated24h": 0,
|
|
"totalViews": 0,
|
|
"latestCreatedAt": null,
|
|
"latestUpdatedAt": null
|
|
},
|
|
"cards": {
|
|
"seriesTotal": 0,
|
|
"cardTypesTotal": 0,
|
|
"cardPrintsTotal": 0,
|
|
"parallelPrintsTotal": 0,
|
|
"bySeries": []
|
|
}
|
|
}
|
|
```
|
|
|
|
### 인증
|
|
|
|
| Method | Path | 권한 | 기능 |
|
|
|---|---|---|---|
|
|
| POST | `/api/v1/auth/register` | 공개 | 미인증 회원 생성·가입 인증 메일 발송 |
|
|
| POST | `/api/v1/auth/resend-verification` | 공개 | 가입 인증 메일 재전송 |
|
|
| POST | `/api/v1/auth/verify-email` | 공개 | 이메일 인증 토큰 확인 |
|
|
| POST | `/api/v1/auth/login` | 공개 | 로그인과 JWT 발급, `last_login_at` 갱신 |
|
|
| POST | `/api/v1/auth/forgot-password` | 공개 | 비밀번호 재설정 메일 요청 |
|
|
| POST | `/api/v1/auth/reset-password` | 공개 | 1회용 토큰으로 비밀번호 변경 |
|
|
| GET | `/api/v1/auth/me` | 회원 | 내 정보와 인증 세션 확인, `last_login_at` 갱신 |
|
|
| PUT | `/api/v1/auth/me` | 회원 | 프로필·덱 저장 설정 변경 |
|
|
| POST | `/api/v1/auth/me/password` | 회원 | 비밀번호 변경 |
|
|
| DELETE | `/api/v1/auth/me` | 회원 | `DELETE` 확인 후 본인 계정 탈퇴 |
|
|
| GET | `/api/v1/auth/avatar/:userId` | 공개 | PNG 아바타 |
|
|
|
|
### 덱
|
|
|
|
| Method | Path | 권한 | 기능 |
|
|
|---|---|---|---|
|
|
| GET | `/api/v1/decks/presets` | 공개 | 공개 사용자 덱 목록 |
|
|
| GET | `/api/v1/decks/:shareCode/preview` | 공개 | 덱 미리보기 PNG |
|
|
| GET | `/api/v1/decks/:shareCode/thumbnail` | 공개 | 덱 목록용 축소 WebP 썸네일 |
|
|
| GET | `/api/v1/decks/:shareCode` | 조건부 | 공개 덱 또는 소유 덱 상세 |
|
|
| GET | `/api/v1/decks` | 회원 | 내 덱 목록 |
|
|
| POST | `/api/v1/decks` | 회원 | 내 덱 저장과 공개 링크 생성 |
|
|
| PUT | `/api/v1/decks/:shareCode` | 소유자·관리자 | 덱 수정 |
|
|
| DELETE | `/api/v1/decks/:shareCode` | 소유자·관리자 | 덱 삭제 |
|
|
| GET | `/api/v1/decks/favorites` | 회원 | 내 즐겨찾기 코드 목록 |
|
|
| POST | `/api/v1/decks/favorites` | 회원 | 브라우저 즐겨찾기 일괄 이전 |
|
|
| PUT | `/api/v1/decks/favorites/:shareCode` | 회원 | 즐겨찾기 추가 |
|
|
| DELETE | `/api/v1/decks/favorites/:shareCode` | 회원 | 즐겨찾기 제거 |
|
|
|
|
### 관리자
|
|
|
|
| Method | Path | 기능 |
|
|
|---|---|---|
|
|
| GET | `/api/v1/admin/users` | 회원 목록·검색 (`deckPublic`, `deckTotal`, `lastLoginAt` 포함) |
|
|
| GET | `/api/v1/admin/users/:id` | 회원 상세 (`deckPublic`, `deckTotal`, `deckCount`, `lastLoginAt` 포함) |
|
|
| PUT | `/api/v1/admin/users/:id` | 회원 정보·역할·활성 상태 변경 |
|
|
| POST | `/api/v1/admin/users/:id/verify-email` | 미인증 회원 이메일 수동 승인 |
|
|
| POST | `/api/v1/admin/users/:id/reset-password` | 비밀번호 초기화 |
|
|
| DELETE | `/api/v1/admin/users/:id` | `DELETE` 확인 후 회원 탈퇴 처리 |
|
|
| GET | `/api/v1/admin/decks` | 전체 덱 목록 |
|
|
| POST | `/api/v1/admin/decks/bulk-delete` | 선택한 덱 최대 100개와 썸네일 일괄 삭제 |
|
|
| DELETE | `/api/v1/admin/decks/:shareCode` | 덱 삭제 |
|
|
| GET | `/api/v1/admin/previews/orphans` | 미참조 덱 미리보기 목록 |
|
|
| DELETE | `/api/v1/admin/previews/orphans` | 미참조 덱 미리보기 일괄 삭제 |
|
|
| POST | `/api/v1/admin/decks/preset` | 레거시 프리셋 생성 |
|
|
|
|
## 스타일
|
|
|
|
- 현재는 Tailwind 유틸리티, 공통 CSS, 페이지 전용 CSS, 일부 인라인 스타일을 혼용한다.
|
|
- 기존 CSS는 `todo.md`의 화면별 체크리스트 순서에 따라 점진적으로 Tailwind로 전환한다.
|
|
- 관리자와 로그인·회원가입 화면은 Tailwind 전환을 완료하고 전용 CSS를 제거했다.
|
|
- 관리자 탭·모달 상태는 JavaScript에서 `border-*`, `text-*`, `hidden`, `flex` 유틸리티를 전환한다.
|
|
- 덱 카드·툴바·프로필·앱 셸 스타일은 `style/components.css`(`@layer components`)에 통합되어 `output.css`로 빌드된다.
|
|
- `style/output.css`는 `style/input.css`에서 생성하는 빌드 산출물이다.
|
|
- 헤더 로고 크기는 `style/brand.css`만 기준으로 사용한다.
|
|
- 기본 글꼴은 Pretendard 계열을 우선 사용한다.
|
|
|
|
## 알려진 제약
|
|
|
|
- `cardList.html`, `result.html`의 인라인 스크립트가 커서 변경 영향 범위가 넓다.
|
|
- `admin.html`은 3개 언어 번역 적용이 완료되지 않았다.
|
|
- 외부 통계 API는 집계값만 제공하며, 상세 사용자·덱 정보는 포함하지 않는다.
|
|
- 현재 Compose의 컨테이너명(`uniare-api`, `uniare-web`)과 데이터 볼륨명(`uniare-data`)은 고정값이며 환경 변수화되지 않았다.
|
|
- Resend 발송은 운영 `.env`의 API 키·발신 주소와 Resend에서 인증된 발신 도메인이 필요하다.
|