시간 제한 없음은 정말 무제한, 대기실 설정은 바꾸면 바로 적용, 모르는 사람 기다리기(빠른 시작·공개 방) 제거

- 오목에서 [시간 제한 없음]을 골라도 30초가 흐르던 문제: 대기실 설정이 [설정 저장]을 눌러야
  서버에 반영되어, 누르지 않고 [게임 시작]하면 기본값(한 수 30초)으로 시작했다. 이제 설정은
  바꾸는 즉시 서버에 보낸다(저장 버튼 없음, 휴대폰 시트는 [닫기]만). 빠르게 여러 번 눌러도
  먼저 온 방 정보가 새 값을 되돌리지 않게 lib/option-sync.ts로 맞춘다.
- 시간 제한 없음(오목·체스·장기·오델로·바둑·쿼리도): 무응답 한도(idleLimitSec, 5~10분 뒤 시간패)와
  그 카운트다운을 없앴다. 체스는 첫 수 제한도 피셔일 때만. 연결이 끊긴 사람의 접속 유예는 그대로.
- 홈의 [지금 열린 방] 탭, 게임 소개의 [아무나와 빠른 시작]·기다리는 방 목록, 대기실의 방 공개
  설정을 뺐다(방은 모두 친구만). 서버 API는 남겨 두고 화면에서만 부르지 않는다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
EJClaw
2026-10-08 20:42:46 +09:00
parent 1e1408302a
commit 19c163f4d2
41 changed files with 215 additions and 264 deletions

View File

