init: create Play Uniare project

This commit is contained in:
2026-06-19 10:58:41 +09:00
commit d127f8ff60
28 changed files with 2744 additions and 0 deletions

9
.dockerignore Normal file
View File

@@ -0,0 +1,9 @@
.git
.gitignore
.vscode
dist
node_modules
npm-debug.log
.env
.env.*
docs

2
.env.example Normal file
View File

@@ -0,0 +1,2 @@
VITE_CARD_ASSET_BASE_URL=/card-assets
VITE_SOCKET_SERVER_URL=http://localhost:3000

27
.gitignore vendored Normal file
View File

@@ -0,0 +1,27 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
.env
.env.*
!.env.example
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

236
AGENTS.md Normal file
View File

@@ -0,0 +1,236 @@
# Play Uniare AI 작업 지침
## 1. 문서 목적
이 파일은 사람과 AI 작업자가 같은 기준으로 프로젝트를 이해하고 변경하기
위한 최상위 작업 규칙이다. 작업 시작 전에 이 파일과 `docs/`의 관련 문서를
반드시 읽는다.
## 2. 프로젝트 목표
- 기존 UA Deck Builder가 전달한 덱 데이터로 카드 시뮬레이터를 제공한다.
- 게임 규칙을 자동 판정하지 않는다.
- 사용자가 카드 이동, 뒤집기, 탭, 덱·손패·무덤 이동을 직접 수행한다.
- 혼자 연습하는 로컬 모드와 방 코드 기반 2인 세션을 제공한다.
- 각 플레이어는 자신이 소유한 카드만 조작한다.
- 상대 효과로 내 카드가 제거되는 경우에도 내 카드 소유자가 직접 처리한다.
## 3. 기술 기준
- 프런트엔드: Vue 3 Composition API와 `<script setup>`
- 빌드 도구: Vite
- 스타일: Tailwind CSS 우선
- 상태 관리: Pinia 도입 예정
- 화면 이동: Vue Router 도입 예정
- 실시간 통신: Socket.IO 도입 예정
- 배포: Docker Compose와 Nginx
- 운영 환경: UGREEN NAS
새 라이브러리를 추가하기 전에는 기존 기술로 해결 가능한지 확인하고, 추가
이유와 영향을 `docs/history.md`에 기록한다.
## 4. 작업 시작 절차
1. `AGENTS.md`를 읽는다.
2. `docs/spec.md`, `docs/map.md`, `docs/todo.md`를 읽는다.
3. `git status`로 기존 변경 사항을 확인한다.
4. 관련 코드와 기존 패턴을 확인한다.
5. 작업 범위를 작게 정하고 구현한다.
6. 빌드와 관련 검증을 수행한다.
7. 변경 성격에 맞는 문서를 업데이트한다.
8. 작업 결과 형식에 맞춰 보고한다.
## 5. 코드 작성 규칙
### 5.1 공통
- 기존 패턴을 우선하며 최소 범위만 수정한다.
- 하나의 컴포넌트는 하나의 분명한 역할을 갖는다.
- 반복되는 값은 상수, 설정 또는 데이터 모델로 분리한다.
- 서버 주소, 이미지 원본 주소, 포트, 비밀값을 코드에 하드코딩하지 않는다.
- 임시 이름과 의미 없는 자리표시자 이름을 사용하지 않는다.
- 검증 없이 작업을 완료 처리하지 않는다.
### 5.2 네이밍
- 의미가 드러나는 영문 이름을 사용한다.
- 축약어를 남용하지 않는다.
- `temp`, `tmp`, `test1`, `data1`, `foo`, `bar`, `aaa` 같은 임시 이름을
금지한다.
- Vue 컴포넌트 파일은 PascalCase를 사용한다.
- 함수와 변수는 camelCase를 사용한다.
- 상수는 의미가 분명한 경우 UPPER_SNAKE_CASE를 사용한다.
- 이벤트 이름은 발생한 사실을 나타내는 kebab-case를 사용한다.
- 불리언 값은 `is`, `has`, `can`, `should` 접두어를 우선한다.
### 5.3 Vue
- Composition API와 `<script setup>`을 사용한다.
- 화면 컴포넌트는 `src/views/`, 재사용 컴포넌트는 `src/components/`에 둔다.
- 비즈니스 상태를 컴포넌트 내부에 중복 저장하지 않는다.
- props를 직접 변경하지 않는다.
- 외부 효과와 구독은 컴포넌트 해제 시 반드시 정리한다.
### 5.4 스타일
- Tailwind CSS 유틸리티를 기본으로 사용한다.
- 반복되는 디자인 패턴은 컴포넌트로 추출한다.
- 임의 픽셀값은 게임 보드 좌표처럼 필요한 경우에만 사용하고 이유를
문서화한다.
- 카드 위치는 화면 픽셀이 아니라 보드 기준 정규화 좌표 또는 영역 기반으로
저장한다.
- 인라인 `style`은 드래그 좌표, 회전처럼 동적 계산이 필요한 경우에만
허용한다.
### 5.5 주석
- 함수와 클래스 앞에는 한국어 JSDoc 문서화 주석을 작성한다.
- 주석은 코드가 무엇을 하는지 반복하기보다 이유, 제약, 입력·출력을 설명한다.
- 복잡한 타입은 `@typedef`, 함수 입력은 `@param`, 반환은 `@returns`를 쓴다.
```js
/**
* 덱 빌더에서 전달된 카드 목록을 실제 카드 인스턴스로 확장한다.
*
* @param {ImportedDeckCard[]} importedCards 수량이 포함된 덱 카드 목록
* @returns {GameCard[]} 고유 식별자가 부여된 카드 인스턴스 목록
*/
function createGameCardInstances(importedCards) {
return []
}
```
## 6. 상태 관리 원칙
### 6.1 상태 구분
- 서버 권위 상태: 방, 플레이어, 카드 소유권, 카드 영역, 덱 순서
- 공유 게임 상태: 필드 카드 좌표, 앞뒷면, 탭 상태, 무덤, 공개 영역
- 개인 비공개 상태: 내 손패 카드 식별 정보
- 로컬 UI 상태: 선택 카드, 드래그 중 여부, 메뉴 열림, 확대 상태
서버 권위 상태를 UI 컴포넌트의 로컬 상태만으로 확정하지 않는다.
### 6.2 카드 모델
카드는 덱 원본 정보와 플레이 중 인스턴스를 구분한다.
```js
{
instanceId: 'player-1-card-17',
ownerId: 'player-1',
cardNumber: 'EX01BT/HTR-2-001',
imageFileName: 'EX01BT_HTR-2-001.png',
zone: 'deck',
position: { x: 0.5, y: 0.5 },
isFaceUp: false,
isTapped: false,
stackOrder: 17
}
```
- `instanceId`는 같은 카드 여러 장을 구분하는 고유값이다.
- 소유권은 게임 시작 후 임의로 변경하지 않는다.
- 카드 조작 요청은 요청자와 `ownerId`가 같은지 서버에서 확인한다.
- 상대 손패에는 카드 번호와 이미지 정보를 전송하지 않는 방향을 우선한다.
### 6.3 실시간 이벤트
- 상태 전체를 매번 전송하기보다 명시적인 이벤트와 필요한 변경값을 전송한다.
- 이벤트 이름은 `card-moved`, `card-flipped`, `deck-shuffled`처럼 작성한다.
- 재접속 시에는 서버의 최신 전체 스냅샷으로 복구한다.
- 같은 이벤트가 중복 수신되어도 상태가 망가지지 않도록 설계한다.
## 7. 데이터 처리 원칙
- 기존 덱 빌더 쿼리는 초기 덱 불러오기 용도로만 사용한다.
- 외부 입력은 신뢰하지 않고 스키마 검사 후 사용한다.
- 카드 이미지 URL은 환경변수의 원본 주소와 파일 경로를 조합한다.
- NAS 운영 환경에서는
`/volume1/docker/projects/apps/uniare/config/www/images`를 컨테이너에 읽기
전용으로 연결하고 `/card-assets` 경로로 제공한다.
- 기존 Uniare의 `config/www/images`에는 쓰기 작업을 수행하지 않는다.
- URL 길이 문제를 고려해 장기적으로 덱 코드 또는 `deckId` 방식을 도입한다.
- URL, 로그, Git에 사용자 토큰이나 비밀값을 남기지 않는다.
- 오류가 있는 카드 한 장 때문에 전체 덱 불러오기가 중단되지 않도록 오류
목록을 사용자에게 명확히 보여준다.
## 8. 문서 업데이트 기준
- 기능 변경 → `docs/spec.md`
- 버그 수정 → `docs/update.md`
- 이슈 발견 → `docs/todo.md`
- 구조·기술·의사결정 변경 → `docs/history.md`
- 화면 변경 → `docs/map.md`
- 배포 변경 → `docs/deploy.md`
- 개발 명령 변경 → `docs/development.md`
문서 업데이트 누락을 금지한다.
## 9. 금지 사항
- 기존 스타일과 구조를 확인하지 않은 변경
- 요청 범위를 벗어난 대규모 리팩터링
- 서버 주소, 포트, API 키와 비밀번호 하드코딩
- `.env`, 인증서, 개인키, NAS 계정 정보 커밋
- 조작 권한 검증을 클라이언트에만 의존
- 상대의 비공개 손패 정보를 클라이언트로 전송
- 테스트 또는 빌드 검증 없이 완료 처리
- 관련 문서 업데이트 누락
## 10. 의사결정 원칙
1. 기존 패턴 우선
2. 최소 수정
3. 유지보수 용이성
4. 구조 파악 용이성
5. 사용자가 직접 운영할 수 있는 단순성
6. 비공개 정보와 카드 소유권의 안전성
## 11. 단계별 개발 계획
### 1단계: 로컬 싱글 플레이
- 덱 쿼리 파싱과 검증
- 카드 인스턴스 생성
- 덱 셔플과 드로우
- 카드 이동, 뒤집기, 탭
- 손패, 필드, 무덤, 공개 영역
- 덱 위·아래 이동과 위에서 N장 공개
### 2단계: 2인 세션
- 로비와 방 생성·참가
- 플레이어 좌석과 소유권
- Socket.IO 서버
- 실시간 카드 이벤트 동기화
- 상대 손패 비공개 처리
- 연결 종료와 재접속 복구
### 3단계: 운영 안정화
- NAS Docker 배포
- 상태 저장소 도입 검토
- 모바일·태블릿 조작 개선
- 오류 로그와 상태 복구
- 관전자와 게임 기록은 별도 검토
구체적인 미완료 항목은 `docs/todo.md`에서 관리한다.
## 12. 작업 결과 보고 형식
- 수정 내용
- 변경 파일
- 검증 내용
- 문서 반영 여부
- 남은 이슈
## 13. 최종 체크리스트
- [ ] `AGENTS.md`와 관련 `docs/` 확인
- [ ] 코드 수정 완료
- [ ] 네이밍과 JSDoc 규칙 확인
- [ ] 빌드 또는 관련 검증 수행
- [ ] 변경 성격에 맞는 문서 반영
- [ ] 민감 정보 포함 여부 확인
- [ ] 남은 이슈를 `docs/todo.md`에 기록

