# Solusi Lengkap 3D Digital Twin Next.js (oohmediabandung.com)
**Senior Full-Stack Next.js & WebGL Solution Architecture**

---

## 1. Analisis Akar Masalah (Root-Cause Analysis)

Kegagalan render (blank screen / layar putih / build crash) pada komponen 3D Digital Twin di server produksi Next.js umumnya disebabkan oleh tiga faktor teknis:

1. **Konflik Server-Side Rendering (SSR):**
   Library WebGL seperti **Three.js** langsung menginisialisasi modul atau mengakses objek `window`, `document`, `navigator`, `HTMLCanvasElement`, dan `WebGLRenderingContext`. Saat proses `next build` atau saat server Node.js me-render HTML awal untuk klien, objek browser tersebut belum ada di lingkungan server (`ReferenceError: window is not defined` atau `WebGLRenderer failed to create`).

2. **Server Components vs Client Components (Next.js 13/14/15 App Router):**
   Secara default, file di dalam folder `app/` adalah **React Server Components (RSC)**. Menggunakan hook seperti `useState`, `useEffect`, `useRef`, atau instansiasi canvas di RSC tanpa deklarasi `'use client';` dan tanpa `dynamic(() => import(...), { ssr: false })` menyebabkan fatal hydration mismatch dan kegagalan render di sisi klien.

3. **Resolusi Jalur File Aset 3D (.glb / .gltf) di Production:**
   - Folder `/public` di Next.js dipetakan ke root path `/`. Menulis path seperti `"/public/models/spot.glb"` akan menghasilkan HTTP 404.
   - Ketika server produksi mengembalikan halaman fallback HTML 404 (`<!DOCTYPE html>...`) alih-alih file binary 3D, parser Three.js (`GLTFLoader`) mengalami error `SyntaxError: Unexpected token '<' in JSON at position 0` dan kanvas menjadi blank hitam atau tidak ter-render.
   - Penggunaan `basePath` atau CDN reverse proxy tanpa fungsi resolusi path membuat URL aset terputus.

---

## 2. Struktur Folder Standar Next.js di Produksi

```
oohmediabandung.com/
├── app/                              # Next.js App Router (atau pages/ jika Pages Router)
│   ├── titik-reklame/
│   │   └── [id]/
│   │       └── page.tsx              # Halaman Server Component
│   ├── layout.tsx
│   └── page.tsx
├── components/
│   ├── 3d/
│   │   ├── DigitalTwin3DViewer.tsx   # Core Three.js Engine (dengan 'use client')
│   │   ├── DigitalTwinClientWrapper.tsx # Dynamic Import { ssr: false }
│   │   └── ErrorBoundary3D.tsx       # WebGL Crash Recovery Guard
├── public/                           # Aset Statis
│   ├── models/                       # File 3D Binary
│   │   ├── billboard-pasteur.glb
│   │   ├── videotron-dago.glb
│   │   └── textures/
│   └── icons/
├── next.config.js                    # Konfigurasi Webpack Fallback
└── package.json
```

---

## 3. Kode Komponen 1: `components/3d/DigitalTwinClientWrapper.tsx`
Gunakan file ini sebagai jembatan *dynamic import* dengan `{ ssr: false }`:

```tsx
'use client';

import dynamic from 'next/dynamic';
import React from 'react';

// Mematikan SSR secara ketat untuk modul Three.js
const DigitalTwin3DViewer = dynamic(
  () => import('./DigitalTwin3DViewer'),
  {
    ssr: false,
    loading: () => (
      <div className="w-full h-96 min-h-[420px] rounded-2xl bg-slate-950 border border-cyan-500/30 flex flex-col items-center justify-center p-6 text-center">
        <div className="w-12 h-12 rounded-xl bg-cyan-500/10 border border-cyan-400/40 flex items-center justify-center mb-3 animate-pulse">
          <div className="w-6 h-6 border-2 border-cyan-400 border-t-transparent rounded-full animate-spin" />
        </div>
        <p className="text-xs font-mono font-bold text-cyan-300">
          Menginisialisasi WebGL 3D Digital Twin Bandung...
        </p>
        <span className="text-[10px] text-slate-400 mt-1">
          oohmediabandung.com · Three.js Client Hydration
        </span>
      </div>
    ),
  }
);

export default function DigitalTwinClientWrapper(props: any) {
  return <DigitalTwin3DViewer {...props} />;
}
```

---

## 4. Contoh Pemanggilan di Next.js App Router (`app/titik-reklame/[id]/page.tsx`)

```tsx
import DigitalTwinClientWrapper from '@/components/3d/DigitalTwinClientWrapper';

interface PageProps {
  params: { id: string };
}

export default async function DetailTitikReklamePage({ params }: PageProps) {
  // Server-side data fetching (SEO-friendly)
  const spotData = {
    id: params.id || 'BDG-01',
    name: 'Jl. Pasteur (Near Toll Exit Cipularang)',
    location: 'Jl. Dr. Djunjunan No. 145, Sukajadi, Bandung',
    dimensions: '10 x 5 Meter',
    dailyOTS: 285400,
    modelUrl: '/models/billboard-pasteur.glb', // Path relatif terhadap folder /public
  };

  return (
    <main className="min-h-screen bg-slate-950 text-white p-6 md:p-10">
      <div className="max-w-6xl mx-auto">
        <h1 className="text-2xl font-bold mb-2">3D Digital Twin · {spotData.name}</h1>
        <p className="text-sm text-slate-400 mb-6">
          Visualisasi interaktif sudut pandang driver, drone aerial, dan pencahayaan real-time.
        </p>

        {/* Pemanggilan Komponen 3D yang aman dari error SSR */}
        <DigitalTwinClientWrapper
          id={spotData.id}
          name={spotData.name}
          location={spotData.location}
          dimensions={spotData.dimensions}
          dailyOTS={spotData.dailyOTS}
          modelUrl={spotData.modelUrl}
        />
      </div>
    </main>
  );
}
```

---

## 5. Konfigurasi `next.config.js` (Webpack & Fallback)

```javascript
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  webpack: (config, { isServer }) => {
    if (!isServer) {
      config.resolve.fallback = {
        ...config.resolve.fallback,
        fs: false,
        path: false,
        crypto: false,
      };
    }

    config.module.rules.push({
      test: /\.(glb|gltf)$/,
      type: 'asset/resource',
    });

    return config;
  },
};

module.exports = nextConfig;
```
