방 막대: ⋯ 메뉴를 없애고 규칙·설정·링크 복사·나가기를 모두 막대에 꺼내 둔다
- 휴대폰에서는 아이콘 아래 짧은 글자(홈/링크/채팅/규칙/설정/나가기)로 쌓아 320px 한 줄에 맞춘다. - 방장 전용 '게임 끝내기(무효)'는 오류로 멈춘 게임일 때만 막대에 나온다. - CopyButton의 done에 글자 대신 요소도 넣을 수 있게. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,7 @@ export function Avatar({ user, size = 40 }: { user: Pick<PublicUser, 'id' | 'nic
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CopyButton({ text, label, done = '복사했어요 ✓', className = 'btn btn-secondary' }: { text: string; label: ReactNode; done?: string; className?: string }) {
|
export function CopyButton({ text, label, done = '복사했어요 ✓', className = 'btn btn-secondary' }: { text: string; label: ReactNode; done?: ReactNode; className?: string }) {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const onClick = async () => {
|
const onClick = async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -88,7 +88,6 @@ function Room({ code, me }: { code: string; me: PublicUser }) {
|
|||||||
const nav = useNavigate();
|
const nav = useNavigate();
|
||||||
const { room, game, result, status, joinError, pending, slow, needsRefresh } = useRoom();
|
const { room, game, result, status, joinError, pending, slow, needsRefresh } = useRoom();
|
||||||
const [ui, setUi] = useState<GameUI | null>(null);
|
const [ui, setUi] = useState<GameUI | null>(null);
|
||||||
const [menu, setMenu] = useState(false);
|
|
||||||
const [leaveAsk, setLeaveAsk] = useState(false);
|
const [leaveAsk, setLeaveAsk] = useState(false);
|
||||||
const [chatOpen, setChatOpen] = useState(false);
|
const [chatOpen, setChatOpen] = useState(false);
|
||||||
const gameId = room?.gameId;
|
const gameId = room?.gameId;
|
||||||
@@ -158,31 +157,59 @@ function Room({ code, me }: { code: string; me: PublicUser }) {
|
|||||||
{g.nameKo} 방 {formatRoomCode(code)}
|
{g.nameKo} 방 {formatRoomCode(code)}
|
||||||
</h1>
|
</h1>
|
||||||
<div className="room-bar-inner">
|
<div className="room-bar-inner">
|
||||||
|
{/* No ⋯ menu: every room action sits on this bar. Phones stack icon over a short label (.rb-btn). */}
|
||||||
{/* ← 홈 leaves the room, same as [나가기]. */}
|
{/* ← 홈 leaves the room, same as [나가기]. */}
|
||||||
<button className="btn btn-ghost btn-sm" onClick={askLeave} aria-label="방 나가고 홈으로">
|
<button className="btn btn-ghost btn-sm rb-btn" onClick={askLeave} aria-label="방 나가고 홈으로">
|
||||||
← 홈
|
<span className="rb-ico">←</span>
|
||||||
|
<span className="rb-lbl">홈</span>
|
||||||
</button>
|
</button>
|
||||||
<strong className="rb-game" style={{ fontSize: '1.1rem' }}>
|
<strong className="rb-game" style={{ fontSize: '1.1rem' }}>
|
||||||
{g.nameKo}
|
{g.nameKo}
|
||||||
</strong>
|
</strong>
|
||||||
{/* On phones the link sits in the menu so ← 홈 · 채팅 · 🚪 나가기 · 메뉴 fit on one line. */}
|
<CopyButton
|
||||||
|
text={`${location.origin}/r/${code}`}
|
||||||
|
label={
|
||||||
|
<>
|
||||||
|
<span className="rb-ico">🔗</span>
|
||||||
|
<span className="rb-lbl">
|
||||||
<span className="wide-only">
|
<span className="wide-only">
|
||||||
<CopyButton text={`${location.origin}/r/${code}`} label={<>방 <CodeLabel code={code} /> 🔗</>} done="링크 복사됨 ✓" className="btn btn-ghost btn-sm" />
|
방 <CodeLabel code={code} />
|
||||||
</span>
|
</span>
|
||||||
|
<span className="phone-only">링크</span>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
done={
|
||||||
|
<>
|
||||||
|
<span className="rb-ico">✓</span>
|
||||||
|
<span className="rb-lbl">복사됨</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
className="btn btn-ghost btn-sm rb-btn"
|
||||||
|
/>
|
||||||
<span style={{ flex: 1 }} />
|
<span style={{ flex: 1 }} />
|
||||||
{!roomChatClosed && (
|
{!roomChatClosed && (
|
||||||
<span className="phone-only">
|
<span className="phone-only">
|
||||||
<ChatButton onOpen={() => setChatOpen(true)} />
|
<ChatButton onOpen={() => setChatOpen(true)} />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<button className="btn btn-ghost btn-sm wide-only" onClick={() => nav(`/rules/${room.gameId}`)}>
|
<button className="btn btn-ghost btn-sm rb-btn" onClick={() => nav(`/rules/${room.gameId}`)} aria-label="규칙 보기">
|
||||||
📖 규칙
|
<span className="rb-ico">📖</span>
|
||||||
|
<span className="rb-lbl">규칙</span>
|
||||||
</button>
|
</button>
|
||||||
<button className="btn btn-danger btn-sm" onClick={askLeave}>
|
<button className="btn btn-ghost btn-sm rb-btn" onClick={() => nav('/me')} aria-label="글자 크기 · 소리 설정">
|
||||||
🚪 나가기
|
<span className="rb-ico">⚙️</span>
|
||||||
|
<span className="rb-lbl">설정</span>
|
||||||
</button>
|
</button>
|
||||||
<button className="btn btn-secondary btn-sm" onClick={() => setMenu(true)} aria-label="메뉴">
|
{isHost && room.status === 'paused' && (
|
||||||
⋯ 메뉴
|
<button className="btn btn-danger btn-sm rb-btn" onClick={() => socket.send({ t: 'void' })} aria-label="게임 끝내기(무효)">
|
||||||
|
<span className="rb-ico">⏹</span>
|
||||||
|
<span className="rb-lbl">게임 끝내기</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button className="btn btn-danger btn-sm rb-btn" onClick={askLeave} aria-label="🚪 나가기">
|
||||||
|
<span className="rb-ico">🚪</span>
|
||||||
|
<span className="rb-lbl">나가기</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -209,25 +236,6 @@ function Room({ code, me }: { code: string; me: PublicUser }) {
|
|||||||
</button>
|
</button>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
)}
|
)}
|
||||||
{menu && (
|
|
||||||
<Sheet onClose={() => setMenu(false)} label="메뉴">
|
|
||||||
<h2>메뉴</h2>
|
|
||||||
<span className="phone-only">
|
|
||||||
<CopyButton text={`${location.origin}/r/${code}`} label={<>🔗 방 <CodeLabel code={code} /> 링크 복사</>} done="링크 복사됨 ✓" className="btn btn-secondary btn-block" />
|
|
||||||
</span>
|
|
||||||
<button className="btn btn-secondary btn-block" onClick={() => nav(`/rules/${room.gameId}`)}>
|
|
||||||
📖 규칙 보기
|
|
||||||
</button>
|
|
||||||
<button className="btn btn-secondary btn-block" onClick={() => nav('/me')}>
|
|
||||||
⚙️ 글자 크기 · 소리 설정
|
|
||||||
</button>
|
|
||||||
{isHost && room.status === 'paused' && (
|
|
||||||
<button className="btn btn-danger btn-block" onClick={() => (socket.send({ t: 'void' }), setMenu(false))}>
|
|
||||||
게임 끝내기(무효)
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Sheet>
|
|
||||||
)}
|
|
||||||
{leaveAsk && (
|
{leaveAsk && (
|
||||||
<ConfirmSheet
|
<ConfirmSheet
|
||||||
title="방에서 나갈까요?"
|
title="방에서 나갈까요?"
|
||||||
@@ -731,8 +739,10 @@ function ChatButton({ onOpen }: { onOpen: () => void }) {
|
|||||||
const seen = useChatSeen((s) => s.seen);
|
const seen = useChatSeen((s) => s.seen);
|
||||||
const unread = Math.max(0, total - seen);
|
const unread = Math.max(0, total - seen);
|
||||||
return (
|
return (
|
||||||
<button className="btn btn-ghost btn-sm" onClick={onOpen} aria-label={`채팅${unread ? `, 새 메시지 ${unread}개` : ''}`}>
|
<button className="btn btn-ghost btn-sm rb-btn" onClick={onOpen} aria-label={`채팅${unread ? `, 새 메시지 ${unread}개` : ''}`}>
|
||||||
💬 채팅{unread > 0 && <span className="badge warning">{unread}</span>}
|
<span className="rb-ico">💬</span>
|
||||||
|
<span className="rb-lbl">채팅</span>
|
||||||
|
{unread > 0 && <span className="badge warning rb-badge">{unread}</span>}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -976,9 +976,14 @@ button.header-user {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
/* Room bar buttons: icon + label inline on PC, icon over a small label on phones (no ⋯ menu). */
|
||||||
|
.rb-btn {
|
||||||
|
gap: 6px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
@media (max-width: 719px) {
|
@media (max-width: 719px) {
|
||||||
.room-bar-inner {
|
.room-bar-inner {
|
||||||
gap: 4px;
|
gap: 2px;
|
||||||
}
|
}
|
||||||
.room-bar .rb-game {
|
.room-bar .rb-game {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -986,6 +991,27 @@ button.header-user {
|
|||||||
.room-bar .btn-sm {
|
.room-bar .btn-sm {
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
}
|
}
|
||||||
|
.room-bar .rb-btn {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
|
padding: 2px 6px;
|
||||||
|
min-width: 46px;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
.room-bar .rb-ico {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
.room-bar .rb-lbl {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.room-bar .rb-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 0 5px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.schema-grid {
|
.schema-grid {
|
||||||
|
|||||||
@@ -57,9 +57,9 @@
|
|||||||
|
|
||||||
## 8. 게임 중
|
## 8. 게임 중
|
||||||
- 위쪽 상태 줄: 지금 할 일 문장("내 차례예요" / "OO님 차례예요"), 남은 시간 링.
|
- 위쪽 상태 줄: 지금 할 일 문장("내 차례예요" / "OO님 차례예요"), 남은 시간 링.
|
||||||
- 방 막대: [← 홈] 게임 이름 · 방 코드 링크(휴대폰은 메뉴 안) · [💬 채팅](휴대폰) · [📖 규칙](PC) · **[🚪 나가기]** · [⋯ 메뉴].
|
- 방 막대(메뉴 버튼 없이 모두 밖에): [← 홈] 게임 이름(PC) · [🔗 방 코드 링크 복사] · [💬 채팅](휴대폰) · [📖 규칙] · [⚙️ 설정](글자 크기·소리, `/me`) · [⏹ 게임 끝내기](방장, 오류로 멈춘 게임만) · **[🚪 나가기]**. 휴대폰에서는 아이콘 아래 짧은 글자로 쌓아 한 줄(320px)에 들어가게 한다.
|
||||||
- [🚪 나가기]와 [← 홈]은 같은 동작이다: 방에서 나가고 홈으로. 한 번 누르면 바로 나가고, 게임 중에 자리에 앉아 있을 때만 "게임 중에 나가면 기권으로 처리돼요" 확인을 한 번 더 묻는다.
|
- [🚪 나가기]와 [← 홈]은 같은 동작이다: 방에서 나가고 홈으로. 한 번 누르면 바로 나가고, 게임 중에 자리에 앉아 있을 때만 "게임 중에 나가면 기권으로 처리돼요" 확인을 한 번 더 묻는다.
|
||||||
- 메뉴(⋯): 규칙 보기, 소리, 글자 크기, 무르기 요청(지원 게임), 무승부 제안(지원 게임), 기권. 나가기는 메뉴에 넣지 않는다.
|
- 무르기 요청·무승부 제안·기권은 게임 화면 안의 버튼으로 한다(지원 게임).
|
||||||
- 관전자는 [자리 나면 참가] 대기 가능(다음 판부터).
|
- 관전자는 [자리 나면 참가] 대기 가능(다음 판부터).
|
||||||
|
|
||||||
## 9. 게임 끝
|
## 9. 게임 끝
|
||||||
|
|||||||
Reference in New Issue
Block a user