19
Dockerfile Normal file
View File

@@ -0,0 +1,19 @@
FROM node:22-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:1.27-alpine AS runtime
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget --quiet --tries=1 --spider http://127.0.0.1/ || exit 1

27
README.md Normal file
View File

@@ -0,0 +1,27 @@
# Play Uniare
Union Arena 덱을 불러와 혼자 연습하거나 2인 세션으로 플레이하는 웹 카드
시뮬레이터입니다.
## 기술 구성
- Vue 3
- Vite
- Tailwind CSS
- 향후 Pinia, Vue Router, Socket.IO 도입 예정
## 빠른 실행
```bash
npm install
npm run dev
```
브라우저에서 터미널에 표시된 로컬 주소로 접속합니다.
상세한 개발 및 배포 방법은 다음 문서를 참고합니다.
- 개발: `docs/development.md`
- NAS 배포: `docs/deploy.md`
- 프로젝트 명세: `docs/spec.md`
- AI 작업 규칙: `AGENTS.md`

7
compose.nas.yaml Normal file
View File

@@ -0,0 +1,7 @@
services:
playuniare-web:
volumes:
- type: bind
source: /volume1/docker/projects/apps/uniare/config/www/images
target: /srv/uniare-card-images
read_only: true

7
compose.test.nas.yaml Normal file
View File

