Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
.bkit
17 changes: 10 additions & 7 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"framer-motion": "^12.38.0",
"lucide-react": "^1.7.0",
"next": "16.2.2",
"react": "19.2.4",
"react-dom": "19.2.4",
Expand Down
4 changes: 2 additions & 2 deletions src/app/api/live/[slug]/places/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createStaticSceneBootstrap } from "../../../../../shared/scene";
import { MVP_PLACES } from "../../../../../data/places";
import { toLiveStateCacheKey } from "../../../../../shared/cache";
import { validateLivePlaceSnapshot } from "../../../../../shared/contracts";
import { normalizeLiveLevel } from "../../../../../shared/selectors";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";
Expand All @@ -26,8 +27,7 @@ export async function GET(
}

const bootstrap = createStaticSceneBootstrap(place);
const raw = request.nextUrl.searchParams.get("density") ?? "medium";
const density = raw === "low" || raw === "high" ? raw : "medium";
const density = normalizeLiveLevel(request.nextUrl.searchParams.get("density"));

const snapshot = validateLivePlaceSnapshot({
geometryId: bootstrap.geometryId,
Expand Down
9 changes: 5 additions & 4 deletions src/app/api/live/[slug]/traffic/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createStaticSceneBootstrap } from "../../../../../shared/scene";
import { MVP_PLACES } from "../../../../../data/places";
import { toLiveStateCacheKey } from "../../../../../shared/cache";
import { validateLiveTrafficSnapshot } from "../../../../../shared/contracts";
import { normalizeHour, toTrafficPolicy } from "../../../../../shared/domains";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";
Expand All @@ -27,14 +28,14 @@ export async function GET(

const bootstrap = createStaticSceneBootstrap(place);
const hour = Number(request.nextUrl.searchParams.get("hour") ?? "12");
const normalizedHour = Number.isFinite(hour) ? ((hour % 24) + 24) % 24 : 12;
const rushHour = (normalizedHour >= 8 && normalizedHour <= 10) || (normalizedHour >= 17 && normalizedHour <= 20);
const normalizedHour = normalizeHour(hour);
const trafficPolicy = toTrafficPolicy(normalizedHour);

const payload = validateLiveTrafficSnapshot({
geometryId: bootstrap.geometryId,
key: toLiveStateCacheKey(bootstrap.geometryId, "traffic"),
density: rushHour ? "high" : "medium",
speedKph: rushHour ? 18 : 32,
density: trafficPolicy.density,
speedKph: trafficPolicy.speedKph,
capturedAtIso: new Date().toISOString(),
});

Expand Down
15 changes: 5 additions & 10 deletions src/app/api/live/[slug]/weather/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createStaticSceneBootstrap } from "../../../../../shared/scene";
import { MVP_PLACES } from "../../../../../data/places";
import { toLiveStateCacheKey } from "../../../../../shared/cache";
import { validateLiveWeatherSnapshot } from "../../../../../shared/contracts";
import { buildWeatherPolicy, normalizeHour } from "../../../../../shared/domains";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";
Expand All @@ -27,20 +28,14 @@ export async function GET(

const bootstrap = createStaticSceneBootstrap(place);
const hour = Number(request.nextUrl.searchParams.get("hour") ?? "12");
const normalizedHour = Number.isFinite(hour) ? ((hour % 24) + 24) % 24 : 12;

const condition =
normalizedHour >= 6 && normalizedHour < 17
? "clear"
: normalizedHour >= 17 && normalizedHour < 21
? "cloudy"
: "rain";
const normalizedHour = normalizeHour(hour);
const weatherPolicy = buildWeatherPolicy(normalizedHour);

const snapshot = validateLiveWeatherSnapshot({
geometryId: bootstrap.geometryId,
key: toLiveStateCacheKey(bootstrap.geometryId, "weather"),
condition,
temperatureCelsius: condition === "rain" ? 10 : 18,
condition: weatherPolicy.condition,
temperatureCelsius: weatherPolicy.temperatureCelsius,
capturedAtIso: new Date().toISOString(),
});

Expand Down
29 changes: 27 additions & 2 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,32 @@
}

body {
background: var(--background);
background: #000;
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
font-family: var(--font-sans), system-ui, -apple-system, sans-serif;
overflow: hidden;
}

@layer components {
.glass-panel {
@apply rounded-2xl border border-white/15 bg-black/45 backdrop-blur-xl;
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
}

.glass-button {
@apply flex items-center justify-center rounded-xl border border-white/10 bg-white/5 transition-all hover:bg-white/15 active:scale-95 disabled:opacity-50;
}

.glass-button-active {
@apply border-cyan-400/50 bg-cyan-500/20 text-cyan-300 ring-1 ring-cyan-500/30;
}
}

/* Cesium UI Hiding */
.cesium-viewer-bottom {
display: none !important;
}
.cesium-viewer-toolbar {
top: 20px !important;
right: 20px !important;
}
76 changes: 69 additions & 7 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,80 @@
import GlobeScene from "../globe/GlobeScene";
import { MVP_PLACES } from "../data/places";
import { Globe, MapPin, Search } from "lucide-react";
import Link from "next/link";

export default function Home() {
return (
<main className="relative flex flex-1 bg-black">
<GlobeScene places={MVP_PLACES} />

<div className="pointer-events-none absolute left-4 top-4 z-10 rounded-xl border border-white/10 bg-black/55 px-4 py-3 text-white backdrop-blur-sm">
<p className="text-xs font-medium uppercase tracking-[0.22em] text-cyan-300">
WorMap · Globe MVP
</p>
<h1 className="mt-1 text-lg font-semibold">장소를 클릭해 진입하세요</h1>
<p className="mt-1 text-sm text-zinc-300">
마커 3개(Shibuya / Times Square / Gangnam) 준비됨
{/* Top Header */}
<div className="pointer-events-none absolute left-8 top-8 z-10 flex flex-col gap-4 animate-in fade-in slide-in-from-top-4 duration-1000">
<div className="glass-panel px-6 py-5 w-80">
<div className="flex items-center gap-2.5">
<div className="flex h-10 w-10 items-center justify-center rounded-2xl bg-cyan-500/20 text-cyan-400 ring-1 ring-cyan-500/30">
<Globe size={20} className="animate-spin-slow" />
</div>
<div>
<p className="text-[10px] font-bold uppercase tracking-[0.25em] text-cyan-300/70">WorMap Engine</p>
<h1 className="text-xl font-black text-white tracking-tighter">GLOBAL DISCOVERY</h1>
</div>
</div>

<div className="mt-6 flex items-center gap-3 glass-button h-10 px-4 text-xs font-bold text-zinc-400 pointer-events-auto cursor-text">
<Search size={14} />
<span>Search coordinates or places...</span>
</div>

<div className="mt-6 space-y-2">
<div className="flex items-center justify-between text-[11px] font-bold uppercase tracking-wider text-zinc-500">
<span>System Status</span>
<span className="text-green-500">Online</span>
</div>
<div className="h-1 w-full bg-white/5 rounded-full overflow-hidden">
<div className="h-full w-2/3 bg-cyan-500/50 rounded-full" />
</div>
</div>
</div>
</div>

{/* Bottom Place Selector */}
<div className="pointer-events-none absolute inset-x-0 bottom-12 z-10 flex justify-center px-8 animate-in fade-in slide-in-from-bottom-8 duration-1000 delay-300">
<div className="flex items-end gap-4">
{MVP_PLACES.map((place, idx) => (
<Link
key={place.id}
href={`/place/${place.slug}`}
className="pointer-events-auto group relative flex flex-col"
>
<div className="absolute -top-8 left-1/2 -translate-x-1/2 opacity-0 group-hover:opacity-100 transition-all duration-300">
<div className="glass-panel px-3 py-1 text-[10px] font-black text-cyan-300 uppercase tracking-widest whitespace-nowrap">
Travel to {place.name}
</div>
</div>

<div className="glass-panel w-48 overflow-hidden transition-all duration-500 group-hover:-translate-y-2 group-hover:ring-2 group-hover:ring-cyan-500/50">
<div className="aspect-[16/10] bg-zinc-900 overflow-hidden relative">
<div className="absolute inset-0 bg-gradient-to-t from-black/80 to-transparent z-10" />
<div className="absolute inset-0 flex items-center justify-center opacity-20 group-hover:scale-110 transition-transform duration-700">
<MapPin size={48} className="text-cyan-500" />
</div>
<div className="absolute bottom-3 left-4 z-20">
<p className="text-[10px] font-bold text-cyan-400 uppercase tracking-widest">{place.city}</p>
<h3 className="text-sm font-black text-white uppercase tracking-tight leading-none mt-0.5">{place.name}</h3>
</div>
</div>
</div>
</Link>
))}
</div>
</div>

{/* Info Badge */}
<div className="pointer-events-none absolute right-8 bottom-8 z-10 glass-panel px-4 py-2 flex items-center gap-3 animate-in fade-in duration-1000">
<div className="h-2 w-2 rounded-full bg-cyan-500 animate-pulse" />
<p className="text-[10px] font-bold text-zinc-400 uppercase tracking-widest">
Coordinates: <span className="text-white tabular-nums">37.5665° N, 126.9780° E</span>
</p>
</div>
</main>
Expand Down
Loading