Merge branch 'm8/mafia' into codex/owner/grp_discord_joke-app

# Conflicts:
#	apps/server/src/rooms/room.ts
#	apps/web/src/games/registry.ts
#	apps/web/src/lib/catalog-ui.ts
#	e2e/games.e2e.ts
#	packages/engine/src/types.ts
#	packages/games/src/index.ts
#	packages/shared/src/catalog.ts
This commit is contained in:
EJClaw
2026-10-06 00:39:35 +09:00
16 changed files with 3238 additions and 14 deletions

View File

@@ -9,10 +9,12 @@ export default function GamesPage() {
<main className="page">
<h1>게임 목록</h1>
<GameGrid games={catalog.filter((g) => g.available)} />
<section className="stack">
<h2>곧 나와요</h2>
<p className="muted">{UPCOMING.join(' · ')}</p>
</section>
{UPCOMING.length > 0 && (
<section className="stack">
<h2>곧 나와요</h2>
<p className="muted">{UPCOMING.join(' · ')}</p>
</section>
)}
</main>
);
}

View File

@@ -86,7 +86,7 @@ export function Home() {
<section className="stack" style={{ gap: 16 }}>
<h2>게임 고르기</h2>
<GameGrid games={available} />
<p className="muted small">곧 나와요: {UPCOMING.join(' · ')}</p>
{UPCOMING.length > 0 && <p className="muted small">곧 나와요: {UPCOMING.join(' · ')}</p>}
</section>
<aside className="stack" style={{ gap: 12 }}>
<h2>지금 열린 방</h2>

View File

@@ -136,6 +136,8 @@ function Room({ code, me }: { code: string; me: PublicUser }) {
const g = { nameKo: gameName(room.gameId) };
const inGame = room.status === 'playing' || room.status === 'paused' || (room.status === 'finished' && !!game);
// Games with their own chat channels (마피아) close the room chat while playing (the server refuses it too).
const roomChatClosed = !!ui.ownChat && (room.status === 'playing' || room.status === 'paused');
const isHost = room.hostId === me.id;
const doLeave = () => {
socket.leave();
@@ -157,9 +159,11 @@ function Room({ code, me }: { code: string; me: PublicUser }) {
</strong>
<CopyButton text={`${location.origin}/r/${code}`} label={<>방 <CodeLabel code={code} /> 🔗</>} done="링크 복사됨 ✓" className="btn btn-ghost btn-sm" />
<span style={{ flex: 1 }} />
<span className="phone-only">
<ChatButton onOpen={() => setChatOpen(true)} />
</span>
{!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>
@@ -183,7 +187,7 @@ function Room({ code, me }: { code: string; me: PublicUser }) {
)}
{room.status === 'finished' && result && game && <ResultPanel room={room} me={me} players={players} onLeave={doLeave} />}
<EmoteBubbles room={room} />
{chatOpen && (
{chatOpen && !roomChatClosed && (
<Sheet onClose={() => setChatOpen(false)} label="채팅">
<ChatPanel room={room} me={me} />
<button className="btn btn-secondary btn-block" onClick={() => setChatOpen(false)}>
@@ -549,7 +553,7 @@ function GameView({ room, ui, me, players, pending, slow }: { room: RoomView; ui
<aside className="game-side" aria-label="게임 정보">
{wide && status}
<ui.Side {...props} />
{wide && <ChatPanel room={room} me={me} title={false} />}
{wide && !(ui.ownChat && (room.status === 'playing' || room.status === 'paused')) && <ChatPanel room={room} me={me} title={false} />}
</aside>
</div>
);