@@ -0,0 +1,7 @@
services:
playuniare-test-web:
volumes:
- type: bind
source: /volume1/docker/projects/apps/uniare/config/www/images
target: /srv/uniare-card-images
read_only: true

11
compose.test.yaml Normal file
View File

@@ -0,0 +1,11 @@
name: playuniare-test
services:
playuniare-test-web:
build:
context: .
dockerfile: Dockerfile
container_name: playuniare-test-web
restart: unless-stopped
ports:
- '18080:80'

11
compose.yaml Normal file
View File

@@ -0,0 +1,11 @@
name: playuniare-production
services:
playuniare-web:
build:
context: .
dockerfile: Dockerfile
container_name: playuniare-web
restart: unless-stopped
ports:
- '18081:80'

31
docker/nginx.conf Normal file
View File

@@ -0,0 +1,31 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location = /health {
access_log off;
add_header Content-Type text/plain;
return 200 'ok';
}
location ^~ /card-assets/ {
alias /srv/uniare-card-images/;
autoindex off;
expires 1h;
add_header Cache-Control "public, max-age=3600";
add_header X-Content-Type-Options "nosniff";
}
location ~* \.(?:css|js|jpg|jpeg|gif|png|svg|ico|webp|woff2?)$ {
expires 7d;
add_header Cache-Control "public, max-age=604800, immutable";
try_files $uri =404;
}
}

342
docs/deploy.md Normal file
View File

