Files
desa-darmasaba/QC/DESA/summary-qc-gallery-desa.md
nico b9b00f0a20 docs(qc): add quality control summaries for various modules
Added comprehensive QC reports and fix summaries for:
- Desa (Berita, Potensi, Profil, Layanan, Penghargaan, Pengumuman)
- Kesehatan (Posyandu)
- Landing Page (APBDes, SDGS, Anti-Korupsi, Profil, Prestasi)
- PPID (Daftar Informasi, Dasar Hukum, IKM, Permohonan, Struktur, Visi Misi)
2026-04-23 12:11:55 +08:00

28 KiB

Quality Control Report - Gallery Desa Admin

Lokasi: /src/app/admin/(dashboard)/desa/gallery/
Tanggal QC: 25 Februari 2026
Status: ⚠️ Needs Improvement (ada issue critical data loss risk)


📋 Ringkasan Eksekutif

Halaman Gallery Desa (Foto & Video) memiliki implementasi yang cukup baik dengan CRUD lengkap, upload gambar, YouTube embed, dan state management terstruktur. Namun ditemukan 18 issue dengan rincian:

  • 🔴 High Priority: 5 issue
  • 🟡 Medium Priority: 8 issue
  • 🟢 Low Priority: 5 issue

Overall Score: 6/10 - Needs Improvement


📁 Struktur File yang Diperiksa

/src/app/admin/(dashboard)/desa/gallery/
├── layout.tsx
├── lib/
│   ├── layoutTabs.tsx          # Tab navigation Foto/Video
│   ├── youtube-utils.ts        # YouTube URL conversion utilities
│   └── youtubeEmbed.tsx        # Reusable embed component (UNUSED)
├── foto/
│   ├── page.tsx                # List foto dengan search & pagination
│   ├── create/
│   │   └── page.tsx            # Upload foto dengan dropzone
│   └── [id]/
│       ├── page.tsx            # Detail foto
│       └── edit/
│           └── page.tsx        # Edit foto (replace image)
└── video/
    ├── page.tsx                # List video dengan search & pagination
    ├── create/
    │   └── page.tsx            # Add video YouTube dengan embed preview
    └── [id]/
        ├── page.tsx            # Detail video
        └── edit/
            └── page.tsx        # Edit video

File Terkait:

  • State: /src/app/admin/(dashboard)/_state/desa/gallery.ts
  • API: /src/app/api/[[...slugs]]/_lib/desa/gallery/foto/ (7 files)
  • API: /src/app/api/[[...slugs]]/_lib/desa/gallery/video/ (6 files)
  • Schema: /prisma/schema.prisma (Model GalleryFoto & GalleryVideo)

🔴 HIGH PRIORITY ISSUES

1. Schema - deletedAt @default(now()) (CRITICAL BUG)

File: prisma/schema.prisma

model GalleryFoto {
  id        String   @id @default(cuid())
  name      String
  deletedAt DateTime @default(now())  // ❌ CRITICAL BUG
  isActive  Boolean  @default(true)
}

model GalleryVideo {
  id        String   @id @default(cuid())
  name      String
  deletedAt DateTime @default(now())  // ❌ CRITICAL BUG
  isActive  Boolean  @default(true)
}

Dampak:

  • Setiap record baru langsung ter-mark sebagai deleted saat dibuat
  • Query dengan filter deletedAt: null tidak akan dapat data baru
  • Soft delete logic tidak bekerja sama sekali
  • Data inconsistency antara deletedAt (set) dan isActive (true)

Severity: 🔴 CRITICAL - Ini adalah bug yang sama seperti di Profil Desa dan Pengumuman

Solusi:

model GalleryFoto {
  id        String   @id @default(cuid())
  name      String
  deletedAt DateTime?  // ✅ Nullable, tanpa default
  isActive  Boolean  @default(true)
}

model GalleryVideo {
  id        String   @id @default(cuid())
  name      String
  deletedAt DateTime?  // ✅ Nullable, tanpa default
  isActive  Boolean  @default(true)
}

Migration Required:

bunx prisma migrate dev --name fix_deleted_at_default
# atau
bunx prisma db push

# Data cleanup
UPDATE "GalleryFoto" SET "deletedAt" = NULL WHERE "isActive" = true;
UPDATE "GalleryVideo" SET "deletedAt" = NULL WHERE "isActive" = true;

