치지직 채팅창 인식 추가 및 주사위 횟수 증가 추가.

This commit is contained in:
kimyu
2026-09-16 02:21:39 +09:00
parent 171a4e19f6
commit 7c402dfd8f
22 changed files with 2133 additions and 90 deletions

View File

@@ -1,6 +1,6 @@
'use client';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useEffect, useRef, useState } from 'react';
import {
ASSET_ASPECT,
@@ -316,6 +316,7 @@ function BoardEditor({
}
export default function Home() {
const router = useRouter();
const [assets, setAssets] = useState<DrinkmarbleAssets>(DEFAULT_ASSETS);
const [board, setBoard] = useState<BoardConfig | null>(null);
const [ready, setReady] = useState(false);
@@ -323,6 +324,13 @@ export default function Home() {
const [busy, setBusy] = useState(false);
const packInputRef = useRef<HTMLInputElement>(null);
const [roomCheesePerDice, setRoomCheesePerDice] = useState(1000);
const [roomMultiplyByCheese, setRoomMultiplyByCheese] = useState(true);
const [roomMaxMultiplier, setRoomMaxMultiplier] = useState(10);
const [joinChannelInput, setJoinChannelInput] = useState('');
const [roomBusy, setRoomBusy] = useState(false);
const [roomError, setRoomError] = useState<string | null>(null);
const reload = () => {
setAssets(getAssets());
setBoard(getOrCreateBoardConfig());
@@ -333,6 +341,67 @@ export default function Home() {
setReady(true);
}, []);
const roomCheeseOptions = () => ({
cheesePerDice: roomCheesePerDice,
multiplyByCheese: roomMultiplyByCheese,
maxMultiplier: roomMultiplyByCheese ? roomMaxMultiplier : 0,
});
const joinByChzzkId = async () => {
const value = joinChannelInput.trim();
if (!value) {
setRoomError('치지직 채널 ID 또는 라이브 URL을 입력하세요.');
return;
}
try {
setRoomBusy(true);
setRoomError(null);
const res = await fetch('/api/rooms/join', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
channelIdOrUrl: value,
...roomCheeseOptions(),
}),
});
const json = await res.json();
if (!res.ok) {
setRoomError(json.error || '룸을 찾을 수 없습니다.');
if (json.room?.id) {
router.push(`/game?roomId=${json.room.id}`);
}
return;
}
router.push(`/game?roomId=${json.room.id}`);
} catch (e) {
setRoomError(e instanceof Error ? e.message : '룸 입장에 실패했습니다.');
} finally {
setRoomBusy(false);
}
};
const enterWithoutIntegration = async () => {
try {
setRoomBusy(true);
setRoomError(null);
const res = await fetch('/api/rooms', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
});
const json = await res.json();
if (!res.ok) {
setRoomError(json.error || '룸 생성에 실패했습니다.');
return;
}
router.push(`/game?roomId=${json.room.id}`);
} catch (e) {
setRoomError(e instanceof Error ? e.message : '입장에 실패했습니다.');
} finally {
setRoomBusy(false);
}
};
const save = async (
kind: 'background' | 'chessman' | 'diceFloor' | { slot: DiceSlot; face: DiceFace },
file: File,
@@ -390,23 +459,110 @@ export default function Home() {
return (
<div className="min-h-screen bg-[#1c1224] text-white">
<div className="mx-auto flex min-h-screen max-w-3xl flex-col gap-8 px-6 py-10">
<header className="flex flex-col items-start gap-4 sm:flex-row sm:items-end sm:justify-between">
<div>
<p className="text-sm text-white/60">온라인 주루마블</p>
<h1 className="mt-1 text-4xl font-bold tracking-tight">Drinkmarble</h1>
<p className="mt-2 max-w-md text-sm text-white/65">
이미지와 판 칸 설정은 서버가 아니라 이 브라우저 localStorage에만 저장됩니다.
<header className="flex w-full flex-col items-stretch gap-5">
<div className="w-full">
{/*<p className="text-sm text-white/60">온라인 주루마블</p>*/}
<h1 className="relative mt-1 inline-block w-full text-center text-4xl font-bold tracking-tight">
<span className="relative inline-block pr-12">
온라인 주루마블!
<span className="pointer-events-none absolute bottom-1 left-full -ml-0.5 whitespace-nowrap text-[11px] font-normal tracking-normal text-white/35">
by kimyu.
</span>
</span>
</h1>
<br></br>
<p className="border-b border-white/10 w-full"></p>
<p className="mt-2 w-full text-sm text-white/65">
이미지와 판 칸 설정은 서버가 아니라 이 브라우저에만 저장됩니다.
처음이면 칸은 랜덤으로 채워집니다.
</p>
</div>
<Link
href="/game"
className="inline-flex items-center justify-center rounded-full bg-amber-500 px-6 py-3 text-base font-semibold text-stone-950 transition hover:bg-amber-400"
>
게임 시작
</Link>
</header>
<section className="flex flex-col gap-4 rounded-2xl border border-white/10 bg-white/5 p-5">
<div>
<h2 className="text-lg font-semibold">룸 입장</h2>
<p className="mt-1 text-[11px] text-white/50">
치즈 규칙을 정한 뒤, 치지직 채널로 입장하세요. 룸이 없으면 자동으로 만들어집니다.
</p>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<label className="flex flex-col gap-1 text-sm">
<span className="text-white/70">몇 치즈(원)마다 주사위 1개</span>
<input
type="number"
min={1}
step={100}
value={roomCheesePerDice}
onChange={(e) => setRoomCheesePerDice(Number(e.target.value) || 1)}
className="rounded-lg border border-white/15 bg-black/40 px-3 py-2"
/>
</label>
<label className="flex items-center gap-2 self-end pb-2 text-sm">
<input
type="checkbox"
checked={roomMultiplyByCheese}
onChange={(e) => setRoomMultiplyByCheese(e.target.checked)}
className="h-4 w-4"
/>
<span>배수 누적 (금액 ÷ 기준만큼 주사위)</span>
</label>
{roomMultiplyByCheese && (
<label className="flex flex-col gap-1 text-sm sm:col-span-2">
<span className="text-white/70">최대 배수 (한 후원당 최대 주사위)</span>
<input
type="number"
min={1}
step={1}
value={roomMaxMultiplier}
onChange={(e) => setRoomMaxMultiplier(Math.max(1, Number(e.target.value) || 1))}
className="rounded-lg border border-white/15 bg-black/40 px-3 py-2"
/>
<span className="text-[11px] text-white/45">
예: 기준 {roomCheesePerDice}원 · 최대 {roomMaxMultiplier}배 → 한 번에 최대{' '}
{roomMaxMultiplier}개
</span>
</label>
)}
</div>
<div className="flex flex-wrap items-end gap-2 border-t border-white/10 pt-3">
<label className="flex min-w-[220px] flex-1 flex-col gap-1 text-sm">
<span className="text-white/70">치지직 채널 ID로 입장</span>
<input
type="text"
placeholder="채널ID 또는 https://chzzk.naver.com/live/…"
value={joinChannelInput}
onChange={(e) => setJoinChannelInput(e.target.value)}
className="rounded-lg border border-white/15 bg-black/40 px-3 py-2 text-sm"
/>
</label>
<button
type="button"
disabled={roomBusy || !joinChannelInput.trim()}
onClick={() => void joinByChzzkId()}
className="rounded-full bg-violet-600 px-4 py-2.5 text-sm font-semibold text-white hover:bg-violet-500 disabled:opacity-50"
>
{roomBusy ? '처리 중…' : '입장'}
</button>
</div>
<button
type="button"
disabled={roomBusy}
onClick={() => void enterWithoutIntegration()}
className="self-start rounded-full bg-white/15 px-4 py-2.5 text-sm text-white hover:bg-white/25 disabled:opacity-50"
>
연동 없이 입장
<span className="ml-1 text-white/50">(채팅·치지직 없음 · 수동 굴림)</span>
</button>
{roomError && (
<p className="rounded-lg bg-red-900/70 px-3 py-2 text-sm">{roomError}</p>
)}
</section>
{!ready || !board ? (
<p className="text-white/50">불러오는 중…</p>
) : (