From 6f72824598a40e924790589ebecb15cd7ddb6316 Mon Sep 17 00:00:00 2001 From: kimyu Date: Wed, 16 Sep 2026 05:29:50 +0900 Subject: [PATCH] =?UTF-8?q?=EC=B9=98=EC=A7=80=EC=A7=81=20=EC=B1=84?= =?UTF-8?q?=EB=84=90=20ID=EB=A1=9C=20=EC=9E=85=EC=9E=A5=ED=95=98=EA=B2=8C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=EC=9D=BC=EB=B6=80=20?= =?UTF-8?q?=ED=8C=90=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/game/asset-pack.ts | 1 + app/game/board-config.ts | 65 +++++++++++++++++++++++++++++--- app/game/cell-event.ts | 2 + app/game/diceplane.tsx | 52 ++++++++++++++++++++------ app/page.tsx | 81 +++++++++++++++++++++++++++++++++------- 5 files changed, 170 insertions(+), 31 deletions(-) diff --git a/app/game/asset-pack.ts b/app/game/asset-pack.ts index f01e6ae..c29a351 100644 --- a/app/game/asset-pack.ts +++ b/app/game/asset-pack.ts @@ -83,6 +83,7 @@ function boardForExport(board: BoardConfig): BoardConfig { type: cell.type, eventId: cell.eventId ?? null, imageSrc: null, + contentVisible: cell.contentVisible !== false, })), }; } diff --git a/app/game/board-config.ts b/app/game/board-config.ts index eeb5937..4a3e90a 100644 --- a/app/game/board-config.ts +++ b/app/game/board-config.ts @@ -34,6 +34,8 @@ export type BoardCellConfig = { eventId?: string | null; /** 칸 전용 이미지 (data URL 또는 상대 경로) */ imageSrc?: string | null; + /** false면 게임에서 이름·이미지를 숨김 (기본 true) */ + contentVisible?: boolean; }; export type BoardConfig = { @@ -117,9 +119,18 @@ export function createRandomBoardConfig( const geo = buildPerimeterGeometry(w, h); const specials = defaultSpecials(w, h); const cells: BoardCellConfig[] = geo.map((_, index) => { - if (index === 0) return { type: '시작', eventId: null, imageSrc: null }; - if (specials[index]) return { ...specials[index], imageSrc: null }; - return { type: randomPresetType(), eventId: null, imageSrc: null }; + if (index === 0) { + return { type: '시작', eventId: null, imageSrc: null, contentVisible: true }; + } + if (specials[index]) { + return { ...specials[index], imageSrc: null, contentVisible: true }; + } + return { + type: randomPresetType(), + eventId: null, + imageSrc: null, + contentVisible: true, + }; }); return { version: BOARD_VERSION, width: w, height: h, cells }; } @@ -136,7 +147,8 @@ function sanitizeCell(src: BoardCellConfig | undefined, fallback: BoardCellConfi : String(src.eventId); const imageSrc = typeof src.imageSrc === 'string' && src.imageSrc.length > 0 ? src.imageSrc : null; - return { type: src.type.trim(), eventId, imageSrc }; + const contentVisible = src.contentVisible !== false; + return { type: src.type.trim(), eventId, imageSrc, contentVisible }; } export function normalizeBoardConfig(input: BoardConfig | null | undefined): BoardConfig { @@ -154,7 +166,12 @@ export function normalizeBoardConfig(input: BoardConfig | null | undefined): Boa cells.push(sanitizeCell(input.cells[i], base.cells[i])); } - cells[0] = { type: '시작', eventId: null, imageSrc: cells[0]?.imageSrc ?? null }; + cells[0] = { + type: '시작', + eventId: null, + imageSrc: cells[0]?.imageSrc ?? null, + contentVisible: cells[0]?.contentVisible !== false, + }; return { version: BOARD_VERSION, width, height, cells }; } @@ -227,6 +244,7 @@ export function resizeBoardConfig( type: '시작', eventId: null, imageSrc: prev.cells[0]?.imageSrc ?? null, + contentVisible: prev.cells[0]?.contentVisible !== false, }); continue; } @@ -242,6 +260,7 @@ export function resizeBoardConfig( type: src?.type && src.type !== '시작' ? src.type : randomPresetType(), eventId: src?.eventId ?? null, imageSrc: src?.imageSrc ?? null, + contentVisible: src?.contentVisible !== false, }); } @@ -258,6 +277,10 @@ export function updateBoardCell( if (index < 0 || index >= current.cells.length) return current; const cells = current.cells.map((cell, i) => { if (i !== index) return cell; + const contentVisible = + patch.contentVisible === undefined + ? cell.contentVisible !== false + : Boolean(patch.contentVisible); if (i === 0) { return { type: '시작', @@ -268,6 +291,7 @@ export function updateBoardCell( : patch.imageSrc === '' ? null : patch.imageSrc, + contentVisible, }; } return { @@ -284,6 +308,7 @@ export function updateBoardCell( : patch.imageSrc === '' ? null : patch.imageSrc, + contentVisible, }; }); const next = { @@ -296,6 +321,25 @@ export function updateBoardCell( return next; } +/** 모든 칸의 내용 표시 여부를 한 번에 설정 */ +export function setAllCellsContentVisible( + visible: boolean, + current = getOrCreateBoardConfig(), +): BoardConfig { + const cells = current.cells.map((cell) => ({ + ...cell, + contentVisible: visible, + })); + const next = { + version: BOARD_VERSION, + width: current.width, + height: current.height, + cells, + }; + writeStoredBoard(next); + return next; +} + export function buildPlaneCells(config: BoardConfig = getOrCreateBoardConfig()): PlaneCell[] { const normalized = normalizeBoardConfig(config); const geo = buildPerimeterGeometry(normalized.width, normalized.height); @@ -306,10 +350,19 @@ export function buildPlaneCells(config: BoardConfig = getOrCreateBoardConfig()): type: normalized.cells[index]?.type ?? (index === 0 ? '시작' : '한잔'), eventId: index === 0 ? null : (normalized.cells[index]?.eventId ?? null), imageSrc: normalized.cells[index]?.imageSrc ?? null, + contentVisible: normalized.cells[index]?.contentVisible !== false, })); } -export function cellColorFor(eventId: string | null | undefined, index: number) { +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': diff --git a/app/game/cell-event.ts b/app/game/cell-event.ts index 3590998..9a00f1d 100644 --- a/app/game/cell-event.ts +++ b/app/game/cell-event.ts @@ -10,6 +10,8 @@ export type PlaneCell = { eventId?: string | null; /** 칸 이미지 (없으면 텍스트만) */ imageSrc?: string | null; + /** false면 게임 판에서 이름·이미지를 숨김 */ + contentVisible?: boolean; }; export type TurnContext = { diff --git a/app/game/diceplane.tsx b/app/game/diceplane.tsx index 37d93cc..751e1e2 100644 --- a/app/game/diceplane.tsx +++ b/app/game/diceplane.tsx @@ -10,8 +10,10 @@ import { getAssets } from './asset-storage'; import { buildPlaneCells, cellColorFor, + DEFAULT_PLANE_SIZE, diceAreaSpan, getOrCreateBoardConfig, + perimeterLength, } from './board-config'; const STEP_MS = 280; @@ -34,15 +36,13 @@ export default function DicePlane({ chatFeed?: ChatFeedItem[]; showChatFeed?: boolean; }) { - const initial = getOrCreateBoardConfig(); - const [planeSize, setPlaneSize] = useState({ - width: initial.width, - height: initial.height, + const [boardReady, setBoardReady] = useState(false); + const [planeSize, setPlaneSize] = useState<{ width: number; height: number }>({ + width: DEFAULT_PLANE_SIZE.width, + height: DEFAULT_PLANE_SIZE.height, }); - const [dicePlaneArray, setDicePlaneArray] = useState(() => - buildPlaneCells(initial), - ); - const chessmanRef = useRef(new Chessman(initial.cells.length)); + const [dicePlaneArray, setDicePlaneArray] = useState([]); + const chessmanRef = useRef(new Chessman(perimeterLength(DEFAULT_PLANE_SIZE.width, DEFAULT_PLANE_SIZE.height))); const boardRef = useRef(dicePlaneArray); const [chessmanIndex, setChessmanIndex] = useState(0); const [chessmanSrc, setChessmanSrc] = useState('/chessman/default.png'); @@ -59,6 +59,7 @@ export default function DicePlane({ const [islandTurnsLeft, setIslandTurnsLeft] = useState(0); const moveTimerRef = useRef | null>(null); + // SSR/초기 렌더에서 랜덤 판을 쓰지 않고, 브라우저에 저장된 설정을 로드 useEffect(() => { const board = getOrCreateBoardConfig(); const cells = buildPlaneCells(board); @@ -72,6 +73,7 @@ export default function DicePlane({ setStatusText('시계방향'); setIslandTurnsLeft(0); setDicePlaneArray(cells); + setBoardReady(true); }, []); useEffect(() => { @@ -175,6 +177,25 @@ export default function DicePlane({ const chessmanCell = dicePlaneArray[chessmanIndex] ?? dicePlaneArray[0]; const diceSpan = diceAreaSpan(planeSize.width, planeSize.height); + if (!boardReady || dicePlaneArray.length === 0) { + return ( +
+ 판 불러오는 중… +
+ ); + } + return (
{ const isStart = index === 0; const isLanded = !moving && index === chessmanIndex && landedLabel != null; + const showContent = plane.contentVisible !== false; return (
- {isStart ? '시작' : plane.type} + {showContent ? (isStart ? '시작' : plane.type) : null}
); })} diff --git a/app/page.tsx b/app/page.tsx index f310f1a..4a0e4a6 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -40,6 +40,8 @@ import { perimeterLength, randomizeAndSaveBoard, resizeBoardConfig, + setAllCellsContentVisible, + setBoardConfig, updateBoardCell, } from './game/board-config'; @@ -145,16 +147,33 @@ function BoardEditor({

주사위판 칸 (board)

- 가로·세로로 판 크기를 바꾸고, 칸마다 이름·효과·이미지를 설정합니다. 0번(시작) 이름은 고정입니다. + 가로·세로로 판 크기를 바꾸고, 칸마다 이름·효과·이미지·내용 표시를 설정합니다. 0번(시작) 이름은 + 고정입니다.

- +
+ + + +
@@ -195,26 +214,35 @@ function BoardEditor({ {geo.map((g, index) => { const cell = board.cells[index]; const active = selected === index; + const showContent = cell?.contentVisible !== false; return ( ); })} @@ -262,6 +290,27 @@ function BoardEditor({ ))}
+
+ +

+ 끄면 판에는 색만 보이고, 착지 시 효과·안내는 그대로 동작합니다. +

+