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 ? ( )} +
+ +
-
+
{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))} > - 전체 내용 표시 + 전체 텍스트 표시 @@ -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() { )} +
@@ -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. 쿠키 안내

+

+ 위에 적은 쿠키는 로그인·보안 등 서비스 제공에 필요한 항목입니다. 선택형 + 광고/통계 쿠키는 두지 않습니다. 브라우저에서 쿠키를 차단하면 치지직 로그인 + 입장이 동작하지 않을 수 있습니다. +

+
+ +
+

3. 문의

+

+ 개인정보·쿠키 관련 문의는 아래로 연락해 주세요. +

+

+ 이메일:{' '} + + kimyu@kimyu.xyz + +

+
+ +

최종 안내일: 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