@@ -0,0 +1,342 @@
# UGREEN NAS Docker 배포 안내
## 문서 목적
이 문서는 터미널과 Docker에 익숙하지 않은 사용자가 Play Uniare의 테스트
버전과 운영 버전을 UGREEN NAS에서 실행하고 업데이트하는 절차를 설명한다.
현재 프로젝트는 Vue 정적 프런트엔드만 포함한다. 실시간 서버가 추가되면
`compose.yaml`에 백엔드 서비스와 상태 저장소를 함께 추가한다.
## 배포 방식
```text
사용자 브라우저
↓
NAS 포트 또는 리버스 프록시
↓
Play Uniare 컨테이너의 Nginx
├── / → Vue 빌드 결과
└── /card-assets/ → 기존 Uniare 카드 이미지
```
Docker 이미지를 만들 때 Node.js로 Vue를 빌드하고, 실행할 때는 가벼운
Nginx가 빌드 결과를 제공한다.
카드 이미지는 다음 기존 폴더를 복사하지 않고 읽기 전용으로 연결한다.
```text
/volume1/docker/projects/apps/uniare/config/www/images
```
## NAS에서 처음 한 번 준비하기
### 1. Docker 앱 설치
UGREEN NAS의 앱 센터에서 Docker 또는 Container Manager에 해당하는 앱을
설치한다. 모델과 UGOS 버전에 따라 앱 이름이 다를 수 있다.
### 2. 프로젝트 저장 폴더 준비
NAS 공유 폴더에 다음과 같은 폴더를 만든다.
```text
/volume1/docker/projects/apps/
├── uniare/
│ └── config/
│ └── www/
│ ├── index.html
│ └── images/
└── playuniare/
```
이 프로젝트의 NAS 경로는 다음으로 고정한다.
```text
/volume1/docker/projects/apps/playuniare
```
### 3. SSH 사용 여부 결정
UGREEN 관리 화면에서 Docker Compose 프로젝트를 직접 등록할 수 있다면 GUI를
우선 사용한다. GUI에서 Git 저장소를 직접 받을 수 없거나 업데이트가 어려운
경우에만 SSH 터미널을 사용한다.
SSH를 활성화할 때는 관리자 화면에서 포트와 계정을 확인한다. NAS 비밀번호를
문서, `.env`, Git 저장소에 기록하지 않는다.
## 테스트 버전 실행
테스트 버전은 외부 도메인에 연결하기 전에 NAS 내부에서 확인하는 용도다.
### 1. NAS에 프로젝트 받기
NAS 터미널에서 프로젝트 저장 폴더로 이동한다.
```bash
cd /volume1/docker/projects/apps
```
처음 한 번만 저장소를 복제한다.
```bash
git clone https://git.sori.studio/zenn/playuniare.sori.studio.git playuniare
cd playuniare
```
비공개 저장소가 인증을 요구하면 비밀번호 대신 Git 서버에서 발급한 접근
토큰을 사용하는 것을 권장한다. 토큰을 명령문이나 문서에 저장하지 않는다.
### 2. 테스트 컨테이너 빌드 및 실행
```bash
docker compose \
-f compose.test.yaml \
-f compose.test.nas.yaml \
up -d --build
```
이 명령은 다음 작업을 한 번에 수행한다.
- 최신 소스 코드로 Docker 이미지 생성
- 테스트 컨테이너 실행
- NAS의 18080 포트와 컨테이너 연결
### 3. 브라우저에서 확인
NAS의 내부 IP가 `192.168.0.20`이라면 다음 주소로 접속한다.
```text
http://192.168.0.20:18080
```
실제 NAS IP는 공유기 또는 UGREEN 관리 화면에서 확인한다.
### 4. 테스트 로그 확인
화면이 열리지 않으면 다음을 실행한다.
```bash
docker compose \
-f compose.test.yaml \
-f compose.test.nas.yaml \
ps
docker compose \
-f compose.test.yaml \
-f compose.test.nas.yaml \
logs --tail=100
```
`Up` 또는 `running` 상태인지 확인한다.
### 5. 테스트 버전 중지
```bash
docker compose \
-f compose.test.yaml \
-f compose.test.nas.yaml \
down
```
컨테이너는 제거되지만 소스 코드와 Docker 이미지는 남아 있다.
## 운영 버전 최초 배포
### 1. 배포 전 로컬 검증
개발 컴퓨터에서 다음 명령이 성공해야 한다.
```bash
npm install
npm run build
```
그 후 변경 사항을 커밋하고 원격 저장소에 푸시한다.
### 2. NAS에서 운영 컨테이너 실행
NAS 프로젝트 폴더에서 실행한다.
```bash
cd /volume1/docker/projects/apps/playuniare
git pull
docker compose \
-f compose.yaml \
-f compose.nas.yaml \
up -d --build
```
운영 컨테이너는 NAS의 18081 포트를 사용한다.
```text
http://NAS_IP:18081
```
### 3. 도메인 연결
UGREEN NAS 또는 사용 중인 리버스 프록시에서 다음과 같이 연결한다.
```text
외부 도메인: https://playuniare.sori.studio
내부 대상: http://127.0.0.1:18081
```
- 외부 접속은 HTTPS를 사용한다.
- 공유기에서 컨테이너 포트를 그대로 외부에 노출하기보다 리버스 프록시를
사용한다.
- 인증서 자동 갱신이 설정되어 있는지 확인한다.
### 4. 카드 이미지 연결 확인
브라우저에서 실제 카드 파일 하나를 직접 연다.
```text
http://NAS_IP:18081/card-assets/ua03/EX01BT_HTR-2-001.png
```
이미지가 보이면 읽기 전용 마운트와 Nginx 경로가 정상이다. 파일명이 실제로
존재하는 카드인지 먼저 확인한다.
이미지가 보이지 않으면 NAS 터미널에서 원본 경로를 확인한다.
```bash
ls /volume1/docker/projects/apps/uniare/config/www/images
```
시리즈 폴더가 보여야 한다. 다음 명령으로 컨테이너 내부 연결도 확인한다.
```bash
docker exec playuniare-web ls /srv/uniare-card-images
```
이 경로 역시 같은 시리즈 폴더를 보여야 한다.
## 새 버전 업데이트
### 안전한 권장 순서
1. 개발 컴퓨터에서 `npm run build`를 실행한다.
2. 변경 내용을 Git에 커밋하고 푸시한다.
3. NAS에서 테스트 버전을 먼저 업데이트한다.
4. 테스트 주소에서 주요 기능을 확인한다.
5. 문제가 없으면 운영 버전을 업데이트한다.
### 테스트 버전 업데이트
```bash
cd /volume1/docker/projects/apps/playuniare
git pull
docker compose \
-f compose.test.yaml \
-f compose.test.nas.yaml \
up -d --build
```
브라우저에서 `http://NAS_IP:18080`을 확인한다.
### 운영 버전 업데이트
```bash
cd /volume1/docker/projects/apps/playuniare
git pull
docker compose \
-f compose.yaml \
-f compose.nas.yaml \
up -d --build
```
브라우저에서 실제 도메인을 확인한다.
### 사용하지 않는 Docker 이미지 정리
업데이트를 여러 번 하면 이전 이미지가 NAS 용량을 차지할 수 있다.
```bash
docker image prune
```
삭제 대상 확인 질문이 나오면 내용을 읽고 `y`를 입력한다. `-a` 옵션이나
볼륨 삭제 명령은 데이터 유실 가능성이 있으므로 이 문서에서는 사용하지 않는다.
## 이전 버전으로 되돌리기
문제가 생기면 먼저 현재 커밋을 확인한다.
```bash
git log --oneline -10
```
정상 동작했던 커밋 식별자를 확인한 뒤 다음처럼 전환한다.
```bash
git checkout 정상커밋식별자
docker compose \
-f compose.yaml \
-f compose.nas.yaml \
up -d --build
```
복구가 끝난 뒤 개발 컴퓨터에서 문제를 수정하고 정상 브랜치에 새 커밋을
만든다. NAS에서 임의로 소스 파일을 직접 수정하지 않는다.
다시 최신 `main`으로 돌아갈 때:
```bash
git checkout main
git pull
docker compose \
-f compose.yaml \
-f compose.nas.yaml \
up -d --build
```
## 운영 점검 명령
```bash
docker compose -f compose.yaml -f compose.nas.yaml ps
docker compose -f compose.yaml -f compose.nas.yaml logs --tail=100
docker compose -f compose.yaml -f compose.nas.yaml restart
```
- `ps`: 실행 상태 확인
- `logs`: 최근 오류 확인
- `restart`: 같은 이미지로 컨테이너 재시작
## 배포 체크리스트
- [ ] 로컬 `npm run build` 성공
- [ ] 관련 문서 업데이트
- [ ] Git 커밋과 푸시 완료
- [ ] 테스트 컨테이너 재빌드
- [ ] 테스트 URL 주요 기능 확인
- [ ] 운영 컨테이너 재빌드
- [ ] 실제 도메인 HTTPS 접속 확인
- [ ] `/card-assets/<series>/<fileName>` 이미지 접속 확인
- [ ] 카드 이미지 마운트가 읽기 전용인지 확인
- [ ] 브라우저 새로고침과 직접 URL 접근 확인
- [ ] 오류 로그 확인
- [ ] `.env`, 토큰, 비밀번호가 Git에 없는지 확인
## 향후 실시간 서버 추가 시
프런트엔드만 있을 때는 Nginx 컨테이너 하나면 충분하다. Socket.IO 서버가
추가되면 다음 항목이 필요하다.
- Node.js 서버 컨테이너
- WebSocket을 지원하는 리버스 프록시 설정
- 프런트엔드와 서버의 환경변수
- 방 상태 백업 또는 Redis 도입 여부
- 컨테이너 재시작 시 방이 종료된다는 안내
이 구조가 변경되면 반드시 이 문서와 `docs/history.md`를 함께 수정한다.
## NAS 전용 Compose 파일을 분리한 이유
- `compose.yaml`: 어느 컴퓨터에서도 검사하고 빌드할 수 있는 공통 운영 설정
- `compose.nas.yaml`: NAS에만 존재하는 카드 이미지 절대 경로 연결
- `compose.test.yaml`: 공통 테스트 설정
- `compose.test.nas.yaml`: 테스트 컨테이너의 NAS 카드 이미지 연결
개발 Mac에는 `/volume1/...` 경로가 없으므로 NAS 전용 파일을 사용하지 않는다.
NAS에서는 반드시 공통 파일과 NAS 전용 파일을 함께 지정한다.

