방 막대: ⋯ 메뉴를 없애고 규칙·설정·링크 복사·나가기를 모두 막대에 꺼내 둔다

- 휴대폰에서는 아이콘 아래 짧은 글자(홈/링크/채팅/규칙/설정/나가기)로 쌓아 320px 한 줄에 맞춘다.
- 방장 전용 '게임 끝내기(무효)'는 오류로 멈춘 게임일 때만 막대에 나온다.
- CopyButton의 done에 글자 대신 요소도 넣을 수 있게.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
EJClaw
2026-10-08 11:37:45 +09:00
parent 43156a9f63
commit 475648f9bc
4 changed files with 74 additions and 38 deletions

View File

@@ -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 onClick = async () => {
try {

View File

@@ -88,7 +88,6 @@ function Room({ code, me }: { code: string; me: PublicUser }) {
const nav = useNavigate();
const { room, game, result, status, joinError, pending, slow, needsRefresh } = useRoom();
const [ui, setUi] = useState<GameUI | null>(null);
const [menu, setMenu] = useState(false);
const [leaveAsk, setLeaveAsk] = useState(false);
const [chatOpen, setChatOpen] = useState(false);
const gameId = room?.gameId;
@@ -158,31 +157,59 @@ function Room({ code, me }: { code: string; me: PublicUser }) {
{g.nameKo} 방 {formatRoomCode(code)}
</h1>
<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 [나가기]. */}
<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>
<strong className="rb-game" style={{ fontSize: '1.1rem' }}>
{g.nameKo}
</strong>
{/* On phones the link sits in the menu so ← 홈 · 채팅 · 🚪 나가기 · 메뉴 fit on one line. */}
<span className="wide-only">
<CopyButton text={`${location.origin}/r/${code}`} label={<>방 <CodeLabel code={code} /> 🔗</>} done="링크 복사됨 ✓" className="btn btn-ghost btn-sm" />
</span>
<CopyButton
text={`${location.origin}/r/${code}`}
label={
<>
<span className="rb-ico">🔗</span>
<span className="rb-lbl">
<span className="wide-only">
방 <CodeLabel code={code} />
</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 }} />
{!roomChatClosed && (
<span className="phone-only">
<ChatButton onOpen={() => setChatOpen(true)} />
</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 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 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>
</div>
</nav>
@@ -209,25 +236,6 @@ function Room({ code, me }: { code: string; me: PublicUser }) {
</button>
</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 && (
<ConfirmSheet
title="방에서 나갈까요?"
@@ -731,8 +739,10 @@ function ChatButton({ onOpen }: { onOpen: () => void }) {
const seen = useChatSeen((s) => s.seen);
const unread = Math.max(0, total - seen);
return (
<button className="btn btn-ghost btn-sm" onClick={onOpen} aria-label={`채팅${unread ? `, 새 메시지 ${unread}개` : ''}`}>
💬 채팅{unread > 0 && <span className="badge warning">{unread}</span>}
<button className="btn btn-ghost btn-sm rb-btn" onClick={onOpen} aria-label={`채팅${unread ? `, 새 메시지 ${unread}개` : ''}`}>
<span className="rb-ico">💬</span>
<span className="rb-lbl">채팅</span>
{unread > 0 && <span className="badge warning rb-badge">{unread}</span>}
</button>
);
}

View File

@@ -976,9 +976,14 @@ button.header-user {
white-space: nowrap;
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) {
.room-bar-inner {
gap: 4px;
gap: 2px;
}
.room-bar .rb-game {
display: none;
@@ -986,6 +991,27 @@ button.header-user {
.room-bar .btn-sm {
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 {