From c50c266f2ef55f48a5e00b5ec73656dddd66a8ee Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 13 Aug 2026 14:23:45 +0900 Subject: [PATCH] =?UTF-8?q?UI=20=EA=B0=9C=ED=8E=B8:=20=EC=A0=84=EC=B2=B4?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EB=A7=88=EC=9D=84=20+=20=EA=B1=B4?= =?UTF-8?q?=EB=AC=BC=20=ED=81=B4=EB=A6=AD=20=ED=8C=A8=EB=84=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 마을 캔버스가 뷰포트 전체를 채움 (ResizeObserver 스케일링) - 상단 HUD: 회관·자원 칩 + 고양이 목록·입양 버튼 (반투명 오버레이) - 건물 클릭 → 우측 플로팅 패널: 정보(적성·슬롯·속도·진행)·회수· 배치 가능한 고양이 목록(예상 속도순, 현재 일터 표시)에서 배치/해제 - 회관 클릭 → 증축 요건 체크리스트 패널, 부엌 미건설 시 공터(🚧) 클릭 → 건설 - 고양이 전체 목록은 서랍 패널로 이동, 기존 그리드 섹션 제거 - 배지 클릭 빠른 회수는 유지, 메시지는 토스트로 표시 Co-Authored-By: Claude Fable 5 --- src/app/page.tsx | 23 +- src/components/game/Game.tsx | 861 +++++++++++++++++----------- src/components/game/VillageView.tsx | 105 +++- 3 files changed, 618 insertions(+), 371 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index a2484ff..01417fd 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,26 +1,5 @@ import Game from "@/components/game/Game"; -import DbStatus from "@/components/DbStatus"; export default function Home() { - return ( -
-
-
- 🏡 고양이 마을 - 방치형 경영 게임 — 1단계 -
-
- -
- -
- -
-
- jskim.vexplor.com · dev-pilots - -
-
-
- ); + return ; } diff --git a/src/components/game/Game.tsx b/src/components/game/Game.tsx index 856d9f2..9c3e76b 100644 --- a/src/components/game/Game.tsx +++ b/src/components/game/Game.tsx @@ -1,9 +1,15 @@ "use client"; -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { useGame } from "@/hooks/useGame"; import VillageView from "@/components/game/VillageView"; -import { engine, effectiveRate, activeRecipe, fullnessPenalty } from "@/lib/game/engine"; +import DbStatus from "@/components/DbStatus"; +import { + engine, + effectiveRate, + activeRecipe, + fullnessPenalty, +} from "@/lib/game/engine"; import { APTITUDE_LABEL, BREEDS, @@ -14,36 +20,158 @@ import { PASSIVES, TOOL_IMG, } from "@/lib/game/data"; -import type { AptitudeKey } from "@/lib/game/types"; -import type { Building, Cat, GameState } from "@/lib/game/types"; +import type { AptitudeKey, Building, Cat, GameState } from "@/lib/game/types"; + +type Panel = + | { type: "building"; id: string } + | { type: "hall" } + | { type: "kitchen_lot" } + | { type: "cats" } + | null; export default function Game() { const { state, dispatch } = useGame(); - const [selectedCatId, setSelectedCatId] = useState(null); - const [message, setMessage] = useState(null); + const [panel, setPanel] = useState(null); const [candidates, setCandidates] = useState(null); - - if (!state) { - return ( -
마을 불러오는 중…
- ); - } - - const selectedCat = state.cats.find((c) => c.id === selectedCatId) ?? null; + const [message, setMessage] = useState(null); + const msgTimer = useRef | null>(null); const run = (action: () => string | null | void) => { const error = dispatch(action); setMessage(error ?? null); + if (msgTimer.current) clearTimeout(msgTimer.current); + if (error) msgTimer.current = setTimeout(() => setMessage(null), 3500); }; + useEffect(() => () => { + if (msgTimer.current) clearTimeout(msgTimer.current); + }, []); + + if (!state) { + return ( +
+ 마을 불러오는 중… +
+ ); + } + + const meals = state.buildings + .filter((b) => b.type === "kitchen") + .reduce((sum, b) => sum + Math.floor(b.stored), 0); + const openedBuilding = + panel?.type === "building" + ? state.buildings.find((b) => b.id === panel.id) + : undefined; + return ( -
- setCandidates(engine.generateCandidates())} - onUpgrade={() => run(() => engine.upgradeHall())} +
+ { + if (id === "hall") setPanel({ type: "hall" }); + else if (id === "kitchen_lot") setPanel({ type: "kitchen_lot" }); + else setPanel({ type: "building", id }); + }} /> + {/* ── 상단 HUD ── */} +
+
+ + + 🐱 {state.cats.length}/{state.hall.capacity} + + + 🐟 {Math.floor(state.resources.fish)} + + + 🪵 {Math.floor(state.resources.wood)} + + + 🍖 {meals} + +
+
+ + +
+
+ + {/* ── 메시지 토스트 ── */} + {message && ( +
+ ⚠️ {message} +
+ )} + + {/* ── DB 상태 (좌하단 미니) ── */} +
+ +
+ + {/* ── 우측 패널 ── */} + {panel?.type === "hall" && ( + setPanel(null)}> + run(() => engine.upgradeHall())} /> + + )} + {panel?.type === "kitchen_lot" && ( + setPanel(null)}> + { + run(() => engine.buildKitchen()); + setPanel({ type: "building", id: "kitchen_1" }); + }} + /> + + )} + {openedBuilding && ( + setPanel(null)} + > + run(() => engine.assign(catId, openedBuilding.id))} + onUnassign={(catId) => run(() => engine.unassign(catId))} + onCollect={() => run(() => engine.collect(openedBuilding.id))} + /> + + )} + {panel?.type === "cats" && ( + setPanel(null)}> +
+ {state.cats.map((cat) => ( + + ))} +
+
+ )} + + {/* ── 입양 모달 ── */} {candidates && ( setCandidates(null)} /> )} +
+ ); +} - +/* ──────────────── 공통 패널 셸 ──────────────── */ - {message && ( -

- ⚠️ {message} -

+function SidePanel({ + title, + icon, + onClose, + children, +}: { + title: string; + icon?: string; + onClose: () => void; + children: React.ReactNode; +}) { + return ( + + ); +} + +/* ──────────────── 회관 패널 ──────────────── */ + +function HallPanel({ state, onUpgrade }: { state: GameState; onUpgrade: () => void }) { + const next = HALL_LEVELS[state.hall.level + 1]; + return ( +
+

+ 마을의 중심. 회관 레벨이 수용 인원과 건물·레시피 해금을 결정합니다. +

+
+ 현재 수용 인원 {state.cats.length}/{state.hall.capacity}마리 +
+ {next ? ( +
+

+ Lv{state.hall.level + 1} 증축 → 수용 {next.capacity}마리 +

+
    + {Object.entries(next.cost).map(([key, amount]) => { + const have = Math.floor( + state.resources[key as keyof typeof state.resources], + ); + const ok = have >= (amount ?? 0); + return ( +
  • + {ok ? "✓" : "·"} {key === "wood" ? "🪵 목재" : "🐟 생선"} {have}/{amount} +
  • + ); + })} + {next.reqCat && ( +
  • c.level >= next.reqCat!.level).length >= + next.reqCat.count + ? "text-emerald-600 dark:text-emerald-400" + : "" + } + > + {state.cats.filter((c) => c.level >= next.reqCat!.level).length >= + next.reqCat.count + ? "✓" + : "·"}{" "} + Lv{next.reqCat.level} 이상 고양이{" "} + {state.cats.filter((c) => c.level >= next.reqCat!.level).length}/ + {next.reqCat.count}마리 +
  • + )} +
+ {next.locked ? ( +

+ 🔒 {next.locked} +

+ ) : ( + + )} +
+ ) : ( +

회관이 최고 레벨입니다.

+ )} +
+ ); +} + +/* ──────────────── 부엌 공터 패널 ──────────────── */ + +function KitchenLotPanel({ wood, onBuild }: { wood: number; onBuild: () => void }) { + const canBuild = wood >= KITCHEN_BUILD_COST.wood; + return ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + 부엌 +

+ 부엌을 지으면 요리 적성 고양이가 생선으로 요리를 만들고, 배고픈 + 고양이들이 알아서 먹으러 옵니다. +

+ +
+ ); +} + +/* ──────────────── 건물 패널 (정보 + 배치) ──────────────── */ + +function BuildingPanel({ + building, + state, + onAssign, + onUnassign, + onCollect, +}: { + building: Building; + state: GameState; + onAssign: (catId: string) => void; + onUnassign: (catId: string) => void; + onCollect: () => void; +}) { + const isKitchen = building.type === "kitchen"; + const recipe = activeRecipe(state); + const assignedCats = building.assigned + .map((id) => state.cats.find((c) => c.id === id)) + .filter((c): c is Cat => Boolean(c)); + const totalRate = assignedCats.reduce( + (sum, c) => sum + effectiveRate(c, building.aptitude), + 0, + ); + const stored = Math.floor(building.stored); + const slotsLeft = building.maxSlots - assignedCats.length; + // 배치 후보: 이 건물 적성이 있는 모든 고양이 (다른 곳에서 일하는 고양이 포함) + const candidates = state.cats + .filter((c) => c.aptitude[building.aptitude] > 0 && c.assignedTo !== building.id) + .sort( + (a, b) => + effectiveRate(b, building.aptitude) - effectiveRate(a, building.aptitude), + ); + + return ( +
+ {/* 정보 */} +
+
+ 필요 적성: {APTITUDE_LABEL[building.aptitude]} · 슬롯{" "} + {assignedCats.length}/{building.maxSlots} +
+ {isKitchen ? ( + <> +
+ {recipe.emoji} {recipe.name} ( + {Object.entries(recipe.input) + .map(([k, v]) => `${k === "fish" ? "생선" : "목재"} ${v}`) + .join(" + ")} + → 포만도 +{recipe.fullness}) +
+
요리 속도 {totalRate.toFixed(2)}/초
+
+
+
+ + ) : ( +
생산 속도 {totalRate.toFixed(2)}/초
+ )} +
+ + {/* 산출/선반 */} + {isKitchen ? ( +
+ {recipe.emoji} 선반 {stored}/{KITCHEN_MEAL_CAP} — 배고픈 고양이가 자동으로 먹습니다 +
+ ) : ( + )} + {/* 배치된 고양이 */}
-

🏗️ 생산 건물

-

- 고양이를 선택한 뒤 빈자리를 클릭해 배치하세요. 산출물은 직접 회수해야 합니다. -

-
- {state.buildings.map((b) => ( - run(() => engine.assign(catId, b.id))} - onUnassign={(catId) => run(() => engine.unassign(catId))} - onCollect={() => run(() => engine.collect(b.id))} +

+ 일하는 중 ({assignedCats.length}/{building.maxSlots}) +

+ {assignedCats.length === 0 && ( +

아직 아무도 없어요

+ )} +
+ {assignedCats.map((cat) => ( + onUnassign(cat.id) }} /> ))} - {!state.buildings.some((b) => b.type === "kitchen") && ( - run(() => engine.buildKitchen())} - /> - )}
+ {/* 배치 가능한 고양이 */}
-

