diff --git a/.gitignore b/.gitignore
index 5ef6a52..64f2fe0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,6 +33,9 @@ yarn-error.log*
# env files (can opt-in for committing if needed)
.env*
+# local runtime logs / pids
+/logs/
+
# vercel
.vercel
diff --git a/app/api/chzzk/callback/route.ts b/app/api/chzzk/callback/route.ts
index 1fa5a68..22209d3 100644
--- a/app/api/chzzk/callback/route.ts
+++ b/app/api/chzzk/callback/route.ts
@@ -18,9 +18,11 @@ import {
ensureRoomChat,
} from '@/lib/chzzk/chat-session';
import {
+ clearDiceQueue,
createRoom,
findRoomByChannelId,
getRoom,
+ notifyRoom,
} from '@/lib/rooms/store';
export const runtime = 'nodejs';
@@ -87,6 +89,12 @@ export async function GET(request: Request) {
}
cancelScheduledDisconnectRoomChat(room.id);
+
+ if (decodedState.resetDiceQueue) {
+ clearDiceQueue(room.id);
+ notifyRoom(room.id);
+ }
+
try {
await ensureRoomChat(room.id);
} catch {
diff --git a/app/api/chzzk/login/route.ts b/app/api/chzzk/login/route.ts
index 60efbcb..f800e9c 100644
--- a/app/api/chzzk/login/route.ts
+++ b/app/api/chzzk/login/route.ts
@@ -28,11 +28,13 @@ export async function GET(request: Request) {
const cheesePerDice = Number(url.searchParams.get('cheesePerDice') || 1000);
const multiplyByCheese = url.searchParams.get('multiplyByCheese') !== '0';
const maxMultiplier = Number(url.searchParams.get('maxMultiplier') || 0);
+ const resetDiceQueue = url.searchParams.get('resetDiceQueue') === '1';
const { state, sealed } = createOAuthState({
cheesePerDice: Number.isFinite(cheesePerDice) ? cheesePerDice : 1000,
multiplyByCheese,
maxMultiplier: Number.isFinite(maxMultiplier) ? maxMultiplier : 0,
+ resetDiceQueue,
});
const res = NextResponse.redirect(buildChzzkLoginUrl(state));
diff --git a/app/api/rooms/join/route.ts b/app/api/rooms/join/route.ts
index 170f82f..45dfb65 100644
--- a/app/api/rooms/join/route.ts
+++ b/app/api/rooms/join/route.ts
@@ -11,9 +11,11 @@ import {
ensureRoomChat,
} from '@/lib/chzzk/chat-session';
import {
+ clearDiceQueue,
createRoom,
findRoomByChannelId,
getRoom,
+ notifyRoom,
toPublicRoom,
} from '@/lib/rooms/store';
@@ -30,6 +32,7 @@ export async function POST(request: Request) {
cheesePerDice?: number;
multiplyByCheese?: boolean;
maxMultiplier?: number;
+ resetDiceQueue?: boolean;
};
const session = await getRequestAuthSession(request);
@@ -92,6 +95,11 @@ export async function POST(request: Request) {
cancelScheduledDisconnectRoomChat(room.id);
+ if (body.resetDiceQueue) {
+ clearDiceQueue(room.id);
+ notifyRoom(room.id);
+ }
+
if (room.settings.chzzkEnabled) {
try {
await ensureRoomChat(room.id);
diff --git a/app/apple-icon.png b/app/apple-icon.png
new file mode 100644
index 0000000..a67e7d2
Binary files /dev/null and b/app/apple-icon.png differ
diff --git a/app/favicon.ico b/app/favicon.ico
index 718d6fe..c060142 100644
Binary files a/app/favicon.ico and b/app/favicon.ico differ
diff --git a/app/game/board-config.ts b/app/game/board-config.ts
index 6c8b86a..591a41b 100644
--- a/app/game/board-config.ts
+++ b/app/game/board-config.ts
@@ -12,10 +12,16 @@ export const CELL_TYPE_PRESETS = [
'다같이한잔',
'영어금지 3분',
'두잔마셔',
- '애교/매도 100자',
+ '애교/매도 50자',
'무인도',
'역방향',
'세계여행',
+ 'TMI 알려주기',
+ '뒤로 3칸',
+ '앞으로 4칸',
+ '방어권',
+ '방어권 전부삭제',
+ '물한잔',
] as const;
export type CellEventOption = {
@@ -36,7 +42,7 @@ export type BoardCellConfig = {
eventId?: string | null;
/** 칸 전용 이미지 (data URL 또는 상대 경로) */
imageSrc?: string | null;
- /** false면 게임에서 이름·이미지를 숨김 (기본 true) */
+ /** false면 게임에서 칸 이름(텍스트)만 숨김 (이미지는 유지) */
contentVisible?: boolean;
};
@@ -59,27 +65,27 @@ const DEFAULT_BOARD_LAYOUT: ReadonlyArray<{
{ type: '시작', eventId: null },
{ type: '한잔마셔' },
{ type: '~~체 3분' },
- { type: '안주 한입' },
+ { type: '방어권' },
{ type: '노래 1절' },
{ type: '다같이한잔' },
- { type: '영어금지 3분' },
- { type: '애교/매도 100자' },
+ { type: '앞으로 4칸' },
+ { type: '애교/매도 50자' },
{ type: '무인도', eventId: 'island' },
{ type: '안주 한입' },
{ type: '두잔마셔' },
- { type: '~~체 3분' },
+ { type: '물한잔' },
{ type: '한잔마셔' },
- { type: '애교/매도 100자' },
+ { type: 'TMI 알려주기' },
{ type: '역방향', eventId: 'reverse' },
- { type: '영어금지 3분' },
+ { type: '뒤로 3칸' },
{ type: '안주 한입' },
{ type: '다같이한잔' },
- { type: '노래 1절' },
+ { type: '방어권 전부삭제' },
{ type: '한잔마셔' },
{ type: '~~체 3분' },
{ type: '두잔마셔' },
{ type: '세계여행', eventId: 'airplane' },
- { type: '애교/매도 100자' },
+ { type: '애교/매도 50자' },
{ type: '안주 한입' },
{ type: '영어금지 3분' },
{ type: '다같이한잔' },
@@ -439,12 +445,7 @@ export function buildPlaneCells(config: BoardConfig = getOrCreateBoardConfig()):
export function cellColorFor(
eventId: string | null | undefined,
index: number,
- options?: { contentVisible?: boolean },
) {
- // 내용 숨김이면 종류가 색으로도 드러나지 않게 통일
- if (options?.contentVisible === false && index !== 0) {
- return '#be123c';
- }
if (index === 0) return '#059669';
switch (eventId) {
case 'island':
@@ -458,7 +459,9 @@ export function cellColorFor(
}
}
-/** 주사위 UI를 안쪽 빈 칸에 배치할 때 쓰는 span (1~2) */
+/** 주사위 UI를 안쪽 빈 칸에 배치할 때 쓰는 span (1~2)
+ * 세로 7칸 기준: 시작 행을 건너뛰고 안쪽 2칸 = 절대 위치 3칸까지
+ */
export function diceAreaSpan(width: number, height: number) {
return Math.min(2, Math.max(1, width - 2), Math.max(1, height - 2));
}
diff --git a/app/game/cell-event.ts b/app/game/cell-event.ts
index 51193ca..a9e1a27 100644
--- a/app/game/cell-event.ts
+++ b/app/game/cell-event.ts
@@ -10,7 +10,7 @@ export type PlaneCell = {
eventId?: string | null;
/** 칸 이미지 (없으면 텍스트만) */
imageSrc?: string | null;
- /** false면 게임 판에서 이름·이미지를 숨김 */
+ /** false면 게임 판에서 이름(텍스트)만 숨김 */
contentVisible?: boolean;
};
diff --git a/app/game/chat-feed.tsx b/app/game/chat-feed.tsx
index 38329c4..6c295fc 100644
--- a/app/game/chat-feed.tsx
+++ b/app/game/chat-feed.tsx
@@ -34,15 +34,15 @@ export default function ChatFeedPanel({
채팅
@@ -51,8 +51,8 @@ export default function ChatFeedPanel({
ref={listRef}
className={
compact
- ? 'min-h-0 flex-1 space-y-0.5 overflow-y-auto px-1.5 py-1 text-[10px] leading-snug'
- : 'min-h-0 flex-1 space-y-1 overflow-y-auto px-2 py-1.5 text-[11px] leading-snug'
+ ? 'min-h-0 flex-1 space-y-0.5 overflow-y-auto px-1.5 py-1 text-[20px] leading-snug'
+ : 'min-h-0 flex-1 space-y-1 overflow-y-auto px-2 py-1.5 text-[22px] leading-snug'
}
>
{items.length === 0 ? (
diff --git a/app/game/dice.tsx b/app/game/dice.tsx
index e3f5acf..ae551b8 100644
--- a/app/game/dice.tsx
+++ b/app/game/dice.tsx
@@ -18,6 +18,7 @@ import {
type RapierRigidBody,
} from '@react-three/rapier';
import {
+ Euler,
Quaternion,
SRGBColorSpace,
TextureLoader,
@@ -40,10 +41,33 @@ const WALL_HEIGHT = 8;
const WALL_HALF_T = WALL_THICKNESS / 2;
const WALL_HALF_H = WALL_HEIGHT / 2;
-const STILL_SPEED_SQ = 0.05;
-const STILL_FRAMES = 18;
+const STILL_SPEED_SQ = 0.035;
+const STILL_FRAMES = 24;
const MIN_VIEW_PX = 16;
+/**
+ * 과한 회전감 원인:
+ * 1) 시작 자세가 완전 랜덤 → 처음부터 면이 빙글
+ * 2) 마찰이 선속도를 각속도로 바꿈
+ * 3) 충돌 접점이 COM에서 벗어나며 각속도 발생
+ * 대응: 세워 두고 벽 쪽 병진을 강하게, 각감쇠+각속도 상한으로 억제.
+ */
+const DICE_MASS = 4.5;
+const DICE_RESTITUTION = 0.06;
+const DICE_FRICTION = 0.38;
+const DICE_LINEAR_DAMPING = 0.18;
+const DICE_ANGULAR_DAMPING = 3.4;
+const DICE_GRAVITY_SCALE = 1.2;
+const FLOOR_FRICTION = 0.42;
+const FLOOR_RESTITUTION = 0.03;
+const WALL_FRICTION = 0.22;
+const WALL_RESTITUTION = 0.28;
+const PHYSICS_GRAVITY: [number, number, number] = [0, -12.5, 0];
+/** 각속도 상한(rad/s) */
+const MAX_ANGULAR_SPEED = 7.5;
+/** 벽 방향 초기 선속도 */
+const WALL_PUSH_SPEED = 11.5;
+
const DEFAULT_DICE_FACE_TEXTURES = getDiceFaceTextureList(DEFAULT_ASSETS.dice);
const DICE_FACES = [
@@ -56,6 +80,7 @@ const DICE_FACES = [
];
const _quat = new Quaternion();
+const _euler = new Euler();
const _normal = new Vector3();
const WORLD_UP = new Vector3(0, 1, 0);
@@ -105,10 +130,30 @@ function InvisibleWalls() {
return (
<>
-
-
-
-
+
+
+
+
>
);
}
@@ -231,12 +276,23 @@ function DiceSettleWatcher({
const bodies = diceRefs.map((ref) => ref.current);
if (bodies.some((body) => !body)) return;
+ // 각속도만 과도하면 잘라서, 벽으로 미는 병진은 유지한 채 회전기만 줄임
+ for (const body of bodies) {
+ if (!body) continue;
+ const a = body.angvel();
+ const mag = Math.hypot(a.x, a.y, a.z);
+ if (mag > MAX_ANGULAR_SPEED) {
+ const s = MAX_ANGULAR_SPEED / mag;
+ body.setAngvel({ x: a.x * s, y: a.y * s, z: a.z * s }, false);
+ }
+ }
+
if (bodies.every((body) => isBodyStill(body!))) {
stillFrames.current += 1;
if (stillFrames.current >= STILL_FRAMES && !reported.current) {
reported.current = true;
const faces = bodies.map((body) => readTopFace(body!));
- const sum = faces.reduce((a, b) => a + b, 0);
+ const sum = faces.reduce((acc, value) => acc + value, 0);
onSettled({ sum, faces });
}
} else {
@@ -280,14 +336,19 @@ function RapierDiceExample({
-
+
-
+
@@ -296,10 +357,14 @@ function RapierDiceExample({
key={`die-${index}-${diceCount}-${canvasKey}`}
ref={diceRefs[index]}
type="dynamic"
- position={[x, 4, index === 0 ? 0 : 0.15]}
+ position={[x, 0.65, index === 0 ? 0 : 0.15]}
colliders="cuboid"
- restitution={0.2}
- friction={0.6}
+ mass={DICE_MASS}
+ restitution={DICE_RESTITUTION}
+ friction={DICE_FRICTION}
+ linearDamping={DICE_LINEAR_DAMPING}
+ angularDamping={DICE_ANGULAR_DAMPING}
+ gravityScale={DICE_GRAVITY_SCALE}
>
= [
+ [1, 0],
+ [-1, 0],
+ [0, 1],
+ [0, -1],
+ ];
+ const [wx, wz] = wallDirs[Math.floor(Math.random() * wallDirs.length)]!;
+ let dx = wx + (Math.random() - 0.5) * 0.35;
+ let dz = wz + (Math.random() - 0.5) * 0.35;
+ const dLen = Math.hypot(dx, dz) || 1;
+ dx /= dLen;
+ dz /= dLen;
+
+ // 완전 랜덤 자세 대신: 세워 둔 채 약한 기울기 + 진행 방향 쪽 살짝 기울임
+ // (위에서 볼 때 면이 빙글빙글 도는 느낌을 크게 줄임)
+ const yaw = Math.random() * Math.PI * 2;
+ const tipToward = 0.18 + Math.random() * 0.22; // ~10–23°
+ const tipSide = (Math.random() - 0.5) * 0.16;
+ // 진행 방향이 +X면 Z축 회전으로 기울고, +Z면 X축 회전으로 기울게
+ _euler.set(-dz * tipToward + tipSide, yaw, dx * tipToward + tipSide, 'YXZ');
+ _quat.setFromEuler(_euler);
+ body.setRotation({ x: _quat.x, y: _quat.y, z: _quat.z, w: _quat.w }, true);
+
+ // 병진 위주: 선속도로 벽을 향해 밀기 (토크 임펄스 없음)
+ const speed = WALL_PUSH_SPEED + Math.random() * 3.5;
+ body.setLinvel({ x: dx * speed, y: 0.35 + Math.random() * 0.45, z: dz * speed }, true);
+
+ // 질량 대비 추가 밀기(COM). 회전 임펄스는 넣지 않음
+ const pushImpulse = DICE_MASS * (2.8 + Math.random() * 1.2);
+ body.applyImpulse({ x: dx * pushImpulse, y: 0.4, z: dz * pushImpulse }, true);
return true;
}
@@ -436,10 +528,13 @@ export default memo(function Dice({
rolling || disabled || !canvasReady || (requireDiceQueue && diceQueue <= 0);
return (
-
+
{canvasReady ? (
)}
+
+
+
{
void (async () => {
@@ -502,7 +600,7 @@ export default memo(function Dice({
>
Roll{requireDiceQueue ? ` (${diceQueue})` : ''}
-
+
{queueHint
? queueHint
: !canvasReady
diff --git a/app/game/diceplane.tsx b/app/game/diceplane.tsx
index d82af86..dcb8e12 100644
--- a/app/game/diceplane.tsx
+++ b/app/game/diceplane.tsx
@@ -52,11 +52,7 @@ export default function DicePlane({
const [chessmanSrc, setChessmanSrc] = useState('/chessman/default.png');
const [hopKey, setHopKey] = useState(0);
const [moving, setMoving] = useState(false);
- const [chatOpen, setChatOpen] = useState(true);
-
- useEffect(() => {
- if (showChatFeed) setChatOpen(true);
- }, [showChatFeed]);
+ const [chatOpen, setChatOpen] = useState(false);
const [landedLabel, setLandedLabel] = useState
(null);
const [landedKey, setLandedKey] = useState(0);
const [statusText, setStatusText] = useState('시계방향');
@@ -243,7 +239,7 @@ export default function DicePlane({
{dicePlaneArray.map((plane, index) => {
const isStart = index === 0;
const isLanded = !moving && index === chessmanIndex && landedLabel != null;
- const showContent = plane.contentVisible !== false;
+ const showText = plane.contentVisible !== false;
return (
- {showContent ? (isStart ? '시작' : plane.type) : null}
+ {showText ? (isStart ? '시작' : plane.type) : null}
);
})}
diff --git a/app/icon.png b/app/icon.png
new file mode 100644
index 0000000..57701e8
Binary files /dev/null and b/app/icon.png differ
diff --git a/app/layout.tsx b/app/layout.tsx
index 9852c15..6617fe0 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -13,8 +13,8 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
- title: "Create Next App",
- description: "Generated by create next app",
+ title: "알코올 마블!",
+ description: "치지직 온라인 주루마블",
};
export default function RootLayout({ children }: LayoutProps<"/">) {
diff --git a/app/page.tsx b/app/page.tsx
index ed159c4..d766b1c 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,5 +1,6 @@
'use client';
+import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useEffect, useRef, useState } from 'react';
import {
@@ -147,7 +148,7 @@ function BoardEditor({
주사위판 칸 (board)
- 가로·세로로 판 크기를 바꾸고, 칸마다 이름·효과·이미지·내용 표시를 설정합니다. 0번(시작) 이름은
+ 가로·세로로 판 크기를 바꾸고, 칸마다 이름·효과·이미지·텍스트 표시를 설정합니다. 0번(시작) 이름은
고정입니다.
@@ -157,14 +158,14 @@ function BoardEditor({
className="rounded bg-white/15 px-3 py-1.5 text-xs font-medium text-white hover:bg-white/25"
onClick={() => onChange(setAllCellsContentVisible(true, board))}
>
- 전체 내용 표시
+ 전체 텍스트 표시
onChange(setAllCellsContentVisible(false, board))}
>
- 전체 내용 숨김
+ 전체 텍스트 숨김
{
const cell = board.cells[index];
const active = selected === index;
- const showContent = cell?.contentVisible !== false;
+ const showText = cell?.contentVisible !== false;
return (
setSelected(index)}
className="relative min-h-10 rounded px-0.5 text-[10px] font-semibold text-white sm:text-[11px]"
style={{
gridColumn: g.x + 1,
gridRow: g.y + 1,
backgroundColor: cellColorFor(cell?.eventId, index),
- backgroundImage:
- showContent && cell?.imageSrc ? `url(${cell.imageSrc})` : undefined,
+ backgroundImage: cell?.imageSrc ? `url(${cell.imageSrc})` : undefined,
backgroundSize: 'cover',
backgroundPosition: 'center',
- textShadow:
- showContent && cell?.imageSrc ? '0 1px 2px rgba(0,0,0,0.9)' : undefined,
+ textShadow: cell?.imageSrc ? '0 1px 2px rgba(0,0,0,0.9)' : undefined,
outline: active ? '2px solid #fde68a' : undefined,
outlineOffset: 1,
- opacity: showContent ? 1 : 0.7,
}}
>
{index === 0 ? '시작' : cell?.type}
- {!showContent && (
+ {!showText && (
- 숨김
+ 텍스트숨김
)}
@@ -305,10 +303,10 @@ function BoardEditor({
)
}
/>
- 게임에서 이 칸 내용(이름·이미지) 표시
+ 게임에서 이 칸 이름(텍스트) 표시
- 끄면 판에는 색만 보이고, 착지 시 효과·안내는 그대로 동작합니다.
+ 끄면 판에서 이름 글자만 숨깁니다. 이미지·색·착지 효과는 그대로입니다.
@@ -376,6 +374,7 @@ export default function Home() {
const [roomCheesePerDice, setRoomCheesePerDice] = useState(1000);
const [roomMultiplyByCheese, setRoomMultiplyByCheese] = useState(true);
const [roomMaxMultiplier, setRoomMaxMultiplier] = useState(10);
+ const [resetDiceQueueOnJoin, setResetDiceQueueOnJoin] = useState(false);
const [joinChannelInput, setJoinChannelInput] = useState('');
const [roomBusy, setRoomBusy] = useState(false);
const [roomError, setRoomError] = useState(null);
@@ -436,6 +435,7 @@ export default function Home() {
cheesePerDice: roomCheesePerDice,
multiplyByCheese: roomMultiplyByCheese,
maxMultiplier: roomMultiplyByCheese ? roomMaxMultiplier : 0,
+ resetDiceQueue: resetDiceQueueOnJoin,
});
const startChzzkLogin = () => {
@@ -444,6 +444,7 @@ export default function Home() {
cheesePerDice: String(opts.cheesePerDice),
multiplyByCheese: opts.multiplyByCheese ? '1' : '0',
maxMultiplier: String(opts.maxMultiplier),
+ resetDiceQueue: opts.resetDiceQueue ? '1' : '0',
});
window.location.href = `/api/chzzk/login?${q.toString()}`;
};
@@ -658,6 +659,15 @@ export default function Home() {
)}
+
+ setResetDiceQueueOnJoin(e.target.checked)}
+ className="h-4 w-4"
+ />
+ 남은 주사위 개수를 초기화
+
@@ -790,10 +800,10 @@ export default function Home() {
-
+ {/*
ZIP: manifest.json, board.json, cells/N.*, background.*, chessman.*,
dice/1|2/면.*, dice-floor.*
-
+ */}
{error && (
{error}
@@ -943,6 +953,26 @@ export default function Home() {
)}
+
+
);
diff --git a/app/privacy/page.tsx b/app/privacy/page.tsx
new file mode 100644
index 0000000..6c99688
--- /dev/null
+++ b/app/privacy/page.tsx
@@ -0,0 +1,82 @@
+import Link from 'next/link';
+
+export const metadata = {
+ title: '개인정보·쿠키 고지 | 알코올 마블',
+ description: '알코올 마블 서비스의 쿠키 및 저장 정보 안내',
+};
+
+export default function PrivacyPage() {
+ return (
+
+
+
+
+
+ 1. 수집·이용하는 정보
+
+ 광고·분석용 추적 쿠키나 마케팅 SDK는 사용하지 않습니다. 서비스 제공에 필요한
+ 범위에서만 아래 정보를 사용합니다.
+
+
+
+ 인증 쿠키 : 치지직 로그인 후
+ 본인 채널 확인용 세션입니다. 브라우저에서만 쓰이며(HttpOnly), 치지직
+ 액세스/리프레시 토큰은 저장하지 않습니다.
+
+
+ 로그인 보안 쿠키 : OAuth
+ 진행 중 CSRF 방지용으로 짧은 시간만 유지됩니다.
+
+
+ 브라우저 로컬 저장소 : 주사위판
+ 칸 설정·이미지 에셋 등은 서버가 아니라 사용자 브라우저에만 저장됩니다.
+
+
+ 룸·채팅 연동 : 입장한 치지직
+ 채널의 채팅/후원 이벤트는 해당 방송 룸 진행을 위해 서버 메모리에서 일시적으로
+ 처리됩니다.
+
+
+
+
+
+ 2. 쿠키 안내
+
+ 위에 적은 쿠키는 로그인·보안 등 서비스 제공에 필요한 항목입니다. 선택형
+ 광고/통계 쿠키는 두지 않습니다. 브라우저에서 쿠키를 차단하면 치지직 로그인
+ 입장이 동작하지 않을 수 있습니다.
+
+
+
+
+
+
최종 안내일: 2026-09-21
+
+
+ );
+}
diff --git a/lib/auth/session.ts b/lib/auth/session.ts
index 8928c8d..b280672 100644
--- a/lib/auth/session.ts
+++ b/lib/auth/session.ts
@@ -17,6 +17,7 @@ export type OAuthStatePayload = {
cheesePerDice?: number;
multiplyByCheese?: boolean;
maxMultiplier?: number;
+ resetDiceQueue?: boolean;
/** unix ms */
exp: number;
};
@@ -116,6 +117,7 @@ export function createOAuthState(input: {
cheesePerDice?: number;
multiplyByCheese?: boolean;
maxMultiplier?: number;
+ resetDiceQueue?: boolean;
}) {
const state = b64urlEncode(randomBytes(24));
const payload: OAuthStatePayload = {
@@ -123,6 +125,7 @@ export function createOAuthState(input: {
cheesePerDice: input.cheesePerDice,
multiplyByCheese: input.multiplyByCheese,
maxMultiplier: input.maxMultiplier,
+ resetDiceQueue: input.resetDiceQueue,
exp: Date.now() + 10 * 60 * 1000,
};
return { state, sealed: seal(payload) };
diff --git a/lib/rooms/store.ts b/lib/rooms/store.ts
index da3930a..02d502b 100644
--- a/lib/rooms/store.ts
+++ b/lib/rooms/store.ts
@@ -270,6 +270,14 @@ export function addDice(roomId: string, count = 1) {
return room;
}
+/** 남은 주사위 큐를 0으로 초기화 */
+export function clearDiceQueue(roomId: string) {
+ const room = getRoom(roomId);
+ if (!room) return null;
+ room.diceQueue = 0;
+ return room;
+}
+
export function toPublicRoom(room: RoomRecord): PublicRoomState {
return {
id: room.id,
diff --git a/logs/build.log b/logs/build.log
deleted file mode 100644
index f8f2be6..0000000
--- a/logs/build.log
+++ /dev/null
@@ -1,31 +0,0 @@
-
-> drinkmarble@0.1.0 build
-> next build
-
-▲ Next.js 16.3.3 (Turbopack)
-⚠ Warning: Next.js ignored package-lock.json in /home/kimyu because it is outside the current Git repository (/home/kimyu/workspace/nextjs/drinkmarble).
- To use this directory, set `turbopack.root` in your Next.js config.
-
-✓ Running next.config.ts took 25ms
-
- Creating an optimized production build ...
-✓ Compiled successfully in 3.6s
- Running TypeScript ...
- Finished TypeScript in 2.6s ...
- Collecting page data using 7 workers ...
- Generating static pages using 7 workers (0/6) ...
- Generating static pages using 7 workers (1/6)
- Generating static pages using 7 workers (2/6)
- Generating static pages using 7 workers (4/6)
-✓ Generating static pages using 7 workers (6/6) in 677ms
- Finalizing page optimization ...
-
-Route (app)
-┌ ○ /
-├ ○ /_not-found
-├ ○ /game
-└ ○ /main
-
-
-○ (Static) prerendered as static content
-
diff --git a/logs/drinkmarble.log b/logs/drinkmarble.log
deleted file mode 100644
index 449ae23..0000000
--- a/logs/drinkmarble.log
+++ /dev/null
@@ -1,26 +0,0 @@
-
-> drinkmarble@0.1.0 start
-> next start -p 13000
-
-▲ Next.js 16.3.3
-- Local: http://localhost:13000
-- Network: http://172.30.1.31:13000
-✓ Ready in 149ms
-⚠ Warning: Next.js ignored package-lock.json in /home/kimyu because it is outside the current Git repository (/home/kimyu/workspace/nextjs/drinkmarble).
- To use this directory, set `outputFileTracingRoot` in your Next.js config.
-
-✓ Running next.config.ts took 30ms
-Error: Could not find a production build in the '.next' directory. Try building your app with 'next build' before starting the production server. https://nextjs.org/docs/messages/production-start-no-build-id
- at ignore-listed frames
-
-> drinkmarble@0.1.0 start
-> next start -p 13000
-
-▲ Next.js 16.3.3
-- Local: http://localhost:13000
-- Network: http://172.30.1.31:13000
-✓ Ready in 151ms
-⚠ Warning: Next.js ignored package-lock.json in /home/kimyu because it is outside the current Git repository (/home/kimyu/workspace/nextjs/drinkmarble).
- To use this directory, set `outputFileTracingRoot` in your Next.js config.
-
-✓ Running next.config.ts took 30ms
diff --git a/logs/drinkmarble.pid b/logs/drinkmarble.pid
deleted file mode 100644
index 3fff9e9..0000000
--- a/logs/drinkmarble.pid
+++ /dev/null
@@ -1 +0,0 @@
-2448360
diff --git a/logs/release.log b/logs/release.log
deleted file mode 100644
index 1e46aba..0000000
--- a/logs/release.log
+++ /dev/null
@@ -1,9 +0,0 @@
-
-> drinkmarble@0.1.0 start
-> next start -H 0.0.0.0 -p 13000
-
-▲ Next.js 16.3.3
-- Local: http://localhost:13000
-- Network: http://0.0.0.0:13000
-✓ Ready in 153ms
-✓ Running next.config.ts took 31ms