@@ -126,9 +126,11 @@ try {
// Trying to switch to 한국식 is overridden by the server.
await g.getByRole('button', { name: '설정 바꾸기' }).click();
if (await g.getByText(/위험한 자리 알려|위협 가이드/).count()) throw new Error('host sees a threat-guide switch');
// Changes apply at once; the server keeps the locked rule and the sheet follows it.
await g.getByRole('button', { name: '한국식(쌍삼 금지)' }).click();
await g.getByRole('button', { name: '저장' }).click();
await g.waitForTimeout(300);
if ((await g.getByRole('button', { name: '렌주룰' }).getAttribute('aria-pressed')) !== 'true') throw new Error('locked rule not restored in the sheet');
await g.getByRole('button', { name: '닫기' }).click();
await g.getByText('렌주룰 · 15줄 · 한 수 45초').waitFor();
await g.goto(ORIGIN + '/admin');
await g.getByText('권한이 없어요').waitFor();

View File

@@ -79,8 +79,7 @@ try {
await step('오목: 상세 → 컴퓨터와 연습하기 → 대기실', async () => {
await startPractice(a, 'omok', '호랑이');
await a.getByText('🤖 컴퓨터').first().waitFor();
// The host is seated first, the computer next; no public toggle in a practice room.
await a.getByText('컴퓨터가 있는 방은 친구만 들어와요.').waitFor();
await a.getByText('🤖 연습 게임이에요. 기록에는 남지 않아요.').waitFor();
await a.screenshot({ path: join(SHOTS, 'bots-1-lobby-pc.png') });
});

View File

@@ -839,7 +839,6 @@ const PLANS: Record<string, Plan> = {
// PC host + phone guest, first meld "없음" so hint plays happen early; 10 real turns through the UI.
lobby: async (host) => {
await host.getByRole('group', { name: '첫 등록 최소 점수' }).getByRole('button', { name: '없음' }).click();
await host.getByRole('button', { name: '설정 저장' }).click();
await host.getByText('첫 등록 점수 없음').first().waitFor();
},
drive: async (pages) => {
@@ -851,7 +850,6 @@ const PLANS: Record<string, Plan> = {
// 말 2개로 줄여서 PC 방장 + 휴대폰 친구가 실제 화면 조작으로 끝까지 둔다(중간 화면도 저장).
lobby: async (host) => {
await host.getByRole('button', { name: '2개', exact: true }).click();
await host.getByRole('button', { name: '설정 저장' }).click();
await host.getByText(/말 2개/).first().waitFor();
},
drive: async (pages) => {
@@ -1062,7 +1060,6 @@ const PLANS: Record<string, Plan> = {
lobby: async (host) => {
await host.getByRole('button', { name: '체스960 (첫 줄 무작위)' }).click();
await host.getByRole('button', { name: '방장이 백', exact: true }).click();
await host.getByRole('button', { name: '설정 저장' }).click();
await host.getByText(/체스960 · /).first().waitFor();
},
drive: async (pages) => {
@@ -1211,7 +1208,6 @@ const PLANS: Record<string, Plan> = {
// 흔들기/폭탄 sheets, one 고 and then 스톱 — until the hand ends (스톱 or 나가리) and the game result shows.
lobby: async (host) => {
await host.getByRole('group', { name: '판 수' }).getByRole('button', { name: '1판', exact: true }).click();
await host.getByRole('button', { name: '설정 저장' }).click();
await host.getByText(/^1판 · /).first().waitFor();
},
drive: async (pages) => {
@@ -1263,7 +1259,6 @@ const PLANS: Record<string, Plan> = {
players: 4,
lobby: async (host) => {
await host.getByRole('group', { name: '반응 제한 시간' }).getByRole('button', { name: '25초' }).click();
await host.getByRole('button', { name: '설정 저장' }).click();
await host.getByText(/반응 25초/).first().waitFor();
},
drive: async (pages) => {
@@ -1370,7 +1365,6 @@ const PLANS: Record<string, Plan> = {
players: 3,
lobby: async (host) => {
await host.getByRole('group', { name: '뒤집기 제한 시간' }).getByRole('button', { name: '5초', exact: true }).click();
await host.getByRole('button', { name: '설정 저장' }).click();
await host.getByText(/뒤집기 5초/).first().waitFor();
},
drive: async (pages) => {
@@ -1455,7 +1449,6 @@ const PLANS: Record<string, Plan> = {
lobby: async (host) => {
await host.getByRole('group', { name: '라운드 제한' }).getByRole('button', { name: '15라운드' }).click();
await host.getByRole('group', { name: '시작 포인트' }).getByRole('button', { name: '200P' }).click();
await host.getByRole('button', { name: '설정 저장' }).click();
await host.getByText(/^15라운드 · 시작 200P/).first().waitFor();
},
drive: async (pages) => {
@@ -1510,7 +1503,6 @@ const PLANS: Record<string, Plan> = {
await host.getByRole('button', { name: '▼ 자세한 설정' }).click();
await host.getByRole('group', { name: '9~12 카드 빼기' }).getByRole('button', { name: '켜기' }).click();
await host.getByRole('group', { name: '한 턴 제한 시간' }).getByRole('button', { name: '45초' }).click();
await host.getByRole('button', { name: '설정 저장' }).click();
await host.getByText(/9~12 빼기/).first().waitFor();
},
drive: async (pages) => {
@@ -1572,7 +1564,6 @@ const PLANS: Record<string, Plan> = {
// return tokens over 10 and choose nobles when it happens. Mid-game shots in light and dark.
lobby: async (host) => {
await host.getByRole('group', { name: '끝나는 점수' }).getByRole('button', { name: '10점(짧은 판)' }).click();
await host.getByRole('button', { name: '설정 저장' }).click();
await host.getByText(/^10점 · 한 차례 60초/).first().waitFor();
},
drive: async (pages) => {
@@ -1691,7 +1682,6 @@ const PLANS: Record<string, Plan> = {
// through the UI until round settlements (with a duplicate-tie cancellation) and the final result appear.
lobby: async (host) => {
await host.getByRole('group', { name: '라운드 수' }).getByRole('button', { name: '3라운드' }).click();
await host.getByRole('button', { name: '설정 저장' }).click();
await host.getByText(/^3라운드 · 흰 주사위 자동/).first().waitFor();
},
drive: async (pages) => {
@@ -1744,7 +1734,6 @@ const PLANS: Record<string, Plan> = {
players: 3,
lobby: async (host) => {
await host.getByRole('group', { name: '같은 벌레 몇 장이면 패배' }).getByRole('button', { name: '3장(짧게)' }).click();
await host.getByRole('button', { name: '설정 저장' }).click();
await host.getByText(/같은 벌레 3장이면 패배/).first().waitFor();
},
drive: async (pages) => {
@@ -1803,7 +1792,6 @@ const PLANS: Record<string, Plan> = {
await host.getByRole('group', { name: '한 턴 제한 시간' }).getByRole('button', { name: '30초' }).click();
await host.getByRole('button', { name: '▼ 자세한 설정' }).click();
await host.getByRole('group', { name: '특수 카드 장수' }).getByRole('button', { name: '6장' }).click();
await host.getByRole('button', { name: '설정 저장' }).click();
await host.getByText(/^전용 카드 · 손패 5장 · 목숨 없음 · 30초/).first().waitFor();
},
drive: async (pages) => {
@@ -1855,7 +1843,6 @@ const PLANS: Record<string, Plan> = {
lobby: async (host) => {
await host.getByRole('button', { name: '▼ 자세한 설정' }).click();
await host.getByRole('group', { name: '카드 장수' }).getByRole('button', { name: '55장' }).click();
await host.getByRole('button', { name: '설정 저장' }).click();
await host.getByText(/^탑 쌓기 · 55장 · 틀리면 1초 잠금/).first().waitFor();
},
drive: async (pages) => {
@@ -1916,7 +1903,6 @@ const PLANS: Record<string, Plan> = {
lobby: async (host) => {
await host.getByRole('group', { name: '라운드 수' }).getByRole('button', { name: '1라운드' }).click();
await host.getByRole('group', { name: '그리기 시간' }).getByRole('button', { name: '60초' }).click();
await host.getByRole('button', { name: '설정 저장' }).click();
await host.getByText(/^여러 명 맞히기 · 1라운드 · 60초/).first().waitFor();
},
drive: async (pages) => {
@@ -1951,7 +1937,6 @@ const PLANS: Record<string, Plan> = {
await host.getByRole('group', { name: '지목 투표 시간' }).getByRole('button', { name: '10초', exact: true }).click();
await host.getByRole('group', { name: '최후의 반론 시간' }).getByRole('button', { name: '10초', exact: true }).click();
await host.getByRole('group', { name: '찬반 투표 시간' }).getByRole('button', { name: '5초', exact: true }).click();
await host.getByRole('button', { name: '설정 저장' }).click();
await host.getByText(/밤 45초 · 투표 10초/).first().waitFor();
},
drive: async (pages) => {

View File

@@ -88,17 +88,20 @@ try {
await b.screenshot({ path: join(SHOTS, '5-lobby-phone.png') });
});
await step('A: 설정(방장 흑, 자유룰) → 시작', async () => {
// PC: the host edits settings inline in the lobby.
await step('A: 설정(방장 흑, 자유룰, 시간 제한 없음) → 바로 시작', async () => {
// PC: the host edits settings inline in the lobby. Every change applies at once (no save button).
await a.getByRole('button', { name: '자유룰' }).click();
await a.getByRole('button', { name: '방장이 흑' }).click();
await a.getByRole('button', { name: '설정 저장' }).click();
await a.getByText('자유룰 · 15줄').waitFor();
await a.getByRole('button', { name: '시간 제한 없음' }).click();
await a.getByText('자유룰 · 15줄 · 시간 제한 없음').waitFor();
await b.getByText('자유룰 · 15줄 · 시간 제한 없음').waitFor();
await a.screenshot({ path: join(SHOTS, '1-lobby-pc.png') });
await a.getByRole('button', { name: '▶ 게임 시작' }).click();
await a.getByRole('button', { name: '시작하기' }).click();
await a.getByText('내 차례예요').waitFor();
await b.getByText('호랑이님이 생각 중이에요').waitFor();
// 시간 제한 없음: no countdown anywhere.
for (const p of [a, b]) if ((await p.getByRole('timer').count()) !== 0) throw new Error('시간 제한 없음인데 타이머가 보여요');
});
await step('대국: 흑 가로 5목', async () => {

View File

@@ -96,7 +96,6 @@ try {
await a.getByLabel('자리').getByText('고양이').waitFor();
await a.getByRole('button', { name: '자유룰' }).click();
await a.getByRole('button', { name: '방장이 흑' }).click();
await a.getByRole('button', { name: '설정 저장' }).click();
await a.getByText('자유룰 · 15줄').waitFor();
await a.getByRole('button', { name: '▶ 게임 시작' }).click();
await a.getByRole('button', { name: '시작하기' }).click();