- 🐾 우리 고양이들 ({state.cats.length}/{state.hall.capacity}) -

-
- {state.cats.map((cat) => ( - - setSelectedCatId(cat.id === selectedCatId ? null : cat.id) - } - /> - ))} +

+ 배치 가능한 고양이 +

+ {candidates.length === 0 && ( +

+ {APTITUDE_LABEL[building.aptitude]} 적성이 있는 고양이가 없어요 +

+ )} +
+ {candidates.map((cat) => { + const workplace = state.buildings.find((b) => b.id === cat.assignedTo); + return ( + onAssign(cat.id), + }} + /> + ); + })}
+ {slotsLeft <= 0 && candidates.length > 0 && ( +

+ 슬롯이 가득 찼어요 — 먼저 해제하거나 건물을 증축하세요 +

+ )}
); } -/** 패시브 스킬 배지 — 마우스오버 시 상세 툴팁 */ +/** 패널용 고양이 행 */ +function CatRow({ + cat, + aptitude, + tag, + action, +}: { + cat: Cat; + aptitude: AptitudeKey; + tag: string | null; + action: { + label: string; + style: "primary" | "danger"; + disabled?: boolean; + onClick: () => void; + }; +}) { + return ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + +
+
+ {cat.name}{" "} + Lv{cat.level} +
+
+ {APTITUDE_LABEL[aptitude]} {cat.aptitude[aptitude]} ·{" "} + {effectiveRate(cat, aptitude).toFixed(2)}/초 + {fullnessPenalty(cat) < 1 && ( + 배고픔 + )} +
+ {tag &&
{tag}
} +
+ +
+ ); +} + +/* ──────────────── 패시브 배지 (툴팁) ──────────────── */ + function PassiveBadge({ passiveKey }: { passiveKey: string }) { const def = PASSIVES.find((p) => p.key === passiveKey); if (!def) return null; @@ -137,7 +535,82 @@ function PassiveBadge({ passiveKey }: { passiveKey: string }) { ); } -/** 입양 모달 — 후보 3마리 중 1마리 선택 */ +/* ──────────────── 고양이 카드 (정보용) ──────────────── */ + +function CatCard({ cat, buildings }: { cat: Cat; buildings: Building[] }) { + const workplace = buildings.find((b) => b.id === cat.assignedTo); + const expNeeded = cat.level * 60; + + return ( +
+
+ + {/* eslint-disable-next-line @next/next/no-img-element */} + + {cat.name}{" "} + Lv{cat.level} + + {cat.breedName} +
+ +
+
+
+ +

+ 🎣 낚시 {cat.aptitude.fishing} · 🪓 벌목 {cat.aptitude.logging} · 🍳 요리{" "} + {cat.aptitude.cooking} · 💪 근면 {cat.stats.diligence} +

+ +
+
+
+
+ + 🍽️ {Math.floor(cat.status.fullness)} (식욕 {cat.stats.appetite}) + + {fullnessPenalty(cat) < 1 && ( + + 배고픔 + + )} +
+ + {cat.passives.length > 0 && ( +
+ {cat.passives.map((key) => ( + + ))} +
+ )} + +

+ {workplace ? ( + + {workplace.emoji} {workplace.name}에서 일하는 중 + + ) : ( + 쉬는 중 + )} +

+
+ ); +} + +/* ──────────────── 입양 모달 ──────────────── */ + function AdoptModal({ candidates, onPick, @@ -231,273 +704,9 @@ function AdoptModal({ })}

- 한 마리만 선택할 수 있어요 · 능력치와 패시브에 마우스를 올리면 상세 정보가 보입니다 + 한 마리만 선택할 수 있어요 · 패시브에 마우스를 올리면 상세 정보가 보입니다

); } - -function TopBar({ - state, - onAdopt, - onUpgrade, -}: { - state: GameState; - onAdopt: () => void; - onUpgrade: () => void; -}) { - const full = state.cats.length >= state.hall.capacity; - const meals = state.buildings - .filter((b) => b.type === "kitchen") - .reduce((sum, b) => sum + Math.floor(b.stored), 0); - const next = HALL_LEVELS[state.hall.level + 1]; - const nextLabel = next - ? `증축 Lv${state.hall.level + 1}: 목재 ${next.cost.wood ?? 0}${next.cost.fish ? `, 생선 ${next.cost.fish}` : ""}${next.reqCat ? `, Lv${next.reqCat.level}↑ ${next.reqCat.count}마리` : ""} → 수용 ${next.capacity}마리` - : "최고 레벨"; - - return ( -
-
- 🏛️ 마을 회관 Lv{state.hall.level} - 🐱 {state.cats.length}/{state.hall.capacity}마리 - 🐟 {Math.floor(state.resources.fish)} - 🪵 {Math.floor(state.resources.wood)} - 🍖 {meals} -
- - -
-
-

{nextLabel}

-
- ); -} - -function BuildKitchenCard({ wood, onBuild }: { wood: number; onBuild: () => void }) { - const canBuild = wood >= KITCHEN_BUILD_COST.wood; - return ( -
-
- {/* eslint-disable-next-line @next/next/no-img-element */} - 부엌 - 부엌 (미건설) -
-

- 요리 적성 고양이를 배치해 생선으로 요리를 만듭니다. 배고픈 고양이가 알아서 먹으러 와요. -

- -
- ); -} - -function BuildingCard({ - building, - state, - selectedCat, - onAssign, - onUnassign, - onCollect, -}: { - building: Building; - state: GameState; - selectedCat: Cat | null; - onAssign: (catId: string) => void; - onUnassign: (catId: string) => void; - onCollect: () => void; -}) { - const cats = state.cats; - const isKitchen = building.type === "kitchen"; - const recipe = activeRecipe(state); - const assignedCats = building.assigned - .map((id) => cats.find((c) => c.id === id)) - .filter((c): c is Cat => Boolean(c)); - const totalRate = assignedCats.reduce( - (sum, c) => sum + effectiveRate(c, building.aptitude), - 0, - ); - const stored = Math.floor(building.stored); - const emptySlots = building.maxSlots - assignedCats.length; - const canAssignSelected = - selectedCat !== null && - selectedCat.assignedTo !== building.id && - selectedCat.aptitude[building.aptitude] > 0; - - return ( -
-
-

- {/* eslint-disable-next-line @next/next/no-img-element */} - {building.name} - {building.name} Lv{building.level} -

- - 필요 적성: {APTITUDE_LABEL[building.aptitude]} - -
- {isKitchen ? ( -

- {recipe.emoji} {recipe.name} ( - {Object.entries(recipe.input) - .map(([k, v]) => `${k === "fish" ? "생선" : "목재"} ${v}`) - .join(" + ")} - ) · 요리 속도 {totalRate.toFixed(2)}/초 -

- ) : ( -

- 생산 속도 {totalRate.toFixed(2)}/초 -

- )} - -
- {assignedCats.map((cat) => ( - - ))} - {Array.from({ length: emptySlots }).map((_, i) => ( - - ))} -
- - {isKitchen ? ( -
- {recipe.emoji} 선반 {stored}/{KITCHEN_MEAL_CAP} — 배고픈 고양이가 자동으로 먹습니다 -
- ) : ( - - )} -
- ); -} - -function CatCard({ - cat, - buildings, - selected, - onSelect, -}: { - cat: Cat; - buildings: Building[]; - selected: boolean; - onSelect: () => void; -}) { - const workplace = buildings.find((b) => b.id === cat.assignedTo); - const expNeeded = cat.level * 60; - - return ( - - ); -} diff --git a/src/components/game/VillageView.tsx b/src/components/game/VillageView.tsx index 3239aa2..e33e752 100644 --- a/src/components/game/VillageView.tsx +++ b/src/components/game/VillageView.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; import { engine, catRate } from "@/lib/game/engine"; import { catSprite, TOOL_IMG, type CatFrame } from "@/lib/game/data"; import type { AptitudeKey } from "@/lib/game/types"; @@ -86,8 +86,14 @@ function workSpot(buildingId: string, slotIndex: number) { }; } -export default function VillageView() { +export default function VillageView({ + onBuildingClick, +}: { + onBuildingClick?: (id: string) => void; +}) { const canvasRef = useRef(null); + const wrapRef = useRef(null); + const [scale, setScale] = useState(1); const animsRef = useRef>(new Map()); const bubblesRef = useRef([]); const toolImagesRef = useRef>(new Map()); @@ -96,8 +102,24 @@ export default function VillageView() { // src 키 기반 이미지 캐시 (품종별 스프라이트 지연 로드) const imageCacheRef = useRef>(new Map()); const hitRectsRef = useRef< - Array<{ id: string; x: number; y: number; w: number; h: number }> + Array<{ id: string; kind: "collect" | "open"; x: number; y: number; w: number; h: number }> >([]); + const onBuildingClickRef = useRef(onBuildingClick); + onBuildingClickRef.current = onBuildingClick; + + // 전체화면 스케일 (컨테이너 크기에 맞춤) + useEffect(() => { + const el = wrapRef.current; + if (!el) return; + const update = () => + setScale( + Math.min(el.clientWidth / CANVAS_W, el.clientHeight / CANVAS_H) * 0.98, + ); + update(); + const ro = new ResizeObserver(update); + ro.observe(el); + return () => ro.disconnect(); + }, []); useEffect(() => { const canvas = canvasRef.current; @@ -368,11 +390,14 @@ export default function VillageView() { sprites.push({ y: y - 6, draw: () => drawEmoji(d.emoji, x, y + 4, 18) }); } + hitRectsRef.current = []; + // 회관 { const t = BUILDING_TILES.hall; const x = isoX(t.i, t.j); const y = isoY(t.i, t.j) + TH / 2; + hitRectsRef.current.push({ id: "hall", kind: "open", x: x - 34, y: y - 40, w: 68, h: 70 }); sprites.push({ y, draw: () => { @@ -386,8 +411,25 @@ export default function VillageView() { }); } + // 부엌 미건설 시 공터 표시 + if (!state.buildings.some((b) => b.type === "kitchen")) { + const t = BUILDING_TILES.kitchen_1; + const x = isoX(t.i, t.j); + const y = isoY(t.i, t.j) + TH / 2; + hitRectsRef.current.push({ id: "kitchen_lot", kind: "open", x: x - 30, y: y - 34, w: 60, h: 56 }); + sprites.push({ + y, + draw: () => { + drawEmoji("🚧", x, y + 2, 26); + ctx.font = "11px sans-serif"; + ctx.textAlign = "center"; + ctx.fillStyle = "rgba(107,114,128,0.9)"; + ctx.fillText("부엌 공터", x, y + 16); + }, + }); + } + // 생산 건물 + 회수 배지 - hitRectsRef.current = []; for (const b of state.buildings) { const t = BUILDING_TILES[b.id]; if (!t) continue; @@ -395,8 +437,11 @@ export default function VillageView() { const y = isoY(t.i, t.j) + TH / 2; const stored = Math.floor(b.stored); const isKitchen = b.type === "kitchen"; - if (!isKitchen) - hitRectsRef.current.push({ id: b.id, x: x - 30, y: y - 44, w: 60, h: 72 }); + // 건물 본체 → 정보 패널 + hitRectsRef.current.push({ id: b.id, kind: "open", x: x - 30, y: y - 38, w: 60, h: 66 }); + // 배지 → 빠른 회수 (생산 건물만) + if (!isKitchen && stored > 0) + hitRectsRef.current.push({ id: b.id, kind: "collect", x: x - 40, y: y - 62, w: 80, h: 26 }); sprites.push({ y, draw: () => { @@ -512,19 +557,29 @@ export default function VillageView() { const rect = canvas.getBoundingClientRect(); const x = ((e.clientX - rect.left) / rect.width) * CANVAS_W; const y = ((e.clientY - rect.top) / rect.height) * CANVAS_H; + const hit = (r: { x: number; y: number; w: number; h: number }) => + x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h; + + // 1) 배지(빠른 회수) 우선 for (const r of hitRectsRef.current) { - if (x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h) { - const state = engine.peek(); - const b = state?.buildings.find((bb) => bb.id === r.id); - const amount = b ? Math.floor(b.stored) : 0; - if (b && amount > 0) { - engine.collect(r.id); - engine.save(); - bubblesRef.current.push({ - x: r.x + r.w / 2, y: r.y, born: performance.now(), - text: `+${amount} ${b.resource === "fish" ? "🐟" : "🪵"}`, - }); - } + if (r.kind !== "collect" || !hit(r)) continue; + const s = engine.peek(); + const b = s?.buildings.find((bb) => bb.id === r.id); + const amount = b ? Math.floor(b.stored) : 0; + if (b && b.resource && amount > 0) { + engine.collect(r.id); + engine.save(); + bubblesRef.current.push({ + x: r.x + r.w / 2, y: r.y, born: performance.now(), + text: `+${amount} ${b.resource === "fish" ? "🐟" : "🪵"}`, + }); + } + return; + } + // 2) 건물 본체 → 정보 패널 열기 + for (const r of hitRectsRef.current) { + if (r.kind === "open" && hit(r)) { + onBuildingClickRef.current?.(r.id); return; } } @@ -538,14 +593,18 @@ export default function VillageView() { }, []); return ( -
+
-

- 건물 위 주황 배지를 클릭하면 산출물을 회수합니다 -

); }