WallpaperMachine
The wallpapers you love. A new home on your Mac.
머더미스터리 진행용 단계 타이머
Translation pending
LifestyleTypeScript · JavaScriptGitHub @yongyooo22OnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Loading the full README. The saved excerpt is shown below.
친구들과 테이블 위에 휴대폰이나 태블릿 한 대를 두고 함께 보는 머더미스터리 진행용 단계 타이머입니다. 시나리오(단계 이름 + 시간 목록)를 만들어 두고, 게임 중에는 멀리서도 읽히는 큰 숫자로 남은 시간을 보여 줍니다. • 시나리오 목록: 서버에 저장되어 앱을 쓰는 모든 사용자에게 공유됩니다(Vercel 배포: Upstash Redis, 로컬 실행: 파일). 새로고침 버튼으로만 다시 불러옵니다(자동 폴링 없음). • 게임 진행: 시작할 때 시나리오 구성을 기기에 복사해 진행합니다. 진행 상황은 이 기기에만 저장되며 새로고침해도 이어집니다. • 진행 결과: 단계별 계획 시간과 실제 진행 시간(일시정지 제외, 초과 포함)을 비교합니다. 배포는 Vercel + Upstash Redis를 기준으로 합니다(아래 ‘Vercel 배포’). 내 컴퓨터에서 실행할 때는 Node.js 20.19 이상이 필요합니다.Read the full README ↗
친구들과 테이블 위에 휴대폰이나 태블릿 한 대를 두고 함께 보는 머더미스터리 진행용 단계 타이머입니다. 시나리오(단계 이름 + 시간 목록)를 만들어 두고, 게임 중에는 멀리서도 읽히는 큰 숫자로 남은 시간을 보여 줍니다.
배포는 Vercel + Upstash Redis를 기준으로 합니다(아래 ‘Vercel 배포’). 내 컴퓨터에서 실행할 때는 Node.js 20.19 이상이 필요합니다.
npm install
npm run build # 프론트엔드 빌드(dist/)
npm start # http://localhost:8787 — 앱 화면과 API를 함께 제공
로컬 실행은 기본적으로 data/scenarios.json 파일에 저장합니다. Upstash 환경 변수(KV_REST_API_URL·KV_REST_API_TOKEN)를
셸에 넣고 실행하면 로컬 서버도 Redis에 저장합니다(배포된 앱과 같은 데이터를 보게 되므로 주의).
개발 중에는 npm run dev로 API 서버(8787)와 Vite 개발 서버(5173)를 함께 띄웁니다. Vite가 /api 요청을 API 서버로 넘깁니다.
같은 Wi‑Fi의 휴대폰에서 http://<컴퓨터 IP>:8787로 접속할 수 있습니다. 다만 화면 꺼짐 방지(Wake Lock)와
오프라인 실행(서비스 워커)은 HTTPS 또는 localhost에서만 동작하므로, 실제로 쓸 때는 HTTPS 주소로 배포하는 것을 권장합니다.
| 이름 | 기본값 | 설명 |
|---|---|---|
KV_REST_API_URL, KV_REST_API_TOKEN | (없음) | Upstash Redis REST 주소·토큰. Vercel Storage로 연결하면 자동으로 들어옵니다. |
UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN | (없음) | 위와 같은 값의 다른 이름(Upstash 콘솔에서 직접 넣는 경우). KV_*가 있으면 그쪽을 씁니다. |
TRASH_RETENTION_DAYS | 30 | 휴지통 보관 기간. 지나면 영구 삭제 |
CORS_ORIGIN | (없음) | 앱 화면을 다른 주소에 올릴 때 허용할 출처. 예: https://example.github.io |
PORT | 8787 | 로컬 서버 포트 |
HOST | 0.0.0.0 | 로컬 서버 바인딩 주소 |
DATA_DIR | ./data | 로컬 파일 저장 폴더(scenarios.json). Redis 환경 변수가 없을 때만 씁니다. |
STATIC_DIR | ./dist | 로컬 서버가 제공할 빌드된 앱 화면 폴더 |
환경 변수 이름 앞에 앱 이름을 붙이지 않습니다(예: MYAPP_KV_REST_API_URL은 읽지 않음). 여러 앱이 같은 Redis를 쓸 때의 구분은
아래의 Redis 키 접두사로 합니다.
앱 화면과 서버를 다른 주소에 올릴 때는 빌드할 때 VITE_API_BASE=https://서버주소 npm run build로 API 주소를 지정합니다.
화면은 상대 경로로 빌드되고 해시 라우팅(#/...)을 쓰므로 GitHub Pages 같은 정적 호스팅의 하위 경로에도 올릴 수 있습니다.
이 서버에는 로그인이 없습니다. 주소를 아는 사람은 누구나 시나리오를 만들고 고치고 지울 수 있습니다(휴지통에서 30일간 복원 가능). 공개 인터넷에 올린다면 접근 제한(예: 리버스 프록시의 기본 인증)을 함께 두는 것을 권장합니다.
이미 다른 앱들이 쓰는 Upstash Redis 데이터베이스를 새로 만들지 않고 함께 씁니다. 이 앱의 데이터는 전용 키 접두사로 분리됩니다.
vercel.json에 있으므로 바꿀 것이 없습니다
(Framework: Vite, Build: npm run build, Output: dist, API: api/index.js).KV_REST_API_URL, KV_REST_API_TOKEN이 Production(필요하면 Preview도)에 들어왔는지 확인합니다.https://<배포 주소>/api/health를 열어 {"ok":true,"storage":"redis"}가 나오는지 확인합니다.
환경 변수가 없으면 503과 함께 확인할 변수 이름을 알려 줍니다(그때도 파일에 몰래 저장하지 않습니다).로그인이 없으므로 배포 주소를 아는 사람은 누구나 시나리오를 만들고 고치고 지울 수 있습니다(휴지통에서 30일간 복원 가능).
이 앱은 Redis에 아래 두 키만 씁니다. 접두사 murder-mystery-timer는 server/redis-store.mjs의 KEY_PREFIX로 고정되어 있습니다.
| 키 | 형식 | 내용 |
|---|---|---|
murder-mystery-timer:scenarios | 해시 | 필드 = 시나리오 id, 값 = 시나리오 JSON(목록에 보이는 것) |
murder-mystery-timer:trash | 해시 | 필드 = 시나리오 id, 값 = 시나리오 JSON(휴지통, deletedAt 있음) |
scenarios.json의 항목과 같습니다: { id, name, stages: [{ id, name, durationSec }], createdAt, updatedAt, rev, deletedAt }.WRITE_SCRIPT)로 합니다. 읽어 둔 값이 그대로일 때만 쓰는 compare-and-set이라
여러 사람이 동시에 저장해도 서로의 변경을 덮어쓰지 않습니다. 같은 버전을 보고 동시에 편집하면 먼저 저장한 쪽만 저장되고,
나머지는 지금처럼 ‘다른 기기에서 먼저 수정했어요’(409) 확인창을 받습니다. 목록+휴지통 500개 한도도 스크립트 안에서 함께 확인합니다.KEYS·SCAN·FLUSHDB·FLUSHALL·DEL을 쓰지 않고, 위 두 키 밖의 키는 읽지도 쓰지도 않습니다.
요청에 들어온 id는 해시 안의 필드 이름으로만 쓰이므로 다른 키를 가리킬 수 없습니다. 보관 기간이 지난 휴지통 정리도 murder-mystery-timer:trash 안에서만 합니다.| 키 | 내용 |
|---|---|
mt:scenarios:v1 | 마지막으로 받은 목록 사본(오프라인일 때 표시) |
mt:settings:v1 | 알림음·진동·화면 꺼짐 방지 설정, 목록 정렬 |
mt:game:v1 | 진행 중인 게임 |
mt:result:v1 | 마지막 게임 결과 |
src/config.ts의 name(지금은 Murder Mystery Timer), shortName(홈 화면 아이콘 아래 이름, Murder Timer)만 고치면 됩니다.
화면 상단, 브라우저 탭 제목, 홈 화면 아이콘 이름(manifest·iOS)에 모두 반영됩니다.
아이콘 이미지는 public/icons/*.svg를 고친 뒤 npm run icons로 PNG를 다시 만듭니다.
성인들이 게임 중에 쓰는 차분한 도구를 목표로, 장식 없이 색·글꼴·여백·얇은 구분선으로 정리했습니다. 값은 모두 src/styles/tokens.css에 있습니다.
| 항목 | 값 |
|---|---|
| 배경 | 차콜 #1C1D1F, 목록·팝업 면은 한 톤 밝은 #252528(두꺼운 테두리·큰 그림자 없이) |
| 글자 | 아이보리 #F0E9DF(14.0:1), 보조 #ADA9A5(7.2:1), 안내 #A39F9B(6.4:1) — 목록 면 위에서도 5.8:1 이상 |
| 강조 | 버건디 #873E4D — 주요 버튼 면에만 쓴다(아이보리 글자 6.2:1). 차콜 위의 선(현재 단계 표시·진행 바)은 같은 계열의 밝은 #B45567(3.6:1) |
| 상태 | 1분 이하 앰버 #F0AE45(8.3:1), 시간 초과 빨강 #F47265(5.7:1), 삭제·종료 확인은 빨간 테두리 버튼 |
| 구분선 | #36363A(배경보다 조금 밝은 회색), 테두리 버튼·입력칸 #76726F(배경·목록 면 대비 3:1 이상) |
| 글꼴 | 기본은 본명조(Noto Serif KR). 첫 화면·새 시나리오 창은 앱 이름·시나리오 제목만 명조, 설명·시간·버튼·메뉴는 Pretendard |
| 버튼 | 높이·터치 영역 최소 44px. 모서리 4px(첫 화면은 6px). 화면마다 버건디로 채운 주요 버튼은 하나만 |
@fontsource-variable/noto-serif-kr, pretendard, 모두 OFL)이라
화면에 실제로 쓰인 글자 묶음만 내려받고, 받은 글꼴은 서비스 워커가 보관합니다. 글꼴을 받지 못하면 시스템 글꼴로 표시됩니다.가로 화면(태블릿·PC, 휴대폰을 눕혀 테이블에 두는 경우)을 기본으로 배치했습니다. 세로 화면에서도 같은 기능을 위에서 아래로 쌓아 씁니다. 첫 화면은 가운데 한 열(최대 1,040px)로, 나머지 화면의 가로 배치에서는 두 칸 사이에 얇은 세로선 하나만 둡니다.
| 화면 | 주소 | 내용 |
|---|---|---|
| 시나리오 목록 | #/ | 가운데 한 열: (진행 중인 게임) → ‘내 시나리오’ 개수·새 시나리오 → 검색칸·정렬 → 목록(제목 명조, 단계 수·총 시간, 실행·⋯ 편집/복제/삭제) → 새로고침·갱신 시각 / 휴지통 |
| 새 시나리오(창) | 목록의 ‘새 시나리오’ | 조사 2회·조사 3회 템플릿 또는 직접 구성 → 바로 편집 화면(저장해야 목록에 추가) |
| 시나리오 편집 | #/new, #/edit/:id | 가로: 왼쪽 이름·단계 목록(이름 옆에 분·초), 오른쪽 총 계획 시간·저장과 빠른 입력. 저장하면 목록으로 |
| 게임 진행 | #/play | 목록의 ‘실행’으로 들어오면 ‘시작 전’으로 기다리고, ‘시작’을 눌러야 카운트다운이 시작된다. 아래 ‘진행 화면 배치’ 참고 |
| 진행 결과 | #/result | 가로: 왼쪽 계획/실제/차이 요약과 버튼, 오른쪽 단계별 계획·실제·차이 표 |
| 휴지통(보조 화면) | #/trash | 복원, 영구 삭제 |
| 설정(모달) | 목록 상단 버튼 | 알림음, 진동, 화면 꺼짐 방지(이 기기에만 저장). 전체화면을 못 쓰는 브라우저에서는 ‘홈 화면에 추가’ 안내 |
03 / 07) → 상태(진행 중·일시정지 …) → 현재 단계 이름(명조) → 남은 시간(가장 크게) → 얇은 진행 바 →
다음 단계 │ 전체 남은 시간 → −1분 / 일시정지(유일한 버건디 버튼) / +1분 → 이전 단계 / 다음 단계. 오른쪽 위에 알림음·전체화면 아이콘.aria-current), 남은 단계는 빈 원. 두 자리 순번(01).
단계가 많으면 목록만 스크롤되고 현재 단계가 보이도록 맞춥니다. 행을 눌러도 이동하지 않습니다(단계 이동은 이전·다음 단계 버튼으로만).+01:23 형식으로 계속 셉니다.
이때 주요 버튼이 ‘다음 단계 시작’으로, 마지막 단계에서는 ‘게임 마치기’로 바뀌고, 일시정지·계속하기는 아래 줄로 옮겨집니다.rev 비교), 덮어쓸지 고르게 합니다.한 줄에 한 단계, 줄 끝에 시간을 적습니다. 빈 줄과 #으로 시작하는 줄은 건너뜁니다.
사건 소개 5 ← 숫자만 쓰면 분
1차 조사 20분
1차 토론 15:30 ← 분:초
긴 조사 1:30:00 ← 시:분:초
해설 7분 30초
api/index.js Vercel 서버리스 함수(모든 /api/* 요청, Redis 저장소만 사용)
vercel.json Vercel 빌드·라우팅 설정
server/app.mjs API 처리기(로컬 서버와 Vercel 함수가 함께 씀)
server/redis*.mjs Upstash Redis REST 클라이언트와 Redis 저장소(키 접두사, compare-and-set 스크립트)
server/store.mjs 로컬 파일 저장소(로컬 실행·테스트용)
server/index.mjs 로컬 Node 서버(정적 파일 + API, 의존성 없음)
shared/limits.json 서버·화면이 함께 쓰는 입력 제한(이름 길이, 단계 수, 최대 시간)
src/config.ts 앱 이름(한 곳에서 변경)
src/styles/ 디자인 토큰(tokens.css)과 기본 스타일(글꼴은 src/main.tsx에서 불러옴)
src/components/ 공통 컴포넌트: Button·IconButton, TextField, SearchField, Modal·ConfirmDialog, Menu, InlineAlert, Toast, Switch, EmptyState, Icon …
src/screens/ 화면(목록, 새 시나리오 창, 편집, 진행, 결과, 휴지통, 설정)
src/game/ 진행 계산(engine.ts, 순수 함수)과 기기 저장
src/data/ API 클라이언트, 목록 캐시 저장소, 편집 초안 검사, 템플릿
색·글자 크기·간격·모서리·버튼 높이는 모두 src/styles/tokens.css의 토큰으로 관리합니다.
| 메서드·경로 | 설명 |
|---|---|
GET /api/health | 상태 확인 { ok, storage: 'redis' | 'file' }. 저장소에 닿지 못하면 503 |
GET /api/scenarios | 목록(휴지통 제외), 최근 수정순 |
POST /api/scenarios | 만들기 { name, stages: [{ id?, name, durationSec }] } |
PUT /api/scenarios/:id | 고치기 { name, stages, rev } — rev가 다르면 409 conflict |
PATCH /api/scenarios/:id | 이름 바꾸기 { name } |
POST /api/scenarios/:id/duplicate | 복제(‘이름 (복사본)’) |
DELETE /api/scenarios/:id | 휴지통으로 이동 |
GET /api/trash | 휴지통 목록 |
POST /api/trash/:id/restore | 복원 |
DELETE /api/trash/:id | 영구 삭제 |
npm test # 단위 테스트(vitest) + 서버 API 테스트(node:test, 파일·Redis 저장소 모두)
npm run test:e2e # 빌드 후 Playwright E2E(Chromium: 가로 휴대폰·가로 태블릿·세로 휴대폰)
npm run qa:screens # 화면 점검: 여러 화면 크기·상태 스크린샷 + 가로 넘침/터치 영역/겹침 자동 검사
서버 API 테스트는 같은 시나리오를 세 저장소로 실행합니다: 파일, Redis(메모리 대역), Redis(실제 redis-server — 설치되어 있을 때만,
실제 Lua 스크립트 실행). Redis 테스트는 server/testing/upstash-test-server.mjs가 띄우는 Upstash REST 호환 서버를 쓰며 실제 Upstash에는 연결하지 않습니다.
키 격리(다른 앱의 키를 건드리지 않음), 동시 저장, 개수 한도, Vercel 요청 본문 형식, 환경 변수 누락도 검사합니다.
E2E 테스트 서버는 데이터를 지우므로 셸에 Redis 환경 변수가 있어도 무시하고 임시 파일 저장소로 뜹니다.
qa:screens도 실행 중인 서버의 데이터를 지우고 점검용 시나리오로 채우므로, 파일 저장소로 띄운 점검 전용 서버에서만 실행하세요(Redis 저장소 서버에서는 실행을 거부합니다).
DATA_DIR=/tmp/mt-qa npm start &
npm run qa:screens # 결과: e2e/screenshots/*.png, report.json
Playwright(Chromium) 기기 흉내로 아래 크기에서 화면을 캡처해 확인했습니다(npm run qa:screens, 12개 크기 × 33~34개 상태 = 400장).
화면마다 가로 넘침, 44px 미만 터치 영역, 진행 화면 요소 겹침(숫자·단계 이름·상태·진행 바·조작·아이콘·왼쪽 칸), 버튼 글자 잘림,
조작 버튼이 첫 화면 밖으로 밀림, 단계·시나리오·진행 순서 이름 잘림, 진행 순서에서 현재 단계가 안 보임, 숫자가 가장 큰 글자가 아님,
시나리오 이름이 안 보임을 자동 검사했고 모두 통과했습니다.
진행 화면의 남은 시간 숫자 크기(측정값, 14:32 / +01:23 / 1:29:30 기준)
이 환경에서는 실제 기기와 브라우저, Vercel·Upstash 계정을 쓸 수 없어 아래는 확인하지 못했습니다.
실제 Vercel 배포와 실제 Upstash Redis 연결(Upstash REST 형식을 따르는 테스트 서버 + 실제 redis-server 7.0으로만 확인)
req.body로 넘기는 경우와 직접 읽는 경우를 모두 처리하도록 만들고 테스트했지만, 실제 Vercel에서는 확인하지 못했습니다.실제 iPhone Safari / Android Chrome / iPad에서의 표시와 동작(점검은 Linux Chromium의 기기 흉내로만 했습니다)
알림음 재생, 진동, 화면 꺼짐 방지(Wake Lock), 전체화면 전환의 실제 동작
실제 화면 키보드가 올라왔을 때 저장 영역이 입력칸을 가리지 않는지(키보드가 열리면 하단 고정을 푸는 방식으로 구현)
터치 화면에서 손잡이 끌기와 페이지 스크롤이 겹치지 않는지(마우스 끌기와 키보드 이동은 E2E로 확인)
노치·홈 표시줄 안전 여백(safe-area)이 실제로 적용된 모습
홈 화면에 추가한 앱(PWA)으로의 실행과 실제 휴대폰에서의 오프라인 실행 (헤드리스 Chromium에서 오프라인 상태로 새로고침했을 때 앱 화면과 저장된 목록이 열리는 것까지는 확인)
백그라운드에 오래 있다가 돌아왔을 때의 알림 동작(시간 계산은 시각 차이로 하므로 맞게 이어지도록 구현)