132
docs/development.md Normal file
View File

@@ -0,0 +1,132 @@
# 로컬 개발과 테스트 실행 안내
## 처음 한 번만 준비하기
### 1. Node.js 확인
터미널에서 실행한다.
```bash
node --version
npm --version
```
두 명령 모두 버전 번호가 출력되어야 한다. 명령을 찾을 수 없다는 메시지가
나오면 Node.js LTS 버전을 먼저 설치한다.
### 2. 프로젝트 폴더로 이동
```bash
cd /Users/bicute/Desktop/dxp6800pro/uniare-simulator
```
### 3. 패키지 설치
프로젝트를 처음 받았거나 `package.json`이 변경되었을 때 실행한다.
```bash
npm install
```
`node_modules`는 자동 생성되며 Git에 올리지 않는다.
## 개발 서버 실행
```bash
npm run dev
```
터미널에 다음과 비슷한 주소가 표시된다.
```text
Local: http://localhost:5173/
```
해당 주소를 브라우저에서 연다. 개발 중 코드를 저장하면 화면이 자동으로
갱신된다.
개발 서버를 종료하려면 실행 중인 터미널에서 `Control + C`를 누른다.
## 같은 네트워크의 다른 기기에서 테스트
```bash
npm run dev -- --host
```
터미널의 `Network` 주소를 같은 공유기에 연결된 휴대전화나 태블릿에서 연다.
macOS 방화벽이 연결 허용 여부를 물으면 개발 중에만 허용한다.
## 배포용 빌드 확인
코드 변경을 완료하기 전에 반드시 실행한다.
```bash
npm run build
```
성공하면 `dist/` 폴더가 생성된다. 오류가 발생하면 배포하지 말고 오류를 먼저
수정한다.
빌드 결과를 로컬에서 확인하려면 다음을 실행한다.
```bash
npm run preview
```
개발 서버와 마찬가지로 터미널에 표시된 주소로 접속한다.
## 환경변수 사용
로컬 전용 값은 프로젝트 루트의 `.env.local`에 작성한다.
```text
VITE_CARD_ASSET_BASE_URL=http://127.0.0.1:5500/images
VITE_SOCKET_SERVER_URL=http://localhost:3000
```
- `VITE_`로 시작하는 값은 브라우저에 노출될 수 있다.
- 비밀번호, 개인키, NAS 관리자 계정은 넣지 않는다.
- `.env.local`은 Git에 커밋하지 않는다.
## 일반적인 작업 순서
```bash
git pull
npm install
npm run dev
```
작업이 끝난 뒤:
```bash
npm run build
git status
```
변경 문서를 확인한 후 커밋하고 푸시한다.
## 자주 발생하는 문제
### 포트가 이미 사용 중이라고 나오는 경우
Vite가 다른 포트를 자동으로 제시할 수 있다. 터미널에 새로 표시된 주소로
접속한다. 반드시 5173 포트를 고정할 필요는 없다.
### 패키지 오류가 발생하는 경우
먼저 다음을 다시 실행한다.
```bash
npm install
npm run build
```
문제가 지속되면 오류 전문과 Node.js 버전을 기록하고 임의로 대규모 패키지
업데이트를 하지 않는다.
### 카드 이미지가 보이지 않는 경우
- 덱의 `series` 값이 올바른지 확인한다.
- 이미지 파일명이 실제 파일과 같은지 확인한다.
- `VITE_CARD_ASSET_BASE_URL` 값 끝에 불필요한 경로가 없는지 확인한다.
- 브라우저 개발자 도구의 Network 탭에서 404 또는 CORS 오류를 확인한다.

