치지직 앱으로 연동하고, 연동 확인된 ID만 접속할 수 있도록 수정, 개발용으로는 채널ID를 수동으로 접속할 수 있도록 변경.

최초 추천 고정 배치 추가.
This commit is contained in:
kimyu
2026-09-18 04:09:31 +09:00
parent 6f72824598
commit 73520f9f06
26 changed files with 1272 additions and 162 deletions

View File

@@ -0,0 +1,31 @@
import { NextResponse } from 'next/server';
import { assertChannelRoomAccess } from '@/lib/auth/room-access';
import { addDice, getRoom, notifyRoom, toPublicRoom } from '@/lib/rooms/store';
export const runtime = 'nodejs';
type Params = { params: Promise<{ roomId: string }> };
export async function POST(request: Request, { params }: Params) {
const { roomId } = await params;
const room = getRoom(roomId);
if (!room) {
return NextResponse.json({ error: '방을 찾을 수 없습니다.' }, { status: 404 });
}
const access = await assertChannelRoomAccess(room, request);
if (!access.ok) return access.response;
const body = (await request.json().catch(() => ({}))) as { count?: number };
const count = Number(body.count ?? 1);
if (!Number.isFinite(count) || count < 1) {
return NextResponse.json({ error: 'count는 1 이상이어야 합니다.' }, { status: 400 });
}
const updated = addDice(roomId, count);
if (!updated) {
return NextResponse.json({ error: '처리 실패' }, { status: 500 });
}
notifyRoom(roomId);
return NextResponse.json({ room: toPublicRoom(updated) });
}

View File

