286 lines
15 KiB
Markdown
286 lines
15 KiB
Markdown
# 프로젝트 기술 명세
|
||
|
||
## 프로젝트 개요
|
||
|
||
- 프로젝트명: Play Uniare
|
||
- 목적: Union Arena 덱을 불러와 카드 조작 중심의 로컬·온라인 플레이 제공
|
||
- 현재 단계: 로컬 카드 조작과 온라인 대기실 연결
|
||
- 프런트엔드: Vue 3, Vite, Tailwind CSS
|
||
- 배포 대상: UGREEN NAS의 Docker 환경
|
||
|
||
## 제품 범위
|
||
|
||
### 포함
|
||
|
||
- 기존 덱 빌더 쿼리에서 덱 불러오기
|
||
- 카드 수량만큼 독립 인스턴스 생성
|
||
- 덱 셔플과 드로우
|
||
- 카드 드래그 이동
|
||
- 카드 앞면·뒷면 전환
|
||
- 카드 탭·언탭
|
||
- 손패, 필드, 덱, 무덤, 공개 영역
|
||
- 덱 위·아래 또는 무덤으로 카드 이동
|
||
- 덱 위에서 여러 장을 공개한 후 목적지 지정
|
||
- 혼자 연습하는 로컬 모드
|
||
- 방 코드 기반 2인 세션
|
||
- 각 플레이어가 자신의 카드만 조작하는 권한 모델
|
||
- 비공개 방 코드 기반 대기실
|
||
- 덱 URL 등록과 기본 덱 구성 검증
|
||
- 양쪽 플레이어 준비 완료 후 게임 시작
|
||
|
||
### 제외
|
||
|
||
- 카드 효과와 게임 규칙 자동 판정
|
||
- 음성·텍스트 채팅
|
||
- 랭킹, 매칭, 대회 운영
|
||
- 초기 단계의 회원가입
|
||
|
||
## 덱 입력
|
||
|
||
초기에는 기존 덱 빌더 형식을 호환한다.
|
||
|
||
```text
|
||
?series=ua03&seriesName=HUNTER%C3%97HUNTER&cards=<encoded-json>
|
||
```
|
||
|
||
수신 후 다음 절차를 적용한다.
|
||
|
||
1. 쿼리 길이와 필수 필드를 검사한다.
|
||
2. JSON 디코딩 오류를 처리한다.
|
||
3. `series`, `imgSrc`, `Number`, `count`를 검증한다.
|
||
4. 카드 수량만큼 고유한 게임 카드 인스턴스를 만든다.
|
||
5. 이미지 주소는 환경변수 기반 카드 자산 원본과 조합한다.
|
||
6. 각 카드의 `count`만큼 고유한 `instanceId`를 가진 게임 카드로 확장한다.
|
||
7. 불러온 카드는 내 덱 영역에 뒷면으로 쌓는다.
|
||
|
||
장기적으로는 축약된 덱 코드 또는 서버의 `deckId` 방식으로 전환한다.
|
||
|
||
- 카드 종류별 수량은 1~100 범위만 허용한다.
|
||
- 한 번에 불러오는 전체 카드 수는 최대 100장으로 제한한다.
|
||
- 잘못된 카드 항목은 제외하고 화면에 오류 내용을 표시한다.
|
||
- `series`와 이미지 파일명은 허용된 문자와 확장자만 사용한다.
|
||
- 덱 쿼리가 없을 때에는 조작 검증용 연습 카드 3장을 표시한다.
|
||
|
||
## 덱 등록 검증
|
||
|
||
- 대기실에서 덱 빌더 결과 페이지 전체 URL을 등록한다.
|
||
- `series` 값 하나를 작품 식별자로 사용해 단일 작품 덱임을 확인한다.
|
||
- 메인 덱은 정확히 50장이어야 한다.
|
||
- 동일 카드 제한은 현재 데이터의 `Number`를 기준으로 최대 4장까지 검사한다.
|
||
- FINAL, COLOR, SPECIAL 트리거는 각각 합계 4장까지 허용한다.
|
||
- AP 색상 카드는 메인 덱에 포함하지 않고 별도 3장을 사용하는 것으로
|
||
검사한다.
|
||
- 현재 카드 데이터에는 카드 이름 전용 필드가 없으므로 동일 이름 제한을 카드
|
||
번호 기준으로 대체한다. 향후 이름 필드가 추가되면 이름 기준으로 전환한다.
|
||
|
||
## 대기실과 방
|
||
|
||
- `/`는 비공개 게임 대기실이다.
|
||
- 방장은 6자리 초대 코드를 생성한다.
|
||
- 참가자는 초대 코드를 입력해 같은 방에 참가한다.
|
||
- 각 플레이어는 자신의 덱 URL을 등록하고 준비 완료를 누른다.
|
||
- 두 플레이어의 연결·덱 등록·준비 완료가 모두 확인되어야 온라인 게임을
|
||
시작한다.
|
||
- 방 상태와 덱 등록 결과는 Socket.IO 서버가 최종 검증한다.
|
||
- 두 플레이어가 모두 준비하면 서버가 각 플레이어에게 자신의 덱 쿼리와
|
||
좌석을 전달하고 `/game`으로 자동 이동시킨다.
|
||
- 서버가 게임 시작 시 두 플레이어 중 한 명을 무작위 선공으로 지정한다.
|
||
- 온라인 게임에서는 사용자가 선공·후공을 직접 선택할 수 없다.
|
||
- 양쪽 플레이어가 첫 손패와 라이프를 확정한 뒤 선공 플레이어의 턴부터
|
||
시작한다.
|
||
- 현재 차례의 플레이어만 턴 종료를 요청할 수 있으며 서버가 상대에게 차례를
|
||
넘긴다.
|
||
- 방은 서버 메모리에 저장하므로 서버 재시작 시 종료된다.
|
||
- 브라우저 새로고침이나 짧은 네트워크 끊김에는 재접속 식별자로 같은 좌석을
|
||
복구한다.
|
||
- 내 덱 순서, 손패, 카드 위치와 준비 상태는 플레이어별 브라우저 저장소에
|
||
보관해 새로고침 후 복구한다.
|
||
- 개인 저장 상태는 상대 플레이어에게 전송하지 않는다.
|
||
- 연결이 끊긴 플레이어의 자리는 60초 동안 보존하고, 그 안에 복구되지 않으면
|
||
진행 중인 게임을 종료한다.
|
||
- 진행 중인 게임이 있는 사용자가 대기실이나 사이트 첫 화면으로 돌아오면
|
||
서버가 자신의 덱 쿼리와 좌석을 다시 전달해 게임 화면으로 복귀시킨다.
|
||
- 명시적인 `방 나가기`는 방 전체를 종료하고 양쪽 플레이어의 재접속 정보와
|
||
개인 게임 스냅샷을 삭제한다.
|
||
- 대기실에서 `현재 방 종료 후 새 방 만들기`로 먹통이 된 방을 정리하고 즉시
|
||
새 초대 코드를 만들 수 있다.
|
||
- 기능 개발을 위해 유효한 덱으로 `/game`에 진입하는 로컬 게임 테스트를
|
||
제공한다.
|
||
|
||
## 권한 모델
|
||
|
||
- 플레이어는 자기 카드만 이동·뒤집기·탭·정리할 수 있다.
|
||
- 상대 효과로 내 카드가 제거될 때에도 내가 직접 내 카드를 이동한다.
|
||
- 서버는 모든 카드 조작에서 요청 사용자와 카드 소유자를 비교한다.
|
||
- 상대 손패에는 장수만 공개하고 카드 식별 정보는 공개하지 않는다.
|
||
- 각 플레이어는 자신의 공개 카드 상태만 서버에 제출할 수 있다.
|
||
- 서버는 필드, 공개, 무덤, 라이프 카드만 상대에게 전달한다.
|
||
- 덱과 손패는 카드 목록을 보내지 않고 장수만 전달한다.
|
||
|
||
## 화면 좌표
|
||
|
||
- 배경 이미지는 카드 배치 가이드로 사용한다.
|
||
- 표준 플레이 보드 원본은 3840×2160의 16:9 이미지다.
|
||
- 표준 카드 비율은 너비÷높이 약 `0.716`으로 사용한다.
|
||
- 표준 카드 너비는 플레이 보드 전체 너비의 `5.75%`를 기준으로 한다.
|
||
- 실제 영역은 배경 위의 투명한 드롭 영역으로 정의한다.
|
||
- 자유 배치 좌표는 보드 너비·높이 기준 0부터 1 사이 값으로 저장한다.
|
||
- 드래그 중인 카드는 보드 경계를 벗어나지 않도록 카드 반쪽 크기만큼 좌표를
|
||
제한한다.
|
||
- 창 크기가 변해도 같은 상대 위치를 유지해야 한다.
|
||
- 보드 좌우 여백은 플레이어별 라이프, 덱·손패 장수, 연결 상태와 메모를
|
||
표시하는 정보 패널 영역으로 활용한다.
|
||
|
||
## 상태 저장
|
||
|
||
- 로컬 모드: 브라우저 메모리, 추후 복구가 필요하면 localStorage 검토
|
||
- 온라인 모드: 서버가 방 상태의 최종 권위를 가진다.
|
||
- 온라인 개인 비공개 상태: 현재 단계에서는 플레이어 브라우저 저장소에
|
||
암호화 없이 저장한다. 공용 PC에서는 게임 후 브라우저 데이터를 정리해야
|
||
한다.
|
||
- 초기 서버: Node.js와 Socket.IO, 메모리 기반 방 상태
|
||
- 운영 안정화 단계: Redis 또는 데이터베이스 저장 검토
|
||
- 서버의 공개 방 상태를 Pinia가 수신해 화면에 표시한다.
|
||
- 공개 방 상태에는 상대 덱의 작품명과 장수만 포함하며 카드 목록은 포함하지
|
||
않는다.
|
||
|
||
## 카드 직접 조작
|
||
|
||
- 포인터 입력으로 카드를 선택하고 자유롭게 이동한다.
|
||
- 카드 더블클릭으로 탭·언탭을 전환한다.
|
||
- 카드 우클릭 메뉴에서 앞면·뒷면, 탭·언탭, 맨 위로 이동을 조작한다.
|
||
- 탭 상태는 카드를 시계 방향으로 90도 회전해 표현한다.
|
||
- 카드를 드래그하거나 `맨 위로`를 누르면 `stackOrder`를 증가시킨다.
|
||
- 여러 카드가 겹친 경우 가장 큰 `stackOrder`를 가진 카드를 위에 표시한다.
|
||
- 탭 카드의 보드 경계는 회전된 실제 너비와 높이를 기준으로 제한한다.
|
||
- 뒷면 전용 이미지가 확정되기 전에는 공통 Union Arena 카드 뒷면 스타일을
|
||
CSS로 표시한다.
|
||
- 키보드 포커스로도 카드를 선택할 수 있어야 한다.
|
||
- 개발 기준 화면에서는 같은 카드의 독립 인스턴스 3장으로 겹침과 선택
|
||
동작을 검증한다.
|
||
- 온라인 게임에서 내 공개 카드는 상대 화면의 반대편 좌표로 변환해 표시한다.
|
||
- 상대 카드는 선택, 드래그, 더블클릭, 우클릭 조작을 할 수 없다.
|
||
- 공개 상태 전송에는 증가하는 리비전 번호를 붙이고, 서버와 상대 브라우저는
|
||
이전 리비전이 늦게 도착하면 무시한다.
|
||
|
||
## 덱·손패·무덤 동작
|
||
|
||
- 덱 카드는 보드의 내 덱 위치에 뒷면으로 겹쳐 표시한다.
|
||
- 온라인 게임에서 서버는 카드 내용이 아닌 인스턴스 식별자 순서만 비공개로
|
||
관리한다.
|
||
- 온라인 셔플은 서버의 암호학적 난수로 순서를 결정한다.
|
||
- 온라인 드로우, 덱 위 여러 장 공개, 라이프 배치는 서버가 반환한 맨 위 카드
|
||
식별자를 사용한다.
|
||
- 덱 위·아래 이동은 서버의 비공개 덱 순서에도 같은 위치로 반영한다.
|
||
- `1장 드로우`는 가장 높은 `stackOrder`의 덱 카드를 손패로 이동한다.
|
||
- 손패 카드는 앞면으로 표시하고 보드 하단 중앙에 자동 정렬한다.
|
||
- 손패는 플레이매트 위가 아니라 화면 하단의 별도 한 줄 트레이에 표시한다.
|
||
- 손패 카드는 필드 카드보다 작게 표시하고 장수가 많아지면 가로로 스크롤한다.
|
||
- 플레이매트의 `ACTION POINT AREA`는 손패 카드로 가리지 않는다.
|
||
- 손패·덱·무덤 카드를 드래그하면 필드 카드로 전환한다.
|
||
- 셔플은 덱 카드의 레이어 순서를 무작위로 다시 배정하고 모두 뒷면·언탭으로
|
||
정리한다.
|
||
- 우클릭 메뉴에서 선택 카드를 손패, 덱 위, 덱 아래, 무덤으로 이동한다.
|
||
- 덱 위 카드는 가장 큰 `stackOrder`, 덱 아래 카드는 가장 작은
|
||
`stackOrder`를 사용한다.
|
||
- 무덤 카드는 앞면으로 무덤 기준 위치에 겹쳐 표시한다.
|
||
- 플레이어 정보 패널의 덱·손패 장수는 현재 영역 상태에서 계산한다.
|
||
- 덱 패널에서 1~10장 사이의 공개 수량을 입력한다.
|
||
- 공개 명령은 덱 위에서 지정한 장수를 가져와 보드 중앙 공개 영역에 앞면으로
|
||
정렬한다.
|
||
- 공개 영역의 카드는 우클릭 메뉴로 각각 덱 위·덱 아래·손패·무덤에
|
||
정리한다.
|
||
- 공개 영역 카드가 이동하면 남은 공개 카드를 다시 중앙 정렬한다.
|
||
|
||
## 게임 준비와 턴 시작
|
||
|
||
- 유효한 덱으로 게임에 진입하면 덱을 섞고 첫 손패 7장을 드로우한다.
|
||
- 멀리건은 한 번만 가능하다.
|
||
- 멀리건은 기존 손패를 제외한 덱에서 새 7장을 먼저 뽑고, 기존 손패를 덱에
|
||
되돌린 뒤 섞는다.
|
||
- 손패 확정 시 덱 위 7장을 보지 않고 라이프 영역에 뒷면으로 배치한다.
|
||
- AP 카드는 별도 3장을 사용하며 현재 화면에서는 사용 가능 AP를 숫자로
|
||
표시한다.
|
||
- 선공 1턴 AP 1, 2턴 AP 2, 3턴 이후 AP 3으로 갱신한다.
|
||
- 후공 1턴 AP 2, 2턴 이후 AP 3으로 갱신한다.
|
||
- 선공 첫 턴에는 자동 드로우하지 않고 그 외 턴 시작에는 1장 드로우한다.
|
||
- 턴 시작 시 내 필드의 탭 카드를 모두 언탭한다.
|
||
- `isUntapLocked`가 설정된 카드는 턴 시작 자동 언탭에서 제외한다.
|
||
- 언탭 제외 잠금은 카드 우클릭 메뉴에서 설정·해제한다.
|
||
- 온라인 게임에서는 서버의 현재 차례가 내 플레이어로 바뀔 때 턴 시작
|
||
언탭, AP 갱신과 드로우를 한 번 적용한다.
|
||
|
||
## 게임 화면
|
||
|
||
- 실제 게임 화면에는 고정 헤더를 표시하지 않는다.
|
||
- 16:9 플레이 보드를 뷰포트 안에서 가능한 가장 크게 표시한다.
|
||
- 하단 손패 트레이 높이를 제외한 영역에서 16:9 플레이 보드를 가능한 가장
|
||
크게 표시한다.
|
||
- 남는 공간은 검은색 레터박스로 처리한다.
|
||
- `F` 키 또는 우측 상단의 반투명 버튼으로 브라우저 전체화면을 전환한다.
|
||
- `Esc` 키로 브라우저 전체화면과 카드 우클릭 메뉴를 닫을 수 있다.
|
||
- 카드 크기 기준 가이드는 기본적으로 숨긴다.
|
||
|
||
## 포트 정책
|
||
|
||
- Vue 개발 서버는 Vite가 제시하는 포트를 사용하며 기본값은 보통 `5173`이다.
|
||
- 로컬 Socket.IO 서버 기본 포트는 `31873`이다.
|
||
- Docker 컨테이너 내부 포트는 다른 컨테이너와 같아도 충돌하지 않는다.
|
||
- NAS 호스트에 공개하는 포트는 다른 서비스와 겹치면 안 된다.
|
||
- NAS 운영 HTTP 호스트 포트 기본값은 `41809`다.
|
||
- 운영 HTTP 호스트 포트는 `.env`의
|
||
`PLAYUNIARE_PRODUCTION_HTTP_PORT`로 변경한다.
|
||
- 기능 테스트는 로컬에서 수행하고 NAS에는 운영 컨테이너만 배포한다.
|
||
- 운영 브라우저는 Socket.IO 포트를 직접 호출하지 않고
|
||
`http://playuniare.sori.studio/socket.io`처럼 같은 도메인의 리버스 프록시
|
||
경로를 사용한다.
|
||
- 포트 번호는 코드에 하드코딩하지 않고 환경변수와 Compose 설정으로 관리한다.
|
||
|
||
## 환경변수
|
||
|
||
```text
|
||
VITE_CARD_ASSET_BASE_URL=/card-assets
|
||
```
|
||
|
||
브라우저 Socket.IO 클라이언트는 별도 서버 URL 없이 현재 접속한 출처의
|
||
`/socket.io`를 사용한다. 로컬 서버 포트를 바꿀 때만 실행 명령에
|
||
`SOCKET_SERVER_PORT`를 지정한다. 실제 운영 값은 `.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는 기존 카드 이미지를 읽을 수 있지만 수정하거나 삭제할 수 없다.
|
||
- Nginx 작업 프로세스는 NAS 카드 파일 소유자와 같은 UID `1000`의
|
||
`cardreader` 사용자로 실행한다.
|
||
- 카드 이미지 URL은 `/card-assets/<series>/<fileName>` 형식을 사용한다.
|
||
- 프런트엔드는 `VITE_CARD_ASSET_BASE_URL`과 시리즈·파일명을 조합해 카드
|
||
이미지 URL을 생성한다.
|
||
- 같은 Play Uniare 도메인으로 제공되므로 별도의 CORS 설정이 필요하지 않다.
|
||
|
||
## 배포 환경
|
||
|
||
- 로컬 개발: Vite 개발 서버
|
||
- 로컬 배포 검증: Docker Compose
|
||
- NAS 운영: `compose.yaml`과 `compose.nas.yaml`
|
||
- 운영 주소: `http://playuniare.sori.studio`
|
||
- NAS 테스트 컨테이너는 별도로 유지하지 않는다.
|