자연 친화 UI 전면 개편 (디자인 문서 기반)
- 폰트: Jua(제목·버튼)·Gowun Dodum(본문)·JetBrains Mono(숫자) next/font 적용 - 팔레트·공통 클래스: 우드/크림/잎사귀 토큰, wood-plank·wood-pill·nature-btn· amber-btn·panel-wood/paper·card-nature·chip-nature·bar-* (globals.css) - HUD: 어두운 나무 바 + 매달린 판자 타이틀(로프) + 우드 자원 pill + 초록 입양 버튼 - 패널: 나무 프레임(나뭇결 텍스처) + 크림 종이 이중 구조 + 잎사귀 장식 - 카드: 비대칭 잎모양 모서리 + 초록 액센트 바, 연두 적성 칩, 초록/앰버 진행 바 - 입양 모달·상점·토스트·배치 힌트 모두 동일 스타일 - 캔버스: 건물 자원 배지를 나뭇결 판자+초록 글로우로, 라벨은 Jua+크림 외곽선 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
이 커밋은 다음에 포함됨:
+162
-143
@@ -57,7 +57,7 @@ export default function Game() {
|
||||
|
||||
if (!state) {
|
||||
return (
|
||||
<div className="fixed inset-0 flex items-center justify-center bg-emerald-50 text-gray-400 dark:bg-gray-950">
|
||||
<div className="fixed inset-0 flex items-center justify-center bg-[#f6eeda] font-jua text-lg text-[#8a7551]">
|
||||
마을 불러오는 중…
|
||||
</div>
|
||||
);
|
||||
@@ -91,76 +91,85 @@ export default function Game() {
|
||||
|
||||
{/* ── 배치 모드 힌트 ── */}
|
||||
{placing && (
|
||||
<div className="absolute bottom-6 left-1/2 z-40 flex -translate-x-1/2 items-center gap-3 rounded-full bg-gray-900/90 px-5 py-2.5 text-sm font-semibold text-white shadow-lg">
|
||||
<div className="wood-plank font-jua absolute bottom-6 left-1/2 z-40 flex -translate-x-1/2 items-center gap-3 px-5 py-2.5 text-sm">
|
||||
📍 {placing.moveId ? "옮길" : "배치할"} 위치를 클릭하세요
|
||||
{placing.type === "dock" && " (부두는 물가에만)"}
|
||||
<button
|
||||
onClick={() => setPlacing(null)}
|
||||
className="rounded-full bg-white/20 px-3 py-1 text-xs hover:bg-white/30"
|
||||
className="wood-pill px-3 py-1 text-xs hover:brightness-125"
|
||||
>
|
||||
취소 (ESC)
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── 상단 HUD ── */}
|
||||
<div className="pointer-events-none absolute inset-x-0 top-0 z-30 flex flex-wrap items-start justify-between gap-2 p-3">
|
||||
<div className="pointer-events-auto flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
onClick={() => setPanel({ type: "hall" })}
|
||||
className="rounded-full bg-white/85 px-4 py-2 text-sm font-bold shadow backdrop-blur hover:bg-white dark:bg-gray-900/85 dark:hover:bg-gray-900"
|
||||
>
|
||||
🏡 고양이 마을 · 회관 Lv{state.hall.level}
|
||||
</button>
|
||||
<span className="rounded-full bg-white/85 px-3 py-2 text-sm shadow backdrop-blur dark:bg-gray-900/85">
|
||||
🐱 {state.cats.length}/{state.hall.capacity}
|
||||
</span>
|
||||
<span className="rounded-full bg-white/85 px-3 py-2 text-sm shadow backdrop-blur dark:bg-gray-900/85">
|
||||
🐟 {Math.floor(state.resources.fish)}
|
||||
</span>
|
||||
<span className="rounded-full bg-white/85 px-3 py-2 text-sm shadow backdrop-blur dark:bg-gray-900/85">
|
||||
🪵 {Math.floor(state.resources.wood)}
|
||||
</span>
|
||||
<span className="rounded-full bg-white/85 px-3 py-2 text-sm shadow backdrop-blur dark:bg-gray-900/85">
|
||||
🪨 {Math.floor(state.resources.stone)}
|
||||
</span>
|
||||
<span
|
||||
title="부엌에 보관 중인 요리"
|
||||
className="rounded-full bg-white/85 px-3 py-2 text-sm shadow backdrop-blur dark:bg-gray-900/85"
|
||||
>
|
||||
🍖 {meals}
|
||||
</span>
|
||||
</div>
|
||||
<div className="pointer-events-auto flex gap-2">
|
||||
<button
|
||||
onClick={() =>
|
||||
setPanel(panel?.type === "shop" ? null : { type: "shop" })
|
||||
}
|
||||
className="rounded-full bg-white/85 px-4 py-2 text-sm font-semibold shadow backdrop-blur hover:bg-white dark:bg-gray-900/85 dark:hover:bg-gray-900"
|
||||
>
|
||||
🛒 상점
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setPanel(panel?.type === "cats" ? null : { type: "cats" })
|
||||
}
|
||||
className="rounded-full bg-white/85 px-4 py-2 text-sm font-semibold shadow backdrop-blur hover:bg-white dark:bg-gray-900/85 dark:hover:bg-gray-900"
|
||||
>
|
||||
🐾 고양이
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCandidates(engine.generateCandidates())}
|
||||
disabled={state.cats.length >= state.hall.capacity}
|
||||
className="rounded-full bg-orange-500 px-4 py-2 text-sm font-semibold text-white shadow hover:bg-orange-600 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
😻 입양
|
||||
</button>
|
||||
{/* ── 상단 HUD (어두운 나무 바) ── */}
|
||||
<div className="nature-topbar pointer-events-none absolute inset-x-0 top-0 z-30 h-[52px]">
|
||||
<div className="flex h-full items-start justify-between gap-2 px-4">
|
||||
{/* 매달린 나무 판자 타이틀 */}
|
||||
<div className="pointer-events-auto relative mt-0 flex items-start gap-3.5">
|
||||
<button
|
||||
onClick={() => setPanel({ type: "hall" })}
|
||||
className="wood-plank relative mt-1 flex items-center gap-2 px-4 py-2"
|
||||
>
|
||||
<span className="absolute -top-1 left-4 h-4 w-[3px] -rotate-12 bg-linear-to-b from-[#c9b184] to-[#8c7550]" />
|
||||
<span className="absolute -top-1 right-4 h-4 w-[3px] rotate-12 bg-linear-to-b from-[#c9b184] to-[#8c7550]" />
|
||||
<span className="font-jua text-[17px] tracking-wide [text-shadow:0_1px_2px_rgba(0,0,0,.45)]">
|
||||
🏡 고양이 마을 · 회관 Lv{state.hall.level}
|
||||
</span>
|
||||
</button>
|
||||
{/* 자원 pill */}
|
||||
<div className="mt-2 flex flex-wrap items-center gap-2">
|
||||
{[
|
||||
["🐱", `${state.cats.length}/${state.hall.capacity}`],
|
||||
["🐟", Math.floor(state.resources.fish)],
|
||||
["🪵", Math.floor(state.resources.wood)],
|
||||
["🪨", Math.floor(state.resources.stone)],
|
||||
["🍖", meals],
|
||||
].map(([icon, value]) => (
|
||||
<span
|
||||
key={icon as string}
|
||||
className="wood-pill flex items-center gap-1.5 py-1 pl-1 pr-3"
|
||||
>
|
||||
<span className="wood-pill-icon grid h-6 w-6 place-items-center text-[13px]">
|
||||
{icon}
|
||||
</span>
|
||||
<span className="font-num text-sm leading-none">{value}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="pointer-events-auto mt-2 flex gap-2">
|
||||
<button
|
||||
onClick={() =>
|
||||
setPanel(panel?.type === "shop" ? null : { type: "shop" })
|
||||
}
|
||||
className="wood-pill font-jua flex items-center gap-1.5 px-4 py-2 text-[15px] hover:brightness-125"
|
||||
>
|
||||
🛒 상점
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setPanel(panel?.type === "cats" ? null : { type: "cats" })
|
||||
}
|
||||
className="wood-pill font-jua flex items-center gap-1.5 px-4 py-2 text-[15px] hover:brightness-125"
|
||||
>
|
||||
🐾 고양이
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCandidates(engine.generateCandidates())}
|
||||
disabled={state.cats.length >= state.hall.capacity}
|
||||
className="nature-btn flex items-center gap-1.5 px-4 py-2 text-[15px] disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
😻 입양
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── 메시지 토스트 ── */}
|
||||
{message && (
|
||||
<div className="absolute left-1/2 top-16 z-50 -translate-x-1/2 rounded-full bg-amber-500/95 px-5 py-2 text-sm font-semibold text-white shadow-lg">
|
||||
<div className="wood-plank font-jua absolute left-1/2 top-16 z-50 -translate-x-1/2 px-5 py-2 text-sm">
|
||||
⚠️ {message}
|
||||
</div>
|
||||
)}
|
||||
@@ -255,23 +264,31 @@ function SidePanel({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<aside className="absolute bottom-3 right-3 top-16 z-40 flex w-full max-w-sm flex-col rounded-2xl border border-gray-200 bg-white/95 shadow-2xl backdrop-blur dark:border-gray-800 dark:bg-gray-900/95">
|
||||
<div className="flex items-center justify-between border-b border-gray-100 px-5 py-4 dark:border-gray-800">
|
||||
<h2 className="flex items-center gap-2 text-lg font-bold">
|
||||
{icon && (
|
||||
/* eslint-disable-next-line @next/next/no-img-element */
|
||||
<img src={icon} alt="" className="h-8 w-8 object-contain" />
|
||||
)}
|
||||
{title}
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="rounded-lg px-3 py-1 text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
<aside className="panel-wood absolute bottom-3 right-3 top-15 z-40 w-full max-w-sm p-2.5">
|
||||
{/* 잎사귀 장식 */}
|
||||
<span className="leaf left-2 top-14 h-6 w-6 rotate-[-28deg]" />
|
||||
<span className="leaf leaf-bright left-1 top-20 h-4 w-4 rotate-24" />
|
||||
<span className="leaf right-2 top-52 h-5 w-5 rotate-140" />
|
||||
<span className="leaf leaf-bright bottom-2 left-24 h-5 w-5 rotate-[-140deg]" />
|
||||
<span className="leaf leaf-bright bottom-1.5 right-32 h-4 w-4 rotate-[-200deg]" />
|
||||
<div className="panel-paper relative flex h-full flex-col overflow-hidden">
|
||||
<div className="flex items-center justify-between border-b border-[rgba(126,95,48,.18)] px-5 py-3.5">
|
||||
<h2 className="font-jua flex items-center gap-2 text-[19px] text-[#4a3418]">
|
||||
{icon && (
|
||||
/* eslint-disable-next-line @next/next/no-img-element */
|
||||
<img src={icon} alt="" className="h-8 w-8 object-contain" />
|
||||
)}
|
||||
{title}
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="grid h-8 w-8 place-items-center rounded-full bg-linear-to-b from-[#e8d6ae] to-[#cdb488] text-sm text-[#6b4a20] shadow hover:brightness-105"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto px-4 py-4">{children}</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto px-5 py-4">{children}</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -282,25 +299,25 @@ function HallPanel({ state, onUpgrade }: { state: GameState; onUpgrade: () => vo
|
||||
const next = HALL_LEVELS[state.hall.level + 1];
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
<p className="text-sm text-[#6f5c39]">
|
||||
마을의 중심. 회관 레벨이 수용 인원과 건물·레시피 해금을 결정합니다.
|
||||
</p>
|
||||
<div className="rounded-lg bg-gray-50 px-4 py-3 text-sm dark:bg-gray-800">
|
||||
<div className="card-nature px-4 py-3 text-sm">
|
||||
현재 수용 인원 <b>{state.cats.length}/{state.hall.capacity}마리</b>
|
||||
</div>
|
||||
{next ? (
|
||||
<div className="rounded-xl border border-gray-200 p-4 dark:border-gray-700">
|
||||
<h3 className="mb-2 font-semibold">
|
||||
<div className="card-nature p-4">
|
||||
<h3 className="font-jua mb-2 text-[16px] text-[#4a3418]">
|
||||
Lv{state.hall.level + 1} 증축 → 수용 {next.capacity}마리
|
||||
</h3>
|
||||
<ul className="mb-3 space-y-1 text-sm text-gray-600 dark:text-gray-300">
|
||||
<ul className="mb-3 space-y-1 text-sm text-[#6f5c39]">
|
||||
{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 (
|
||||
<li key={key} className={ok ? "text-emerald-600 dark:text-emerald-400" : ""}>
|
||||
<li key={key} className={ok ? "text-[#5c8a2a]" : ""}>
|
||||
{ok ? "✓" : "·"} {RES_EMOJI[key]} {RES_LABEL[key]} {have}/{amount}
|
||||
</li>
|
||||
);
|
||||
@@ -309,7 +326,7 @@ function HallPanel({ state, onUpgrade }: { state: GameState; onUpgrade: () => vo
|
||||
const b = state.buildings.find((x) => x.type === req.type);
|
||||
const ok = Boolean(b && b.level >= (req.level ?? 1));
|
||||
return (
|
||||
<li key={req.label} className={ok ? "text-emerald-600 dark:text-emerald-400" : ""}>
|
||||
<li key={req.label} className={ok ? "text-[#5c8a2a]" : ""}>
|
||||
{ok ? "✓" : "·"} {req.label}
|
||||
</li>
|
||||
);
|
||||
@@ -334,13 +351,13 @@ function HallPanel({ state, onUpgrade }: { state: GameState; onUpgrade: () => vo
|
||||
)}
|
||||
</ul>
|
||||
{next.locked ? (
|
||||
<p className="rounded-lg bg-gray-100 px-3 py-2 text-xs text-gray-500 dark:bg-gray-800 dark:text-gray-400">
|
||||
<p className="chip-nature px-3 py-2 text-xs">
|
||||
🔒 {next.locked}
|
||||
</p>
|
||||
) : (
|
||||
<button
|
||||
onClick={onUpgrade}
|
||||
className="w-full rounded-lg bg-emerald-600 py-2 text-sm font-semibold text-white hover:bg-emerald-700"
|
||||
className="nature-btn w-full py-2 text-sm"
|
||||
>
|
||||
🏗️ 증축하기
|
||||
</button>
|
||||
@@ -364,7 +381,7 @@ function ShopPanel({
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<p className="text-xs text-gray-400">
|
||||
<p className="text-xs text-[#8a7551]">
|
||||
구매 후 마을의 원하는 타일을 클릭해 배치합니다. 비용은 배치 확정 시 차감돼요.
|
||||
</p>
|
||||
{BUILD_DEFS.map((def) => {
|
||||
@@ -378,7 +395,7 @@ function ShopPanel({
|
||||
return (
|
||||
<div
|
||||
key={def.type}
|
||||
className="flex items-center gap-3 rounded-xl border border-gray-200 p-3 dark:border-gray-700"
|
||||
className="card-nature flex items-center gap-3 p-3"
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
@@ -387,22 +404,22 @@ function ShopPanel({
|
||||
className={`h-12 w-12 shrink-0 object-contain ${built || !hallOk ? "opacity-40" : ""}`}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-semibold">{def.name}</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<div className="font-jua text-[15px] text-[#3f2c14]">{def.name}</div>
|
||||
<div className="font-num text-xs text-[#8a6316]">
|
||||
{costEntries.map(([k, v]) => `${RES_EMOJI[k]} ${v}`).join(" + ")}
|
||||
{def.reqHallLv > 1 && ` · 회관 Lv${def.reqHallLv}`}
|
||||
</div>
|
||||
<div className="mt-0.5 line-clamp-2 text-[11px] text-gray-400">{def.desc}</div>
|
||||
<div className="mt-0.5 line-clamp-2 text-[11px] text-[#8a7551]">{def.desc}</div>
|
||||
</div>
|
||||
{built ? (
|
||||
<span className="shrink-0 rounded-lg bg-gray-100 px-3 py-1.5 text-xs text-gray-400 dark:bg-gray-800">
|
||||
<span className="chip-nature shrink-0 px-3 py-1.5 text-xs">
|
||||
보유 중
|
||||
</span>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => onBuy(def)}
|
||||
disabled={!hallOk || !affordable}
|
||||
className="shrink-0 rounded-lg bg-amber-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-amber-700 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
className="amber-btn shrink-0 px-3.5 py-1.5 text-xs disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
{!hallOk ? `회관 Lv${def.reqHallLv}` : !affordable ? "자원 부족" : "구매"}
|
||||
</button>
|
||||
@@ -455,7 +472,7 @@ function BuildingPanel({
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{/* 정보 */}
|
||||
<div className="rounded-lg bg-gray-50 px-4 py-3 text-sm leading-relaxed dark:bg-gray-800">
|
||||
<div className="card-nature px-4 py-3 text-sm leading-relaxed">
|
||||
<div className="flex items-center justify-between">
|
||||
<span>
|
||||
필요 적성: <b>{APTITUDE_LABEL[building.aptitude]}</b> · 슬롯{" "}
|
||||
@@ -464,7 +481,7 @@ function BuildingPanel({
|
||||
<button
|
||||
onClick={onMove}
|
||||
title="건물 위치 옮기기"
|
||||
className="shrink-0 rounded-lg bg-gray-200 px-2.5 py-1 text-xs font-semibold text-gray-600 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
className="wood-plank shrink-0 px-2.5 py-1 text-xs hover:brightness-110"
|
||||
>
|
||||
📍 이동
|
||||
</button>
|
||||
@@ -479,9 +496,9 @@ function BuildingPanel({
|
||||
→ 포만도 +{recipe.fullness})
|
||||
</div>
|
||||
<div>요리 속도 {totalRate.toFixed(2)}/초</div>
|
||||
<div className="mt-1 h-1.5 w-full overflow-hidden rounded bg-gray-200 dark:bg-gray-700">
|
||||
<div className="bar-track mt-1 h-2 w-full">
|
||||
<div
|
||||
className="h-full bg-amber-400"
|
||||
className="bar-fill-amber"
|
||||
style={{
|
||||
width: `${Math.min(100, ((building.progress ?? 0) / recipe.work) * 100)}%`,
|
||||
}}
|
||||
@@ -496,18 +513,18 @@ function BuildingPanel({
|
||||
{/* 산출/선반 */}
|
||||
{isKitchen ? (
|
||||
<>
|
||||
<div className="rounded-lg bg-amber-50 px-4 py-2.5 text-center text-sm font-semibold text-amber-700 dark:bg-amber-950 dark:text-amber-300">
|
||||
<div className="card-nature px-4 py-2.5 text-center text-sm font-semibold text-[#8a6316]">
|
||||
{recipe.emoji} 선반 {stored}/{mealCap(building.level)} — 배고픈 고양이가 자동으로 먹습니다
|
||||
</div>
|
||||
{building.level < 2 && (
|
||||
<div className="rounded-xl border border-gray-200 p-3 text-sm dark:border-gray-700">
|
||||
<div className="mb-1 font-semibold">⬆️ 부엌 Lv2 증축</div>
|
||||
<p className="mb-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
<div className="card-nature p-3 text-sm">
|
||||
<div className="font-jua mb-1 text-[15px] text-[#3f2c14]">⬆️ 부엌 Lv2 증축</div>
|
||||
<p className="mb-2 text-xs text-[#6f5c39]">
|
||||
선반 15칸으로 확장 · 회관 Lv3 달성 시 🐟 훈제 생선 해금
|
||||
</p>
|
||||
<button
|
||||
onClick={onUpgradeKitchen}
|
||||
className="w-full rounded-lg bg-emerald-600 py-2 text-xs font-semibold text-white hover:bg-emerald-700"
|
||||
className="nature-btn w-full py-2 text-xs"
|
||||
>
|
||||
{Object.entries(KITCHEN_UPGRADE_COST)
|
||||
.map(([k, v]) => `${RES_EMOJI[k]} ${v}`)
|
||||
@@ -522,7 +539,7 @@ function BuildingPanel({
|
||||
(r) => r.reqHallLv > state.hall.level || r.reqKitchenLv > kitchenLv,
|
||||
);
|
||||
return lockedRecipe ? (
|
||||
<p className="text-xs text-gray-400">
|
||||
<p className="text-xs text-[#8a7551]">
|
||||
🔒 {lockedRecipe.emoji} {lockedRecipe.name} — 회관 Lv
|
||||
{lockedRecipe.reqHallLv} · 부엌 Lv{lockedRecipe.reqKitchenLv} 해금
|
||||
</p>
|
||||
@@ -533,7 +550,7 @@ function BuildingPanel({
|
||||
<button
|
||||
onClick={onCollect}
|
||||
disabled={stored <= 0}
|
||||
className="w-full rounded-lg bg-sky-500 py-2.5 text-sm font-semibold text-white hover:bg-sky-600 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
className="amber-btn w-full py-2.5 text-sm disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
{building.resource === "fish" ? "🐟" : "🪵"} {stored}개 회수
|
||||
</button>
|
||||
@@ -541,11 +558,11 @@ function BuildingPanel({
|
||||
|
||||
{/* 배치된 고양이 */}
|
||||
<section>
|
||||
<h3 className="mb-2 text-sm font-bold text-gray-500 dark:text-gray-400">
|
||||
<h3 className="font-jua mb-2 text-sm text-[#8a7551]">
|
||||
일하는 중 ({assignedCats.length}/{building.maxSlots})
|
||||
</h3>
|
||||
{assignedCats.length === 0 && (
|
||||
<p className="text-xs text-gray-400">아직 아무도 없어요</p>
|
||||
<p className="text-xs text-[#8a7551]">아직 아무도 없어요</p>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
{assignedCats.map((cat) => (
|
||||
@@ -562,11 +579,11 @@ function BuildingPanel({
|
||||
|
||||
{/* 배치 가능한 고양이 */}
|
||||
<section>
|
||||
<h3 className="mb-2 text-sm font-bold text-gray-500 dark:text-gray-400">
|
||||
<h3 className="font-jua mb-2 text-sm text-[#8a7551]">
|
||||
배치 가능한 고양이
|
||||
</h3>
|
||||
{candidates.length === 0 && (
|
||||
<p className="text-xs text-gray-400">
|
||||
<p className="text-xs text-[#8a7551]">
|
||||
{APTITUDE_LABEL[building.aptitude]} 적성이 있는 고양이가 없어요
|
||||
</p>
|
||||
)}
|
||||
@@ -590,7 +607,7 @@ function BuildingPanel({
|
||||
})}
|
||||
</div>
|
||||
{slotsLeft <= 0 && candidates.length > 0 && (
|
||||
<p className="mt-2 text-xs text-amber-600 dark:text-amber-400">
|
||||
<p className="mt-2 text-xs text-[#8a6316]">
|
||||
슬롯이 가득 찼어요 — 먼저 해제하거나 건물을 증축하세요
|
||||
</p>
|
||||
)}
|
||||
@@ -617,7 +634,8 @@ function CatRow({
|
||||
};
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center gap-3 rounded-xl border border-gray-200 px-3 py-2 dark:border-gray-700">
|
||||
<div className="card-nature flex items-center gap-3 px-3 py-2">
|
||||
<span className="card-accent" />
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={catSprite(cat.breed, "idle")}
|
||||
@@ -625,26 +643,26 @@ function CatRow({
|
||||
className="h-10 w-10 shrink-0 object-contain"
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-semibold">
|
||||
<div className="font-jua truncate text-sm text-[#3f2c14]">
|
||||
{cat.name}{" "}
|
||||
<span className="text-xs font-normal text-gray-400">Lv{cat.level}</span>
|
||||
<span className="font-num text-xs text-[#8a7551]">Lv{cat.level}</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<div className="text-xs text-[#6f5c39]">
|
||||
{APTITUDE_LABEL[aptitude]} {cat.aptitude[aptitude]} ·{" "}
|
||||
{effectiveRate(cat, aptitude).toFixed(2)}/초
|
||||
{fullnessPenalty(cat) < 1 && (
|
||||
<span className="ml-1 text-red-500">배고픔</span>
|
||||
<span className="ml-1 text-[#b0472a]">배고픔</span>
|
||||
)}
|
||||
</div>
|
||||
{tag && <div className="text-[11px] text-gray-400">{tag}</div>}
|
||||
{tag && <div className="text-[11px] text-[#a3906c]">{tag}</div>}
|
||||
</div>
|
||||
<button
|
||||
onClick={action.onClick}
|
||||
disabled={action.disabled}
|
||||
className={`shrink-0 rounded-lg px-3 py-1.5 text-xs font-semibold disabled:cursor-not-allowed disabled:opacity-40 ${
|
||||
className={`shrink-0 px-3 py-1.5 text-xs disabled:cursor-not-allowed disabled:opacity-40 ${
|
||||
action.style === "primary"
|
||||
? "bg-orange-500 text-white hover:bg-orange-600"
|
||||
: "bg-gray-100 text-gray-600 hover:bg-red-100 hover:text-red-600 dark:bg-gray-800 dark:text-gray-300"
|
||||
? "nature-btn"
|
||||
: "wood-plank hover:brightness-110"
|
||||
}`}
|
||||
>
|
||||
{action.label}
|
||||
@@ -696,9 +714,10 @@ function CatCard({
|
||||
const cap = BREEDS.find((b) => b.key === cat.breed)?.aptitudeCap ?? 5;
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-gray-200 p-4 dark:border-gray-700">
|
||||
<div className="card-nature p-4">
|
||||
<span className="card-accent" />
|
||||
<div className="mb-1 flex items-center justify-between">
|
||||
<span className="flex items-center gap-1.5 font-semibold">
|
||||
<span className="font-jua flex items-center gap-1.5 text-[16px] text-[#3f2c14]">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={catSprite(cat.breed, "idle")}
|
||||
@@ -706,14 +725,14 @@ function CatCard({
|
||||
className="h-7 w-7 object-contain"
|
||||
/>
|
||||
{cat.name}{" "}
|
||||
<span className="text-xs font-normal text-gray-400">Lv{cat.level}</span>
|
||||
<span className="font-num text-xs text-[#8a7551]">Lv{cat.level}</span>
|
||||
</span>
|
||||
<span className="text-xs text-gray-400">{cat.breedName}</span>
|
||||
<span className="text-xs text-[#a3906c]">{cat.breedName}</span>
|
||||
</div>
|
||||
|
||||
<div className="mb-2 h-1.5 w-full overflow-hidden rounded bg-gray-100 dark:bg-gray-800">
|
||||
<div className="bar-track mb-2 h-1.5 w-full">
|
||||
<div
|
||||
className="h-full bg-orange-400"
|
||||
className="bar-fill-amber"
|
||||
style={{ width: `${Math.min(100, (cat.exp / expNeeded) * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
@@ -724,14 +743,14 @@ function CatCard({
|
||||
.map((k) => (
|
||||
<span
|
||||
key={k}
|
||||
className="flex items-center gap-0.5 rounded bg-sky-50 px-1.5 py-0.5 text-[11px] text-sky-700 dark:bg-sky-950 dark:text-sky-300"
|
||||
className="chip-nature flex items-center gap-0.5 px-1.5 py-0.5 text-[11px]"
|
||||
>
|
||||
{APTITUDE_LABEL[k]} {cat.aptitude[k]}
|
||||
{cat.aptitudePoints > 0 && cat.aptitude[k] < cap && (
|
||||
<button
|
||||
onClick={() => onInvest(cat.id, k)}
|
||||
title="적성 포인트 투자"
|
||||
className="ml-0.5 rounded bg-sky-600 px-1 text-[10px] font-bold text-white hover:bg-sky-700"
|
||||
className="ml-0.5 rounded bg-[#5d8f22] px-1 text-[10px] font-bold text-white hover:bg-[#4f8420]"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
@@ -739,29 +758,27 @@ function CatCard({
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
<p className="mt-1 text-xs text-[#6f5c39]">
|
||||
💪 근면 {cat.stats.diligence}
|
||||
{cat.aptitudePoints > 0 && (
|
||||
<span className="ml-2 rounded bg-orange-100 px-1.5 py-0.5 text-[11px] font-semibold text-orange-600 dark:bg-orange-950 dark:text-orange-300">
|
||||
<span className="ml-2 rounded-full bg-[rgba(214,164,60,.2)] px-2 py-0.5 text-[11px] font-semibold text-[#8a6316]">
|
||||
✨ 적성 포인트 {cat.aptitudePoints}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<div className="h-1.5 flex-1 overflow-hidden rounded bg-gray-100 dark:bg-gray-800">
|
||||
<div className="bar-track h-2 flex-1">
|
||||
<div
|
||||
className={`h-full ${
|
||||
cat.status.fullness < 25 ? "bg-red-400" : "bg-emerald-400"
|
||||
}`}
|
||||
className={cat.status.fullness < 25 ? "bar-fill-red" : "bar-fill-green"}
|
||||
style={{ width: `${cat.status.fullness}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-[11px] text-gray-400">
|
||||
<span className="font-num text-[11px] text-[#8a7a58]">
|
||||
🍽️ {Math.floor(cat.status.fullness)} (식욕 {cat.stats.appetite})
|
||||
</span>
|
||||
{fullnessPenalty(cat) < 1 && (
|
||||
<span className="rounded bg-red-50 px-1.5 py-0.5 text-[11px] text-red-600 dark:bg-red-950 dark:text-red-300">
|
||||
<span className="rounded-full bg-[rgba(214,106,74,.16)] px-2 py-0.5 text-[11px] text-[#b0472a]">
|
||||
배고픔
|
||||
</span>
|
||||
)}
|
||||
@@ -777,11 +794,11 @@ function CatCard({
|
||||
|
||||
<p className="mt-2 text-xs">
|
||||
{workplace ? (
|
||||
<span className="text-sky-600 dark:text-sky-400">
|
||||
<span className="text-[#5c8a2a]">
|
||||
{workplace.emoji} {workplace.name}에서 일하는 중
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-gray-400">쉬는 중</span>
|
||||
<span className="text-[#a3906c]">쉬는 중</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -805,14 +822,15 @@ function AdoptModal({
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className="max-h-[90vh] w-full max-w-3xl overflow-y-auto rounded-2xl bg-white p-6 shadow-2xl dark:bg-gray-900"
|
||||
className="panel-wood max-h-[90vh] w-full max-w-3xl overflow-hidden p-2.5"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="panel-paper max-h-[calc(90vh-20px)] overflow-y-auto p-5">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2 className="text-xl font-bold">😻 입양할 고양이를 선택하세요</h2>
|
||||
<h2 className="font-jua text-xl text-[#4a3418]">😻 입양할 고양이를 선택하세요</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="rounded-lg px-3 py-1 text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
className="grid h-8 w-8 place-items-center rounded-full bg-linear-to-b from-[#e8d6ae] to-[#cdb488] text-sm text-[#6b4a20] shadow hover:brightness-105"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
@@ -826,7 +844,7 @@ function AdoptModal({
|
||||
return (
|
||||
<div
|
||||
key={cat.id}
|
||||
className="flex flex-col rounded-xl border border-gray-200 p-4 dark:border-gray-700"
|
||||
className="card-nature flex flex-col p-4"
|
||||
>
|
||||
<div className="mb-2 text-center">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
@@ -835,8 +853,8 @@ function AdoptModal({
|
||||
alt={cat.name}
|
||||
className="mx-auto h-20 w-auto object-contain drop-shadow-sm"
|
||||
/>
|
||||
<div className="mt-1 font-bold">{cat.name}</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
<div className="font-jua mt-1 text-[17px] text-[#3f2c14]">{cat.name}</div>
|
||||
<div className="text-xs text-[#a3906c]">
|
||||
{cat.breedName}{" "}
|
||||
<span className="text-amber-400">
|
||||
{"★".repeat(breed?.rarity ?? 1)}
|
||||
@@ -844,7 +862,7 @@ function AdoptModal({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-2 rounded-lg bg-gray-50 px-3 py-2 text-xs leading-relaxed dark:bg-gray-800">
|
||||
<div className="chip-nature mb-2 px-3 py-2 text-xs leading-relaxed">
|
||||
💪 근면 {cat.stats.diligence} · ❤️ 체력 {cat.stats.stamina} · 🍽️
|
||||
식욕 {cat.stats.appetite}
|
||||
</div>
|
||||
@@ -854,13 +872,13 @@ function AdoptModal({
|
||||
aptitudes.map((k) => (
|
||||
<span
|
||||
key={k}
|
||||
className="rounded bg-sky-50 px-1.5 py-0.5 text-[11px] text-sky-700 dark:bg-sky-950 dark:text-sky-300"
|
||||
className="chip-nature px-1.5 py-0.5 text-[11px]"
|
||||
>
|
||||
{APTITUDE_LABEL[k]} {cat.aptitude[k]}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className="text-[11px] text-gray-400">적성 없음…</span>
|
||||
<span className="text-[11px] text-[#a3906c]">적성 없음…</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -868,13 +886,13 @@ function AdoptModal({
|
||||
{cat.passives.length > 0 ? (
|
||||
cat.passives.map((key) => <PassiveBadge key={key} passiveKey={key} />)
|
||||
) : (
|
||||
<span className="text-[11px] text-gray-400">패시브 없음</span>
|
||||
<span className="text-[11px] text-[#a3906c]">패시브 없음</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => onPick(cat)}
|
||||
className="mt-auto rounded-lg bg-orange-500 py-2 text-sm font-semibold text-white hover:bg-orange-600"
|
||||
className="nature-btn mt-auto py-2 text-sm"
|
||||
>
|
||||
이 아이로 할래요
|
||||
</button>
|
||||
@@ -882,9 +900,10 @@ function AdoptModal({
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="mt-4 text-center text-xs text-gray-400">
|
||||
<p className="mt-4 text-center text-xs text-[#8a7551]">
|
||||
한 마리만 선택할 수 있어요 · 패시브에 마우스를 올리면 상세 정보가 보입니다
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
새 이슈에서 참조
사용자 차단