건설 시간 시스템: 배치 후 공사장 표시 → 시간 경과 시 완공

- BuildDef.buildTime 추가 (부엌 60초, 채석장 120초), 상점에 ⏱ 표시
- 배치 확정 시 constructionUntil 타임스탬프 설정 (오프라인에도 진행)
- 건설 중: 공사장 일러스트 + 🔨 카운트다운 판자 배지 + "건설 중" 라벨,
  생산·배치 불가, 회관 증축 요건에도 미인정
- 패널: 공사장 이미지 + 진행 바 + 남은 시간 표시
- 완공 시 tick에서 자동 전환

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
이 커밋은 다음에 포함됨:
kjs
2026-08-13 16:08:28 +09:00
co-authored by Claude Fable 5
부모 1f858352a0
커밋 c50e12400b
7개의 변경된 파일과 77개의 추가작업 그리고 5개의 파일을 삭제
+29
파일 보기
@@ -9,6 +9,7 @@ import {
effectiveRate,
activeRecipe,
fullnessPenalty,
isUnderConstruction,
} from "@/lib/game/engine";
import {
APTITUDE_LABEL,
@@ -408,6 +409,7 @@ function ShopPanel({
<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.buildTime}초`}
{def.reqHallLv > 1 && ` · 회관 Lv${def.reqHallLv}`}
</div>
<div className="mt-0.5 line-clamp-2 text-[11px] text-[#8a7551]">{def.desc}</div>
@@ -470,6 +472,33 @@ function BuildingPanel({
effectiveRate(b, building.aptitude) - effectiveRate(a, building.aptitude),
);
if (isUnderConstruction(building)) {
const def = BUILD_DEFS.find((d) => d.type === building.type);
const total = (def?.buildTime ?? 60) * 1000;
const remainMs = Math.max(0, (building.constructionUntil ?? 0) - Date.now());
const pct = Math.min(100, ((total - remainMs) / total) * 100);
const remainSec = Math.ceil(remainMs / 1000);
return (
<div className="space-y-4">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={BUILDING_IMG.construction}
alt="건설 중"
className="mx-auto h-28 object-contain"
/>
<p className="text-center text-sm text-[#6f5c39]">
🔨 열심히 짓는 중이에요…
</p>
<div className="bar-track h-3 w-full">
<div className="bar-fill-amber" style={{ width: `${pct}%` }} />
</div>
<p className="font-num text-center text-sm text-[#8a6316]">
남은 시간 {Math.floor(remainSec / 60)}:{String(remainSec % 60).padStart(2, "0")}
</p>
</div>
);
}
return (
<div className="space-y-5">
{/* 정보 */}