38
docs/history.md Normal file
View File

@@ -0,0 +1,38 @@
# 구조 및 의사결정 이력
## 2026-06-19: 독립 Vue 프로젝트로 시작
- 기존 UA Deck Builder와 별도 Git 저장소로 분리했다.
- 덱 빌더는 카드 선택과 덱 데이터 제공을 담당한다.
- Play Uniare는 카드 조작과 실시간 세션을 담당한다.
- 카드 이미지의 대규모 복제를 피하기 위해 기존 카드 자산 도메인을 참조한다.
## 2026-06-19: Vue 3, Vite, Tailwind CSS 채택
- 사용자가 직접 유지보수할 수 있는 Vue 3를 선택했다.
- 정적 빌드와 빠른 개발 서버를 위해 Vite를 사용한다.
- 일관된 UI 작성을 위해 Tailwind CSS를 기본 스타일 방식으로 정했다.
## 2026-06-19: 카드 소유자만 조작하는 권한 모델
- 플레이어는 자신이 소유한 카드만 조작한다.
- 전투나 효과로 내 카드가 제거되어도 내 카드 소유자가 직접 정리한다.
- 이 원칙으로 동시에 같은 카드를 수정하는 충돌을 줄인다.
- 온라인 모드에서는 서버가 소유권을 최종 검증한다.
## 2026-06-19: Docker 기반 NAS 배포
- UGREEN NAS에서 재현 가능한 배포를 위해 Docker Compose를 사용한다.
- Vue 빌드에는 Node.js를 사용하고 실행 이미지는 Nginx를 사용한다.
- 테스트와 운영 컨테이너를 서로 다른 포트로 분리한다.
## 2026-06-19: 기존 카드 이미지 읽기 전용 공유
- 기존 Uniare의 실제 웹 루트는
`/volume1/docker/projects/apps/uniare/config/www`로 확인했다.
- 카드 원본은 해당 웹 루트의 `images` 폴더를 사용한다.
- 새 프로젝트의 NAS 위치는
`/volume1/docker/projects/apps/playuniare`로 정했다.
- 이미지 중복 저장을 막기 위해 Docker bind mount를 사용한다.
- 원본 보호를 위해 컨테이너에는 읽기 전용으로 연결한다.
- 브라우저에는 같은 출처의 `/card-assets` 경로로 제공한다.

53
docs/map.md Normal file
View File

@@ -0,0 +1,53 @@
# 화면 및 파일 구조
## 현재 화면
### `/`
- 파일: `src/App.vue`
- 역할: 프로젝트 준비 상태를 보여주는 초기 랜딩 화면
- 현재 기능: 제품 목표 안내
- 다음 변경: 로비 화면과 게임 화면을 Vue Router로 분리
- 문서 제목과 기본 언어: `index.html`의 `Play Uniare`, 한국어
## 예정 화면
### `/`
- 로컬 연습 시작
- 방 만들기
- 방 참가
- 덱 쿼리 유효성 안내
### `/room/:roomCode`
- 상대 필드
- 공용 게임 보드
- 내 필드와 손패
- 덱·무덤·공개 영역
- 카드 조작 메뉴
- 연결 상태와 재접속 안내
## 예정 디렉터리 구조
```text
src/
├── components/
│ ├── cards/
│ ├── game-board/
│ └── room/
├── composables/
├── services/
├── stores/
├── types/
├── utils/
└── views/
```
- `components`: 화면에서 재사용하는 UI
- `composables`: Vue 반응형 조합 로직
- `services`: 쿼리 파싱, API, Socket.IO 연결
- `stores`: Pinia 기반 게임·방·사용자 상태
- `types`: JSDoc 타입 정의
- `utils`: Vue에 의존하지 않는 순수 함수
- `views`: Router 단위 화면

106
docs/spec.md Normal file
View File