2. API - File Orphaning Saat Create Gagal

File: src/app/admin/(dashboard)/desa/gallery/foto/create/page.tsx

// Line 78-88
const res = await ApiFetch.api.fileStorage.create.post({
  file,
  name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
  return toast.error('Gagal mengunggah gambar, silakan coba lagi');
}
FotoState.create.form.imagesId = uploaded.id;
await FotoState.create.create();  // ❌ Jika ini gagal, file sudah ter-upload

Dampak:

  • File ter-upload ke server tapi gallery tidak terbuat
  • Orphaned files menumpuk di database dan filesystem
  • Storage waste, tidak ada cleanup mechanism

Severity: 🔴 HIGH - Data integrity issue

Solusi:

Option A - Transaction di API:

// Di API create.ts
try {
  // Validate fileStorage exists first
  const fileStorage = await prisma.fileStorage.findUnique({
    where: { id: body.imagesId }
  });
  
  if (!fileStorage) {
    return Response.json({
      success: false,
      message: "File tidak ditemukan"
    }, { status: 404 });
  }
  
  const gallery = await prisma.galleryFoto.create({
    data: {
      name: body.name,
      deskripsi: body.deskripsi,
      imagesId: body.imagesId,
    }
  });
  
  return { success: true, data: gallery };
} catch (error) {
  // Rollback file jika create gagal
  if (body.imagesId) {
    await prisma.fileStorage.delete({ where: { id: body.imagesId } }).catch(() => {});
  }
  throw error;
}

Option B - Cleanup di Frontend:

try {
  const uploaded = await uploadFile(file);
  const result = await createGallery({ ...imagesId: uploaded.id });
  
  if (!result.success) {
    // Cleanup orphaned file
    await deleteFile(uploaded.id);
    throw new Error('Create gallery failed');
  }
} catch (error) {
  toast.error('Gagal membuat gallery');
}

3. API - Old File Dihapus Sebelum Update Confirmed

File: src/app/api/[[...slugs]]/_lib/desa/gallery/foto/updt.ts

// Line 47-58
if (existing.imagesId && existing.imagesId !== body.imagesId) {
  const oldImage = existing.imageGalleryFoto;
  if (oldImage) {
    const filePath = path.join(oldImage.path, oldImage.name);
    await fs.unlink(filePath);  // ❌ File dihapus DULU
    await prisma.fileStorage.delete({
      where: { id: oldImage.id },
    });
  }
}

// Baru update data
const updated = await prisma.galleryFoto.update({
  where: { id },
  data: { ... }
});

Dampak:

  • Jika prisma.galleryFoto.update() gagal, old file sudah terhapus
  • DATA LOSS - Gallery tidak punya image sama sekali
  • Tidak ada rollback mechanism

Severity: 🔴 HIGH - Data loss risk

Solusi:

// Update data DULU, baru hapus old file
const updated = await prisma.galleryFoto.update({
  where: { id },
  data: {
    name: body.name,
    deskripsi: body.deskripsi,
    imagesId: body.imagesId,
  },
  include: { imageGalleryFoto: true }
});

// Hapus old file SETELAH update berhasil
if (existing.imagesId && existing.imagesId !== body.imagesId) {
  const oldImage = existing.imageGalleryFoto;
  if (oldImage) {
    try {
      const filePath = path.join(oldImage.path, oldImage.name);
      await fs.unlink(filePath);
      await prisma.fileStorage.delete({
        where: { id: oldImage.id },
      });
    } catch (error) {
      console.error('Failed to delete old file:', error);
      // Log error tapi tidak rollback karena update sudah berhasil
    }
  }
}

4. API - Tidak Ada Authentication/Authorization

File: Semua API endpoints di /src/app/api/[[...slugs]]/_lib/desa/gallery/

export default async function fotoCreate(context: Context) {
  // ❌ Tidak ada validasi session/user
  const body = await context.body;
  
  // Langsung proses create
  await prisma.galleryFoto.create({ ... });
}

Dampak:

  • Siapa saja bisa upload/delete foto/video jika tahu endpoint
  • Tidak ada audit trail siapa yang upload/delete
  • Security risk untuk production

Severity: 🔴 HIGH - Security vulnerability

Solusi:

import { getSession } from '@/lib/auth';

export default async function fotoCreate(context: Context) {
  const session = await getSession();
  
  if (!session || !session.user) {
    return Response.json({
      success: false,
      message: "Unauthorized"
    }, { status: 401 });
  }
  
  // Check role/permission jika perlu
  if (!session.user.menuIds?.includes('gallery')) {
    return Response.json({
      success: false,
      message: "Forbidden"
    }, { status: 403 });
  }
  
  const body = await context.body;
  // ... lanjut proses
}

5. API - Tidak Ada Input Validation

File: src/app/api/[[...slugs]]/_lib/desa/gallery/foto/create.ts

// Line 13-23
await prisma.galleryFoto.create({
  data: {
    name: body.name,        // ❌ Tidak ada validasi length
    deskripsi: body.deskripsi,  // ❌ Tidak ada sanitasi XSS
    imagesId: body.imagesId,    // ❌ Tidak cek apakah FileStorage ada
  },
});

Dampak:

  • User bisa input name sangat panjang (bisa break UI/database)
  • XSS attack via deskripsi field (rich text editor)
  • Bisa create gallery dengan imagesId yang tidak valid

Severity: 🔴 HIGH - Security & data integrity

Solusi:

// Validasi input
const { name, deskripsi, imagesId } = await context.body;

// Check length
if (!name || name.length > 255) {
  return Response.json({
    success: false,
    message: "Name maksimal 255 karakter"
  }, { status: 400 });
}

// Check duplikasi
const existing = await prisma.galleryFoto.findFirst({
  where: { name, isActive: true }
});

if (existing) {
  return Response.json({
    success: false,
    message: "Name sudah digunakan"
  }, { status: 400 });
}

// Check fileStorage exists
const fileStorage = await prisma.fileStorage.findUnique({
  where: { id: imagesId }
});

if (!fileStorage) {
  return Response.json({
    success: false,
    message: "File tidak ditemukan"
  }, { status: 404 });
}

// Sanitize HTML (gunakan library seperti DOMPurify di server)
const sanitizedDeskripsi = sanitizeHtml(deskripsi);

// Create
const gallery = await prisma.galleryFoto.create({
  data: {
    name,
    deskripsi: sanitizedDeskripsi,
    imagesId,
  }
});

🟡 MEDIUM PRIORITY ISSUES

6. UI - Dead Code (youtubeEmbed.tsx Tidak Digunakan)

File: src/app/admin/(dashboard)/desa/gallery/lib/youtubeEmbed.tsx

// Component ini TIDAK digunakan di mana pun
export function YoutubeEmbed({ url, ... }: Props) {
  // ... component code
}

Dampak:

  • Dead code menumpuk (120+ baris tidak digunakan)
  • Confusing untuk developer baru
  • Maintenance overhead

Severity: 🟡 MEDIUM - Code quality issue

Solusi:

  • Option A: Hapus file ini jika memang tidak diperlukan
  • Option B: Gunakan component ini di semua halaman (create, edit, detail) untuk konsistensi

Recommendation: Hapus, karena setiap halaman sudah implementasi iframe manual dengan cara berbeda.


7. UI - Inconsistent Styling Foto vs Video

File: foto/page.tsx vs video/page.tsx

// foto/page.tsx - Line 58
<Box p={{ base: 'md', md: 'lg' }}>  // ✅ Responsive padding

// video/page.tsx - Line 60
<Box py={20}>  // ❌ Hardcoded padding

Dampak: Inconsistent spacing antara foto dan video pages.

Severity: 🟡 MEDIUM - UX inconsistency

Solusi:

// video/page.tsx
<Box p={{ base: 'md', md: 'lg' }}>  // ✅ Konsisten dengan foto

8. UI - Memory Leak Potential (createObjectURL)

File: src/app/admin/(dashboard)/desa/gallery/foto/create/page.tsx

// Line 59-62
useEffect(() => {
  if (file) {
    const url = URL.createObjectURL(file);
    setPreviewImage(url);
  }
}, [file]);

// Line 47-52
const resetForm = () => {
  FotoState.create.form = { name: '', deskripsi: '', imagesId: '' };
  setPreviewImage(null);
  setFile(null);
  // ❌ URL.revokeObjectURL() tidak dipanggil
};

Dampak:

  • Memory leak jika user upload banyak gambar tanpa refresh
  • Browser bisa crash setelah banyak createObjectURL tidak di-cleanup

Severity: 🟡 MEDIUM - Performance issue

Solusi:

// Cleanup saat unmount atau file berubah
useEffect(() => {
  if (file) {
    const url = URL.createObjectURL(file);
    setPreviewImage(url);
    
    return () => {
      URL.revokeObjectURL(url);  // ✅ Cleanup
    };
  }
}, [file]);

// Cleanup saat reset
const resetForm = () => {
  if (previewImage) {
    URL.revokeObjectURL(previewImage);  // ✅ Cleanup
  }
  FotoState.create.form = { name: '', deskripsi: '', imagesId: '' };
  setPreviewImage(null);
  setFile(null);
};

9. State - Error Handling Tidak Konsisten

File: src/app/admin/(dashboard)/_state/desa/gallery.ts

// Line 39-53 (foto.create)
async create() {
  try {
    const res = await ApiFetch.api.desa.gallery.foto["create"].post(foto.create.form);
    if (res.status === 200) {
      foto.findMany.load();
      return toast.success("Foto berhasil disimpan!");
    }
    return toast.error("Gagal menyimpan foto");
  } catch (error) {
    console.log((error as Error).message);
    // ❌ Error di-catch tapi tidak ada toast error notification
  }
}

// Line 91-107 (foto.findUnique)
async load(id: string) {
  try {
    const res = await fetch(`/api/desa/gallery/foto/${id}`);
    if (res.ok) {
      const data = await res.json();
      foto.findUnique.data = data.data ?? null;
    }
  } catch (error) {
    console.error("Error fetching foto:", error);
    // ❌ Tidak ada error toast notification
  }
}

// Line 205-227 (foto.findRecent)
async load() {
  try {
    // ...
  } catch (error) {
    console.error("Gagal fetch foto recent:", error);
    // ❌ Tidak ada error toast notification
  }
}

Dampak:

  • User tidak tahu ada error (silent failure)
  • UX buruk (loading forever tanpa feedback)
  • Sulit debug production issues

Severity: 🟡 MEDIUM - UX issue

Solusi:

async create() {
  try {
    const res = await ApiFetch.api.desa.gallery.foto["create"].post(foto.create.form);
    if (res.status === 200) {
      foto.findMany.load();
      return toast.success("Foto berhasil disimpan!");
    }
    return toast.error("Gagal menyimpan foto");
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : 'Unknown error';
    console.error('Create foto failed:', errorMessage);
    toast.error(`Gagal menyimpan foto: ${errorMessage}`);  // ✅ Show error
  }
}

10. State - findMany.load() Dipanggil Tanpa Parameter

File: src/app/admin/(dashboard)/_state/desa/gallery.ts

// Line 47 (foto.create)
async create() {
  // ...
  if (res.status === 200) {
    foto.findMany.load();  // ❌ Tanpa parameter (default page=1, limit=10)
    return toast.success("Foto berhasil disimpan!");
  }
}

// Line 119 (foto.delete)
async byId(id: string) {
  // ...
  if (response.ok) {
    toast.success(result.message || "Foto berhasil dihapus");
    await foto.findMany.load();  // ❌ Tanpa parameter
  }
}

Dampak:

  • Jika user di page 5, setelah create/delete refresh ke page 1
  • User bingung kenapa data hilang (padahal masih ada, cuma page berubah)

Severity: 🟡 MEDIUM - UX issue

Solusi:

// Simpan current pagination state
let currentPage = 1;
let currentLimit = 10;
let currentSearch = '';

// Set parameter saat load
async load(page = 1, limit = 10, search = '') {
  currentPage = page;
  currentLimit = limit;
  currentSearch = search;
  // ... load data
}

// Gunakan current state saat refresh
async create() {
  // ...
  if (res.status === 200) {
    await foto.findMany.load(currentPage, currentLimit, currentSearch);  // ✅ Pass current params
    toast.success("Foto berhasil disimpan!");
  }
}

11. API - Video Search Tidak Include deskripsi

File: src/app/api/[[...slugs]]/_lib/desa/gallery/video/find-many.ts

// Line 18-21
if (search) {
  where.OR = [
    { name: { contains: search, mode: 'insensitive' } }
    // ❌ deskripsi tidak di-include
  ];
}

Bandingkan dengan Foto:

// foto/find-many.ts - Line 20-26
if (search) {
  where.OR = [
    { name: { contains: search, mode: 'insensitive' } },
    { deskripsi: { contains: search, mode: 'insensitive' } }  // ✅ Include deskripsi
  ];
}

Dampak:

  • User tidak bisa search video berdasarkan deskripsi
  • Inconsistent behavior antara foto dan video

Severity: 🟡 MEDIUM - Feature inconsistency

Solusi:

if (search) {
  where.OR = [
    { name: { contains: search, mode: 'insensitive' } },
    { deskripsi: { contains: search, mode: 'insensitive' } }  // ✅ Add deskripsi
  ];
}

12. UI - Skeleton Height Terlalu Besar

File: src/app/admin/(dashboard)/desa/gallery/video/page.tsx

// Line 73-77
if (loading || !data) {
  return (
    <Stack py={20}>
      <Skeleton height={600} radius="md" />  // ❌ Terlalu besar
    </Stack>
  );
}

Dampak: Skeleton mengambil hampir seluruh layar, UX buruk.

Severity: 🟡 MEDIUM - UX issue

Solusi:

<Skeleton height={200} radius="md" />  // ✅ Lebih reasonable

13. UI - Duplicate convertToEmbedUrl Function

File: src/app/admin/(dashboard)/desa/gallery/video/[id]/page.tsx

// Line 106-118
function convertToEmbedUrl(youtubeUrl: string): string {
  try {
    const url = new URL(youtubeUrl);
    const videoId = url.searchParams.get("v");
    if (!videoId) return youtubeUrl;
    return `https://www.youtube.com/embed/${videoId}`;
  } catch (err) {
    return youtubeUrl;
  }
}

Padahal sudah ada di: lib/youtube-utils.ts

export function convertYoutubeUrlToEmbed(url: string) {
  const videoIdMatch = url.match(
    /(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/
  );
  return videoIdMatch ? `https://www.youtube.com/embed/${videoIdMatch[1]}` : null;
}

Dampak:

  • Duplicate code (violation DRY principle)
  • Logic berbeda (page.tsx hanya support watch URL, utils.ts support multiple formats)
  • Maintenance overhead

Severity: 🟡 MEDIUM - Code quality issue

Solusi:

// Import dari utils
import { convertYoutubeUrlToEmbed } from '../../lib/youtube-utils';

// Gunakan function yang sudah ada
const embedLink = convertYoutubeUrlToEmbed(data.linkVideo);

14. Utils - YouTube Shorts URL Tidak Disupport

File: src/app/admin/(dashboard)/desa/gallery/lib/youtube-utils.ts

export function convertYoutubeUrlToEmbed(url: string) {
  const videoIdMatch = url.match(
    /(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/
  );
  // ❌ Regex tidak support youtube.com/shorts/VIDEO_ID
  return videoIdMatch ? `https://www.youtube.com/embed/${videoIdMatch[1]}` : null;
}

Dampak: User tidak bisa input YouTube Shorts URL (format populer).

Severity: 🟡 MEDIUM - Feature gap

Solusi:

export function convertYoutubeUrlToEmbed(url: string) {
  const videoIdMatch = url.match(
    /(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/
  );
  // ✅ Added shorts\/ support
  return videoIdMatch ? `https://www.youtube.com/embed/${videoIdMatch[1]}` : null;
}

🟢 LOW PRIORITY ISSUES

15. UI - Redundant Variable (filteredData)

File: src/app/admin/(dashboard)/desa/gallery/foto/page.tsx

// Line 78-79
const filteredData = data || [];
// ❌ Variable ini redundant, data sudah difilter di backend

Dampak: Minor code clutter.

Severity: 🟢 LOW - Code cleanliness

Solusi: Hapus variable, gunakan langsung data || [].


16. UI - useEffect Redundant di layoutTabs.tsx

File: src/app/admin/(dashboard)/desa/gallery/lib/layoutTabs.tsx

// Line 35-40
useEffect(() => {
  const match = tabs.find(tab => tab.href === pathname)
  if (match) {
    setActiveTab(match.value)
  }
}, [pathname])
// ❌ Redundant karena sudah ada logic serupa di handleTabChange

Dampak: Minor performance overhead.

Severity: 🟢 LOW - Code quality

Solusi: Hapus useEffect jika tidak diperlukan.


17. State - findRecent Tidak Digunakan di UI

File: src/app/admin/(dashboard)/_state/desa/gallery.ts

// Line 205-227
foto: {
  findRecent: {
    loading: false,
    data: [] as any[],
    async load() {
      // ... fetch recent photos
    }
  }
}

Dampak: Dead code di state management.

Severity: 🟢 LOW - Code cleanliness

Solusi:

  • Option A: Hapus jika memang tidak diperlukan
  • Option B: Implementasi di UI (misal: widget "Recent Photos" di dashboard)

18. State - Mix State Mutation dan Return Value

File: src/app/admin/(dashboard)/_state/desa/gallery.ts

// Line 138-203 (foto.update)
async load(id: string) {
  // ... fetch GET
  if (result?.success) {
    const data = result.data;
    this.id = data.id;
    this.form = { ... };
    return data;  // ❌ Mix mutation + return value (confusing API)
  }
}

Dampak: Confusing API, tidak jelas apakah caller harus gunakan return value atau akses state langsung.

Severity: 🟢 LOW - Code quality

Solusi:

// Option A: Hanya mutation (recommended)
async load(id: string) {
  // ... fetch GET
  if (result?.success) {
    const data = result.data;
    this.id = data.id;
    this.form = { ... };
    // No return value
  }
}

// Usage
await foto.update.load(id);
const formData = foto.update.form;  // Akses dari state

// Option B: Hanya return value
async load(id: string) {
  // ... fetch GET
  if (result?.success) {
    return result.data;
  }
  return null;
}

// Usage
const data = await foto.update.load(id);

YANG SUDAH BAIK

Schema:

  • Relasi GalleryFoto ke FileStorage sudah benar
  • Kedua model memiliki soft delete fields (deletedAt, isActive)
  • Audit trail dengan createdAt dan updatedAt

API:

  • CRUD lengkap untuk Foto dan Video
  • Pagination support dengan page, limit
  • Search functionality (foto: name + deskripsi, video: name only)
  • Soft delete di-support via isActive flag di find-many
  • File cleanup saat delete foto (hapus filesystem + database)
  • Error handling ada di semua endpoints
  • Response format konsisten: { success, message, data }

UI/UX:

  • Responsive design (desktop table + mobile cards)
  • Loading states dan skeleton
  • Toast notifications untuk feedback
  • Form validation (name, deskripsi, image required)
  • Dropzone untuk upload gambar dengan preview
  • File size limit (5MB) dan format validation
  • Rich text editor untuk deskripsi
  • YouTube URL conversion dengan embed preview
  • Search dengan debounce (1000ms)
  • Modal konfirmasi hapus
  • Empty state message
  • Reset form functionality

State Management:

  • Valtio proxy untuk global state
  • Separate state untuk foto dan video
  • CRUD operations lengkap
  • Form validation dengan Zod
  • Pagination state management
  • Loading states

Utilities:

  • YouTube URL conversion support multiple formats (watch, embed, youtu.be)
  • Reusable component pattern (youtubeEmbed.tsx - meski tidak digunakan)

📊 Metrics

Aspek Score Keterangan
Schema Design 6/10 Good structure, tapi ada critical bug di deletedAt
API Design 6/10 RESTful, tapi tidak ada auth & validation
API Security 4/10 Tidak ada authentication, XSS risk
UI/UX 7.5/10 Responsive, comprehensive features
State Management 6.5/10 Valtio works well, inconsistency di error handling
Code Quality 6/10 Dead code, duplicate code, memory leak potential

Overall Score: 6/10 - Needs Improvement


🎯 Action Plan

Week 1 (Critical Fixes) 🔴

  • URGENT: Fix deletedAt @default(now()) di schema
  • URGENT: Fix file orphaning saat create gagal
  • URGENT: Fix old file delete sebelum update confirmed
  • URGENT: Tambahkan authentication di semua API endpoints
  • URGENT: Tambahkan input validation di API

Week 2 (High Priority) 🟡

  • Tambahkan rollback mechanism untuk operasi file
  • Fix error handling konsisten (semua catch show toast)
  • Fix findMany.load() pass current pagination params
  • Tambahkan video search include deskripsi
  • Fix memory leak (createObjectURL cleanup)

Week 3 (Polish) 🟢

  • Hapus dead code (youtubeEmbed.tsx, findRecent)
  • Konsistensi styling foto vs video pages
  • Hapus duplicate convertToEmbedUrl function
  • Tambahkan support YouTube Shorts URL
  • Fix skeleton height
  • Fix redundant useEffect di layoutTabs

📝 Technical Notes

Database Migration:

Fix deletedAt default:

# Generate migration
bunx prisma migrate dev --name fix_gallery_deleted_at

# Atau jika tidak pakai migrate
bunx prisma db push

# Data cleanup
UPDATE "GalleryFoto" SET "deletedAt" = NULL WHERE "isActive" = true;
UPDATE "GalleryVideo" SET "deletedAt" = NULL WHERE "isActive" = true;

File Orphan Prevention:

Implementasi transaction pattern di API:

// Di API create.ts
export default async function fotoCreate(context: Context) {
  try {
    // Validate fileStorage exists
    const fileStorage = await prisma.fileStorage.findUnique({
      where: { id: body.imagesId }
    });
    
    if (!fileStorage) {
      return Response.json({
        success: false,
        message: "File tidak ditemukan"
      }, { status: 404 });
    }
    
    // Create gallery dengan transaction
    const gallery = await prisma.galleryFoto.create({
      data: {
        name: body.name,
        deskripsi: body.deskripsi,
        imagesId: body.imagesId,
      }
    });
    
    return { success: true, data: gallery };
  } catch (error) {
    // Rollback file jika create gagal
    if (body.imagesId) {
      await prisma.fileStorage.delete({ 
        where: { id: body.imagesId } 
      }).catch(() => {});
    }
    throw error;
  }
}

Memory Leak Prevention:

// Cleanup createObjectURL
useEffect(() => {
  if (file) {
    const url = URL.createObjectURL(file);
    setPreviewImage(url);
    
    return () => {
      URL.revokeObjectURL(url);  // ✅ Cleanup
    };
  }
}, [file]);

// Cleanup saat reset
const resetForm = () => {
  if (previewImage) {
    URL.revokeObjectURL(previewImage);
  }
  // ...
};

YouTube Shorts Support:

// youtube-utils.ts
export function convertYoutubeUrlToEmbed(url: string) {
  const videoIdMatch = url.match(
    /(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/
  );
  return videoIdMatch 
    ? `https://www.youtube.com/embed/${videoIdMatch[1]}` 
    : null;
}

// Test cases
convertYoutubeUrlToEmbed('https://youtube.com/watch?v=VIDEO_ID');  // ✅
convertYoutubeUrlToEmbed('https://youtu.be/VIDEO_ID');  // ✅
convertYoutubeUrlToEmbed('https://youtube.com/embed/VIDEO_ID');  // ✅
convertYoutubeUrlToEmbed('https://youtube.com/shorts/VIDEO_ID');  // ✅ NEW

📚 References


📈 Comparison dengan QC Sebelumnya

Aspek Profil Desa Potensi Desa Berita Desa Pengumuman Gallery
Schema 6/10 7/10 8/10 7/10 6/10
API Security 4/10 6/10 6/10 6/10 4/10
API Design 7/10 8/10 7.5/10 7/10 6/10
UI/UX 8/10 8.5/10 8/10 7.5/10 7.5/10
State Mgmt 7/10 8/10 8/10 7/10 6.5/10
Code Quality 7/10 7.5/10 7/10 6.5/10 6/10
Overall 6.5/10 7.5/10 7/10 6.5/10 6/10

Gallery memiliki score terendah karena:

  • Critical bug deletedAt @default(now()) (sama seperti Profil & Pengumuman)
  • File orphaning issue (data integrity)
  • Old file dihapus sebelum update confirmed (data loss risk)
  • Tidak ada authentication di API
  • Dead code (youtubeEmbed.tsx tidak digunakan)
  • Memory leak potential
  • Duplicate code (convertToEmbedUrl)

Tapi Gallery punya UI/UX yang bagus (7.5/10) dengan:

  • Upload gambar dengan dropzone & preview
  • YouTube embed conversion
  • Rich text editor
  • Responsive design
  • Comprehensive validation

Dibuat oleh: QC Automation
Review Status: Menunggu Review Developer
Next Review: Setelah implementasi fixes