run(() => engine.adopt())} />
+
+
{message && (
⚠️ {message}
diff --git a/src/components/game/VillageView.tsx b/src/components/game/VillageView.tsx
new file mode 100644
index 0000000..bb9b64c
--- /dev/null
+++ b/src/components/game/VillageView.tsx
@@ -0,0 +1,375 @@
+"use client";
+
+import { useEffect, useRef } from "react";
+import { engine, catRate } from "@/lib/game/engine";
+
+// ---------- 아이소메트릭 그리드 ----------
+const COLS = 11;
+const ROWS = 11;
+const TW = 64; // 타일 가로
+const TH = 32; // 타일 세로
+const PAD_Y = 56;
+const CANVAS_W = ((COLS + ROWS) * TW) / 2 + 16;
+const CANVAS_H = ((COLS + ROWS) * TH) / 2 + PAD_Y + 48;
+const ORIGIN_X = (ROWS * TW) / 2 + 8;
+
+const isoX = (i: number, j: number) => ORIGIN_X + ((i - j) * TW) / 2;
+const isoY = (i: number, j: number) => PAD_Y + ((i + j) * TH) / 2;
+
+// 물 (동쪽 끝 열), 부두는 물가에 붙임
+const isWater = (i: number, j: number) => i >= COLS - 1;
+
+// 건물 위치 (타일 좌표)
+const BUILDING_TILES: Record = {
+ hall: { i: 5, j: 3 },
+ dock_1: { i: 9, j: 5 },
+ lumberyard_1: { i: 2, j: 8 },
+};
+
+const TREES: Array<{ i: number; j: number }> = [
+ { i: 0, j: 7 }, { i: 1, j: 9 }, { i: 3, j: 9 }, { i: 0, j: 9 },
+ { i: 1, j: 6 }, { i: 3, j: 7 },
+];
+const DECOR: Array<{ i: number; j: number; emoji: string }> = [
+ { i: 7, j: 1, emoji: "🌼" }, { i: 3, j: 2, emoji: "🌷" },
+ { i: 8, j: 8, emoji: "🌾" }, { i: 6, j: 9, emoji: "🍄" },
+ { i: 1, j: 3, emoji: "🪨" },
+];
+
+const isBuildingTile = (i: number, j: number) =>
+ Object.values(BUILDING_TILES).some((t) => t.i === i && t.j === j);
+const isWalkable = (i: number, j: number) =>
+ i >= 0 && i < COLS - 1 && j >= 0 && j < ROWS &&
+ !isBuildingTile(i, j) &&
+ !TREES.some((t) => t.i === i && t.j === j);
+
+// ---------- 고양이 이동 상태 (렌더 전용, 저장 안 함) ----------
+interface CatAnim {
+ x: number;
+ y: number;
+ tx: number;
+ ty: number;
+ mode: "wander" | "toWork" | "working";
+ waitUntil: number;
+ flip: boolean;
+}
+interface Bubble {
+ x: number;
+ y: number;
+ born: number;
+ text: string;
+}
+
+const WALK_SPEED = 42; // px/초
+
+function randomWalkTarget(): { x: number; y: number } {
+ for (let tries = 0; tries < 30; tries++) {
+ const i = Math.floor(Math.random() * (COLS - 1));
+ const j = Math.floor(Math.random() * ROWS);
+ if (isWalkable(i, j)) return { x: isoX(i, j), y: isoY(i, j) + TH / 2 };
+ }
+ return { x: isoX(5, 5), y: isoY(5, 5) + TH / 2 };
+}
+
+function workSpot(buildingId: string, slotIndex: number) {
+ const tile = BUILDING_TILES[buildingId] ?? BUILDING_TILES.hall;
+ // 건물 이름 라벨(정남쪽)을 피해서 좌우·아래대각에 배치
+ const angle = (slotIndex * 2 * Math.PI) / 3 + Math.PI / 6;
+ return {
+ x: isoX(tile.i, tile.j) + Math.cos(angle) * 42,
+ y: isoY(tile.i, tile.j) + TH / 2 + Math.sin(angle) * 16 + 8,
+ };
+}
+
+export default function VillageView() {
+ const canvasRef = useRef(null);
+ const animsRef = useRef