@@ -0,0 +1,106 @@
# 프로젝트 기술 명세
## 프로젝트 개요
- 프로젝트명: Play Uniare
- 목적: Union Arena 덱을 불러와 카드 조작 중심의 로컬·온라인 플레이 제공
- 현재 단계: 프로젝트 기반 구성
- 프런트엔드: Vue 3, Vite, Tailwind CSS
- 배포 대상: UGREEN NAS의 Docker 환경
## 제품 범위
### 포함
- 기존 덱 빌더 쿼리에서 덱 불러오기
- 카드 수량만큼 독립 인스턴스 생성
- 덱 셔플과 드로우
- 카드 드래그 이동
- 카드 앞면·뒷면 전환
- 카드 탭·언탭
- 손패, 필드, 덱, 무덤, 공개 영역
- 덱 위·아래 또는 무덤으로 카드 이동
- 덱 위에서 여러 장을 공개한 후 목적지 지정
- 혼자 연습하는 로컬 모드
- 방 코드 기반 2인 세션
- 각 플레이어가 자신의 카드만 조작하는 권한 모델
### 제외
- 카드 효과와 게임 규칙 자동 판정
- 음성·텍스트 채팅
- 랭킹, 매칭, 대회 운영
- 초기 단계의 회원가입
## 덱 입력
초기에는 기존 덱 빌더 형식을 호환한다.
```text
?series=ua03&seriesName=HUNTER%C3%97HUNTER&cards=<encoded-json>
```
수신 후 다음 절차를 적용한다.
1. 쿼리 길이와 필수 필드를 검사한다.
2. JSON 디코딩 오류를 처리한다.
3. `series`, `imgSrc`, `Number`, `count`를 검증한다.
4. 카드 수량만큼 고유한 게임 카드 인스턴스를 만든다.
5. 이미지 주소는 환경변수 기반 카드 자산 원본과 조합한다.
장기적으로는 축약된 덱 코드 또는 서버의 `deckId` 방식으로 전환한다.
## 권한 모델
- 플레이어는 자기 카드만 이동·뒤집기·탭·정리할 수 있다.
- 상대 효과로 내 카드가 제거될 때에도 내가 직접 내 카드를 이동한다.
- 서버는 모든 카드 조작에서 요청 사용자와 카드 소유자를 비교한다.
- 상대 손패에는 장수만 공개하고 카드 식별 정보는 공개하지 않는다.
## 화면 좌표
- 배경 이미지는 카드 배치 가이드로 사용한다.
- 실제 영역은 배경 위의 투명한 드롭 영역으로 정의한다.
- 자유 배치 좌표는 보드 너비·높이 기준 0부터 1 사이 값으로 저장한다.
- 창 크기가 변해도 같은 상대 위치를 유지해야 한다.
## 상태 저장
- 로컬 모드: 브라우저 메모리, 추후 복구가 필요하면 localStorage 검토
- 온라인 모드: 서버가 방 상태의 최종 권위를 가진다.
- 초기 서버: Node.js와 Socket.IO, 메모리 기반 방 상태
- 운영 안정화 단계: Redis 또는 데이터베이스 저장 검토
## 환경변수 예정
```text
VITE_CARD_ASSET_BASE_URL=https://uniare.sori.studio/images
VITE_SOCKET_SERVER_URL=https://playuniare.sori.studio
```
실제 운영 값은 `.env`에 두고 Git에 커밋하지 않는다.
## NAS 카드 이미지 공유
운영 NAS에서는 카드 이미지를 새 프로젝트에 복사하지 않는다. 기존 Uniare
컨테이너가 사용하는 웹 루트의 이미지 폴더를 읽기 전용으로 공유한다.
```text
기존 Uniare 웹 루트:
/volume1/docker/projects/apps/uniare/config/www
카드 이미지 원본:
/volume1/docker/projects/apps/uniare/config/www/images
Play Uniare 컨테이너 내부:
/srv/uniare-card-images
브라우저 공개 경로:
/card-assets
```
- `config/www`는 기존 `index.html`이 존재하는 실제 웹 루트다.
- Docker bind mount는 `images` 폴더만 읽기 전용으로 연결한다.
- Play Uniare는 기존 카드 이미지를 읽을 수 있지만 수정하거나 삭제할 수 없다.
- 카드 이미지 URL은 `/card-assets/<series>/<fileName>` 형식을 사용한다.
- 같은 Play Uniare 도메인으로 제공되므로 별도의 CORS 설정이 필요하지 않다.

58
docs/todo.md Normal file
View File

@@ -0,0 +1,58 @@
# 개발 할 일
## 기반 작업
- [x] Vue 3와 Vite 프로젝트 생성
- [x] Tailwind CSS 설정
- [x] Docker 정적 배포 기반 작성
- [x] 개발·배포·AI 작업 문서 작성
- [ ] ESLint와 Prettier 규칙 확정
- [ ] Vue Router 설치와 화면 구조 생성
- [ ] Pinia 설치와 상태 구조 생성
- [ ] 테스트 도구 도입 범위 결정
## 1단계: 덱 불러오기와 로컬 플레이
- [ ] 기존 덱 쿼리 파서 작성
- [ ] 외부 입력 검증 스키마 작성
- [x] NAS 카드 이미지 읽기 전용 공유 경로 구성
- [ ] 카드 이미지 원본 환경변수를 프런트엔드 코드에 연결
- [ ] 카드 인스턴스 생성
- [ ] 게임 보드 배경과 영역 정의
- [ ] 덱 셔플과 드로우
- [ ] 카드 드래그 이동
- [ ] 앞면·뒷면 전환
- [ ] 탭·언탭
- [ ] 덱·손패·필드·무덤 이동
- [ ] 덱 위 N장 공개와 정리
- [ ] 로컬 상태 초기화
## 2단계: 온라인 2인 세션
- [ ] Node.js 실시간 서버 구성
- [ ] Socket.IO 연결
- [ ] 방 만들기와 방 코드 참가
- [ ] 좌석과 플레이어 소유권
- [ ] 카드 조작 권한 서버 검증
- [ ] 상대 손패 비공개 전송
- [ ] 상태 스냅샷과 재접속
- [ ] 연결 종료 처리
- [ ] NAS WebSocket 프록시 배포
## 3단계: 안정화
- [ ] 모바일·태블릿 Pointer Events 검증
- [ ] 카드 겹침과 z-index 조작 UX
- [ ] 잘못된 덱 입력 오류 화면
- [ ] 이미지 로딩 실패 폴백
- [ ] 접근성 키보드 조작 범위 검토
- [ ] 서버 재시작 시 방 종료 안내
- [ ] Redis 또는 상태 저장 필요성 검토
## 확인이 필요한 정책
- [ ] 카드 뒷면 이미지와 플레이매트 자산 확정
- [ ] 한 방의 최대 플레이어와 관전자 정책
- [ ] 방 유지 시간
- [ ] 사용하지 않는 방 자동 삭제 시간
- [ ] 덱 빌더와 시뮬레이터 사이의 최종 전달 형식