@@ -1,4 +1,5 @@
import { NextResponse } from 'next/server';
import { assertChannelRoomAccess } from '@/lib/auth/room-access';
import {
consumeDice,
getRoom,
@@ -17,6 +18,9 @@ export async function POST(request: Request, { params }: Params) {
return NextResponse.json({ error: '방을 찾을 수 없습니다.' }, { status: 404 });
}
const access = await assertChannelRoomAccess(room, request);
if (!access.ok) return access.response;
const body = (await request.json().catch(() => ({}))) as { count?: number };
const count = Math.max(1, Math.trunc(body.count ?? 1));

View File

@@ -2,6 +2,7 @@ import {
cancelScheduledDisconnectRoomChat,
scheduleDisconnectRoomChat,
} from '@/lib/chzzk/chat-session';
import { assertChannelRoomAccess } from '@/lib/auth/room-access';
import {
addRoomViewer,
getRoom,
@@ -26,6 +27,9 @@ export async function GET(request: Request, { params }: Params) {
});
}
const access = await assertChannelRoomAccess(room, request);
if (!access.ok) return access.response;
const clientId =
new URL(request.url).searchParams.get('clientId')?.trim() ||
crypto.randomUUID();
@@ -40,14 +44,12 @@ export async function GET(request: Request, { params }: Params) {
presenceHeld = false;
const { emptied } = removeRoomViewer(roomId, clientId);
if (emptied) {
// 즉시 끊지 않음 — 빠른 재입장과 경쟁하지 않도록 유예
scheduleDisconnectRoomChat(roomId);
}
};
const stream = new ReadableStream({
start(controller) {
// 재입장 시 예약된 disconnect 취소
cancelScheduledDisconnectRoomChat(roomId);
addRoomViewer(roomId, clientId);
presenceHeld = true;

View File

@@ -1,15 +1,18 @@
import { NextResponse } from 'next/server';
import { assertChannelRoomAccess } from '@/lib/auth/room-access';
import { getRoom, toPublicRoom } from '@/lib/rooms/store';
export const runtime = 'nodejs';
type Params = { params: Promise<{ roomId: string }> };
export async function GET(_request: Request, { params }: Params) {
export async function GET(request: Request, { params }: Params) {
const { roomId } = await params;
const room = getRoom(roomId);
if (!room) {
return NextResponse.json({ error: '방을 찾을 수 없습니다.' }, { status: 404 });
}
const access = await assertChannelRoomAccess(room, request);
if (!access.ok) return access.response;
return NextResponse.json({ room: toPublicRoom(room) });
}

View File

@@ -1,4 +1,5 @@
import { NextResponse } from 'next/server';
import { assertChannelRoomAccess } from '@/lib/auth/room-access';
import {
addDiceFromDonation,
getRoom,
@@ -17,6 +18,9 @@ export async function POST(request: Request, { params }: Params) {
return NextResponse.json({ error: '방을 찾을 수 없습니다.' }, { status: 404 });
}
const access = await assertChannelRoomAccess(room, request);
if (!access.ok) return access.response;
const body = (await request.json().catch(() => ({}))) as {
payAmount?: number;
donatorNickname?: string;

View File

@@ -1,5 +1,11 @@
import { NextResponse } from 'next/server';
import { parseChzzkChannelId } from '@/lib/chzzk/channel';
import {
allowChannelIdJoin,
isChzzkOAuthConfigured,
requireChzzkLoginForChannelRooms,
} from '@/lib/auth/mode';
import { getRequestAuthSession } from '@/lib/auth/room-access';
import {
cancelScheduledDisconnectRoomChat,
ensureRoomChat,
@@ -15,7 +21,8 @@ export const runtime = 'nodejs';
/**
* 치지직 채널 ID(또는 라이브 URL)로 룸 입장.
* 없으면 전달된 치즈 설정으로 새 룸을 만들고 채팅 연결을 시도한다.
* - 개발: 채널 ID 직접 입장 가능 (+ 로그인 세션 있으면 본인 채널도 가능)
* - 릴리즈: 로그인 세션의 채널 ID와 일치할 때만 입장
*/
export async function POST(request: Request) {
const body = (await request.json().catch(() => ({}))) as {
@@ -25,8 +32,39 @@ export async function POST(request: Request) {
maxMultiplier?: number;
};
const session = await getRequestAuthSession(request);
const raw = body.channelIdOrUrl || '';
const channelId = parseChzzkChannelId(raw);
let channelId = parseChzzkChannelId(raw);
if (requireChzzkLoginForChannelRooms()) {
if (!session) {
return NextResponse.json(
{
error:
'릴리즈 환경에서는 치지직 로그인 후 본인 채널로만 입장할 수 있습니다.',
code: 'CHZZK_AUTH_REQUIRED',
allowChannelIdJoin: allowChannelIdJoin(),
oauthConfigured: isChzzkOAuthConfigured(),
},
{ status: 401 },
);
}
// 입력된 채널이 있으면 세션과 일치해야 함. 없으면 세션 채널 사용.
if (channelId && channelId !== session.channelId) {
return NextResponse.json(
{
error: '로그인한 채널과 다른 채널 ID로는 입장할 수 없습니다.',
code: 'CHANNEL_MISMATCH',
},
{ status: 403 },
);
}
channelId = session.channelId;
} else if (!channelId) {
// 개발: 세션만 있으면 세션 채널로 입장 가능
if (session?.channelId) channelId = session.channelId;
}
if (!channelId) {
return NextResponse.json(
{
@@ -44,7 +82,7 @@ export async function POST(request: Request) {
created = true;
room = createRoom({
chzzkEnabled: true,
liveUrl: raw.trim(),
liveUrl: raw.trim() || `https://chzzk.naver.com/live/${channelId}`,
channelId,
cheesePerDice: body.cheesePerDice,
multiplyByCheese: body.multiplyByCheese,
@@ -52,7 +90,6 @@ export async function POST(request: Request) {
});
}
// 이전 세션 leave가 늦게 와도 끊기지 않게 예약 해제 + 실제 WS 생존 여부 확인
cancelScheduledDisconnectRoomChat(room.id);
if (room.settings.chzzkEnabled) {