Files
playuniare.sori.studio/docs/spec.md

12 KiB
Raw Blame History

프로젝트 기술 명세

프로젝트 개요

  • 프로젝트명: Play Uniare
  • 목적: Union Arena 덱을 불러와 카드 조작 중심의 로컬·온라인 플레이 제공
  • 현재 단계: 로컬 카드 조작과 온라인 대기실 연결
  • 프런트엔드: Vue 3, Vite, Tailwind CSS
  • 배포 대상: UGREEN NAS의 Docker 환경

제품 범위

포함

  • 기존 덱 빌더 쿼리에서 덱 불러오기
  • 카드 수량만큼 독립 인스턴스 생성
  • 덱 셔플과 드로우
  • 카드 드래그 이동
  • 카드 앞면·뒷면 전환
  • 카드 탭·언탭
  • 손패, 필드, 덱, 무덤, 공개 영역
  • 덱 위·아래 또는 무덤으로 카드 이동
  • 덱 위에서 여러 장을 공개한 후 목적지 지정
  • 혼자 연습하는 로컬 모드
  • 방 코드 기반 2인 세션
  • 각 플레이어가 자신의 카드만 조작하는 권한 모델
  • 비공개 방 코드 기반 대기실
  • 덱 URL 등록과 기본 덱 구성 검증
  • 양쪽 플레이어 준비 완료 후 게임 시작

제외

  • 카드 효과와 게임 규칙 자동 판정
  • 음성·텍스트 채팅
  • 랭킹, 매칭, 대회 운영
  • 초기 단계의 회원가입

덱 입력

초기에는 기존 덱 빌더 형식을 호환한다.

?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으로 자동 이동시킨다.
  • 방은 서버 메모리에 저장하므로 서버 재시작 시 종료된다.
  • 기능 개발을 위해 유효한 덱으로 /game에 진입하는 로컬 게임 테스트를 제공한다.

권한 모델

  • 플레이어는 자기 카드만 이동·뒤집기·탭·정리할 수 있다.
  • 상대 효과로 내 카드가 제거될 때에도 내가 직접 내 카드를 이동한다.
  • 서버는 모든 카드 조작에서 요청 사용자와 카드 소유자를 비교한다.
  • 상대 손패에는 장수만 공개하고 카드 식별 정보는 공개하지 않는다.

화면 좌표

  • 배경 이미지는 카드 배치 가이드로 사용한다.
  • 표준 플레이 보드 원본은 3840×2160의 16:9 이미지다.
  • 표준 카드 비율은 너비÷높이 약 0.716으로 사용한다.
  • 표준 카드 너비는 플레이 보드 전체 너비의 5.75%를 기준으로 한다.
  • 실제 영역은 배경 위의 투명한 드롭 영역으로 정의한다.
  • 자유 배치 좌표는 보드 너비·높이 기준 0부터 1 사이 값으로 저장한다.
  • 드래그 중인 카드는 보드 경계를 벗어나지 않도록 카드 반쪽 크기만큼 좌표를 제한한다.
  • 창 크기가 변해도 같은 상대 위치를 유지해야 한다.
  • 보드 좌우 여백은 플레이어별 라이프, 덱·손패 장수, 연결 상태와 메모를 표시하는 정보 패널 영역으로 활용한다.

상태 저장

  • 로컬 모드: 브라우저 메모리, 추후 복구가 필요하면 localStorage 검토
  • 온라인 모드: 서버가 방 상태의 최종 권위를 가진다.
  • 초기 서버: Node.js와 Socket.IO, 메모리 기반 방 상태
  • 운영 안정화 단계: Redis 또는 데이터베이스 저장 검토
  • 서버의 공개 방 상태를 Pinia가 수신해 화면에 표시한다.
  • 공개 방 상태에는 상대 덱의 작품명과 장수만 포함하며 카드 목록은 포함하지 않는다.

카드 직접 조작

  • 포인터 입력으로 카드를 선택하고 자유롭게 이동한다.
  • 카드 더블클릭으로 탭·언탭을 전환한다.
  • 카드 우클릭 메뉴에서 앞면·뒷면, 탭·언탭, 맨 위로 이동을 조작한다.
  • 탭 상태는 카드를 시계 방향으로 90도 회전해 표현한다.
  • 카드를 드래그하거나 맨 위로를 누르면 stackOrder를 증가시킨다.
  • 여러 카드가 겹친 경우 가장 큰 stackOrder를 가진 카드를 위에 표시한다.
  • 탭 카드의 보드 경계는 회전된 실제 너비와 높이를 기준으로 제한한다.
  • 뒷면 전용 이미지가 확정되기 전에는 공통 Union Arena 카드 뒷면 스타일을 CSS로 표시한다.
  • 키보드 포커스로도 카드를 선택할 수 있어야 한다.
  • 개발 기준 화면에서는 같은 카드의 독립 인스턴스 3장으로 겹침과 선택 동작을 검증한다.

덱·손패·무덤 동작

  • 덱 카드는 보드의 내 덱 위치에 뒷면으로 겹쳐 표시한다.
  • 1장 드로우는 가장 높은 stackOrder의 덱 카드를 손패로 이동한다.
  • 손패 카드는 앞면으로 표시하고 보드 하단 중앙에 자동 정렬한다.
  • 손패 카드가 많아지면 제한된 폭 안에서 카드 간격을 줄인다.
  • 손패·덱·무덤 카드를 드래그하면 필드 카드로 전환한다.
  • 셔플은 덱 카드의 레이어 순서를 무작위로 다시 배정하고 모두 뒷면·언탭으로 정리한다.
  • 우클릭 메뉴에서 선택 카드를 손패, 덱 위, 덱 아래, 무덤으로 이동한다.
  • 덱 위 카드는 가장 큰 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가 설정된 카드는 턴 시작 자동 언탭에서 제외한다.
  • 언탭 제외 잠금은 카드 우클릭 메뉴에서 설정·해제한다.

게임 화면

  • 실제 게임 화면에는 고정 헤더를 표시하지 않는다.
  • 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 설정으로 관리한다.

환경변수

VITE_CARD_ASSET_BASE_URL=/card-assets

브라우저 Socket.IO 클라이언트는 별도 서버 URL 없이 현재 접속한 출처의 /socket.io를 사용한다. 로컬 서버 포트를 바꿀 때만 실행 명령에 SOCKET_SERVER_PORT를 지정한다. 실제 운영 값은 .env에 두고 Git에 커밋하지 않는다.

NAS 카드 이미지 공유

운영 NAS에서는 카드 이미지를 새 프로젝트에 복사하지 않는다. 기존 Uniare 컨테이너가 사용하는 웹 루트의 이미지 폴더를 읽기 전용으로 공유한다.

기존 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 테스트 컨테이너는 별도로 유지하지 않는다.