13
docs/update.md Normal file
View File

@@ -0,0 +1,13 @@
# 버그 수정 및 작업 기록
## 2026-06-19
- Vue 3와 Vite 기본 프로젝트를 생성했다.
- Tailwind CSS Vite 플러그인을 적용했다.
- 기본 샘플 화면을 프로젝트 안내 화면으로 교체했다.
- UGREEN NAS Docker 테스트·운영 배포 문서를 작성했다.
- AI 작업 지침과 초기 명세, 화면 구조, 할 일, 의사결정 문서를 추가했다.
- Vue 프로덕션 빌드, Docker Compose 설정, Docker 이미지 빌드와 Nginx
헬스체크를 검증했다.
- NAS의 기존 `uniare/config/www/images`를 읽기 전용으로 공유하는 Compose
설정과 `/card-assets` Nginx 경로를 추가했다.

17
index.html Normal file
View File

@@ -0,0 +1,17 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="Union Arena 덱을 불러와 연습하고 함께 플레이하는 웹 카드 시뮬레이터"
/>
<title>Play Uniare</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

1464
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

20
package.json Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "uniare-simulator",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.5.34"
},
"devDependencies": {
"@tailwindcss/vite": "^4.3.1",
"@vitejs/plugin-vue": "^6.0.6",
"tailwindcss": "^4.3.1",
"vite": "^8.0.12"
}
}

1
public/favicon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

24
public/icons.svg Normal file
View File

@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

40
src/App.vue Normal file
View File

@@ -0,0 +1,40 @@
<template>
<main class="min-h-screen bg-slate-950 px-6 py-16 text-slate-100">
<section class="mx-auto max-w-4xl">
<p class="mb-4 text-sm font-semibold tracking-[0.24em] text-cyan-400">
PLAY UNIARE
</p>
<h1 class="text-4xl font-black tracking-tight sm:text-6xl">
Union Arena Simulator
</h1>
<p class="mt-6 max-w-2xl text-base leading-7 text-slate-400 sm:text-lg">
덱 빌더에서 전달받은 카드로 혼자 연습하거나, 방을 만들어 상대와
실시간으로 플레이하는 시뮬레이터를 준비하고 있습니다.
</p>
<div class="mt-10 grid gap-4 sm:grid-cols-3">
<article class="rounded-2xl border border-slate-800 bg-slate-900 p-5">
<p class="text-sm font-bold text-cyan-300">01</p>
<h2 class="mt-3 text-lg font-bold">덱 불러오기</h2>
<p class="mt-2 text-sm leading-6 text-slate-400">
기존 덱 빌더의 쿼리 데이터를 안전하게 변환합니다.
</p>
</article>
<article class="rounded-2xl border border-slate-800 bg-slate-900 p-5">
<p class="text-sm font-bold text-cyan-300">02</p>
<h2 class="mt-3 text-lg font-bold">카드 조작</h2>
<p class="mt-2 text-sm leading-6 text-slate-400">
이동, 뒤집기, 탭과 덱·손패·무덤 이동을 제공합니다.
</p>
</article>
<article class="rounded-2xl border border-slate-800 bg-slate-900 p-5">
<p class="text-sm font-bold text-cyan-300">03</p>
<h2 class="mt-3 text-lg font-bold">2인 세션</h2>
<p class="mt-2 text-sm leading-6 text-slate-400">
각 플레이어가 자신의 카드만 조작하도록 동기화합니다.
</p>
</article>
</div>
</section>
</main>
</template>

5
src/main.js Normal file
View File

@@ -0,0 +1,5 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
createApp(App).mount('#app')

34
src/style.css Normal file
View File

@@ -0,0 +1,34 @@
@import 'tailwindcss';
:root {
font-family:
Inter, Pretendard, 'Noto Sans KR', system-ui, -apple-system, BlinkMacSystemFont,
'Segoe UI', sans-serif;
color: #e2e8f0;
background: #020617;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
* {
box-sizing: border-box;
}
html,
body,
#app {
min-width: 320px;
min-height: 100%;
}
html,
body {
background: #020617;
}
body {
margin: 0;
min-height: 100vh;
}

7
vite.config.js Normal file
View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [vue(), tailwindcss()],
})