방 막대: ⋯ 메뉴를 없애고 규칙·설정·링크 복사·나가기를 모두 막대에 꺼내 둔다
- 휴대폰에서는 아이콘 아래 짧은 글자(홈/링크/채팅/규칙/설정/나가기)로 쌓아 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 onClick = async () => {
|
||||
try {
|
||||
|
||||
@@ -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. */}
|
||||
<CopyButton
|
||||
text={`${location.origin}/r/${code}`}
|
||||
label={
|
||||
<>
|
||||
<span className="rb-ico">🔗</span>
|
||||
<span className="rb-lbl">
|
||||
<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 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -57,9 +57,9 @@
|
||||
|
||||
## 8. 게임 중
|
||||
- 위쪽 상태 줄: 지금 할 일 문장("내 차례예요" / "OO님 차례예요"), 남은 시간 링.
|
||||
- 방 막대: [← 홈] 게임 이름 · 방 코드 링크(휴대폰은 메뉴 안) · [💬 채팅](휴대폰) · [📖 규칙](PC) · **[🚪 나가기]** · [⋯ 메뉴].
|
||||
- 방 막대(메뉴 버튼 없이 모두 밖에): [← 홈] 게임 이름(PC) · [🔗 방 코드 링크 복사] · [💬 채팅](휴대폰) · [📖 규칙] · [⚙️ 설정](글자 크기·소리, `/me`) · [⏹ 게임 끝내기](방장, 오류로 멈춘 게임만) · **[🚪 나가기]**. 휴대폰에서는 아이콘 아래 짧은 글자로 쌓아 한 줄(320px)에 들어가게 한다.
|
||||
- [🚪 나가기]와 [← 홈]은 같은 동작이다: 방에서 나가고 홈으로. 한 번 누르면 바로 나가고, 게임 중에 자리에 앉아 있을 때만 "게임 중에 나가면 기권으로 처리돼요" 확인을 한 번 더 묻는다.
|
||||
- 메뉴(⋯): 규칙 보기, 소리, 글자 크기, 무르기 요청(지원 게임), 무승부 제안(지원 게임), 기권. 나가기는 메뉴에 넣지 않는다.
|
||||
- 무르기 요청·무승부 제안·기권은 게임 화면 안의 버튼으로 한다(지원 게임).
|
||||
- 관전자는 [자리 나면 참가] 대기 가능(다음 판부터).
|
||||
|
||||
## 9. 게임 끝
|
||||
|
||||
Reference in New Issue
Block a user