치지직 앱으로 연동하고, 연동 확인된 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

@@ -1,12 +1,13 @@
'use client';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react';
import Dice from './dice';
import ChatFeedPanel, { type ChatFeedItem } from './chat-feed';
import { Chessman } from './chessman';
import ChessmanPiece from './chessman-piece';
import { applyLandEvent, type PlaneCell } from './cell-event';
import { getAssets } from './asset-storage';
import { SHOW_DEBUG_UI } from '@/lib/ui/debug';
import {
buildPlaneCells,
cellColorFor,
@@ -29,12 +30,15 @@ export default function DicePlane({
onConsumeDice,
chatFeed = [],
showChatFeed = false,
settingsButton = null,
}: {
requireDiceQueue?: boolean;
diceQueue?: number;
onConsumeDice?: (count?: number) => Promise<{ ok: boolean; error?: string }>;
chatFeed?: ChatFeedItem[];
showChatFeed?: boolean;
/** 판 안쪽 왼쪽 아래(둘레 칸 밖)에 배치할 설정 버튼 */
settingsButton?: ReactNode;
}) {
const [boardReady, setBoardReady] = useState(false);
const [planeSize, setPlaneSize] = useState<{ width: number; height: number }>({
@@ -57,7 +61,9 @@ export default function DicePlane({
const [landedKey, setLandedKey] = useState(0);
const [statusText, setStatusText] = useState('시계방향');
const [islandTurnsLeft, setIslandTurnsLeft] = useState(0);
const [pickingDestination, setPickingDestination] = useState(false);
const moveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const pendingRollRef = useRef(0);
// SSR/초기 렌더에서 랜덤 판을 쓰지 않고, 브라우저에 저장된 설정을 로드
useEffect(() => {
@@ -72,6 +78,7 @@ export default function DicePlane({
setLandedLabel(null);
setStatusText('시계방향');
setIslandTurnsLeft(0);
setPickingDestination(false);
setDicePlaneArray(cells);
setBoardReady(true);
}, []);
@@ -104,6 +111,15 @@ export default function DicePlane({
setStatusText(man.facing === 1 ? '시계방향' : '반시계방향');
setIslandTurnsLeft(man.islandTurnsLeft);
if (result?.chooseDestination) {
pendingRollRef.current = roll;
setPickingDestination(true);
showBanner(result.message);
return;
}
setPickingDestination(false);
if (result?.teleportTo != null) {
setChessmanIndex(result.teleportTo);
setHopKey((k) => k + 1);
@@ -116,6 +132,19 @@ export default function DicePlane({
[showBanner],
);
const onCellPick = useCallback(
(index: number) => {
if (!pickingDestination || moving) return;
const man = chessmanRef.current;
man.teleport(index);
setChessmanIndex(index);
setHopKey((k) => k + 1);
setPickingDestination(false);
applyLanding(index, pendingRollRef.current);
},
[pickingDestination, moving, applyLanding],
);
const animateMove = useCallback(
(steps: number, roll: number) => {
const amount = Math.max(0, Math.trunc(steps));
@@ -153,6 +182,7 @@ export default function DicePlane({
if (roll === 0) return;
setLandedLabel(null);
setPickingDestination(false);
const man = chessmanRef.current;
if (man.isOnIsland) {
@@ -217,6 +247,16 @@ export default function DicePlane({
return (
<div
key={plane.id}
role={pickingDestination ? 'button' : undefined}
tabIndex={pickingDestination ? 0 : undefined}
onClick={() => onCellPick(index)}
onKeyDown={(e) => {
if (!pickingDestination) return;
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onCellPick(index);
}
}}
style={{
gridColumn: plane.x + 1,
gridRow: plane.y + 1,
@@ -238,12 +278,16 @@ export default function DicePlane({
padding: 4,
boxShadow: isLanded
? '0 0 0 3px #fde68a, 0 0 18px rgba(253, 224, 71, 0.75)'
: isStart
? 'inset 0 0 0 2px #a7f3d0'
: undefined,
: pickingDestination
? '0 0 0 2px rgba(125, 211, 252, 0.85)'
: isStart
? 'inset 0 0 0 2px #a7f3d0'
: undefined,
transform: isLanded ? 'scale(1.04)' : undefined,
transition: 'box-shadow 180ms ease, transform 180ms ease',
zIndex: isLanded ? 4 : undefined,
zIndex: pickingDestination ? 8 : isLanded ? 4 : undefined,
cursor: pickingDestination ? 'pointer' : undefined,
pointerEvents: pickingDestination ? 'auto' : undefined,
textShadow:
showContent && plane.imageSrc
? '0 1px 3px rgba(0,0,0,0.85)'
@@ -276,13 +320,34 @@ export default function DicePlane({
>
<Dice
onRolled={onDiceRolled}
disabled={moving}
disabled={moving || pickingDestination}
requireDiceQueue={requireDiceQueue}
diceQueue={diceQueue}
onConsumeDice={onConsumeDice}
/>
</div>
{settingsButton && (
<div
style={{
// 둘레 칸 밖: 안쪽 영역 왼쪽 아래
gridColumn: 2,
gridRow: Math.max(3, planeSize.height - 1),
zIndex: 8,
display: 'flex',
alignItems: 'flex-end',
justifyContent: 'flex-start',
padding: 4,
boxSizing: 'border-box',
minWidth: 0,
minHeight: 0,
pointerEvents: 'none',
}}
>
<div style={{ pointerEvents: 'auto' }}>{settingsButton}</div>
</div>
)}
{showChatFeed && (
<div
style={{
@@ -336,10 +401,12 @@ export default function DicePlane({
)}
</div>
<div className="pointer-events-none absolute bottom-3 left-3 z-20 rounded bg-black/65 px-3 py-1 text-sm text-white">
{statusText}
{islandTurnsLeft > 0 ? ` · 무인도 ${islandTurnsLeft}턴` : ''}
</div>
{SHOW_DEBUG_UI && (
<div className="pointer-events-none absolute bottom-3 left-3 z-20 rounded bg-black/65 px-3 py-1 text-sm text-white">
{statusText}
{islandTurnsLeft > 0 ? ` · 무인도 ${islandTurnsLeft}턴` : ''}
</div>
)}
{landedLabel && (
<div