Studi Kasus #8: Aplikasi Wisata Indonesia – Mengelola Galeri Foto


Studi Kasus #8: Aplikasi Wisata Indonesia – Mengelola Galeri Foto

Halo, calon full-stack developer! 

Di tutorial sebelumnya, kita telah membuat API untuk destinasi, penginapan, dan transportasi. Setiap entitas memiliki satu gambar cover. Namun, untuk memperkaya tampilan, kita perlu menambahkan galeri foto yang bisa menampung banyak gambar untuk setiap entitas. Misalnya, sebuah tempat wisata bisa memiliki beberapa foto pemandangan, kamar hotel memiliki foto dari berbagai sudut, dll.

Di Studi Kasus #8 ini, kita akan membuat API untuk mengelola galeri foto. Fitur yang akan dibuat:

  • Upload multiple gambar untuk suatu entitas (destinasi, penginapan, transportasi).
  • Menyimpan informasi gambar ke tabel galleries.
  • Mengambil daftar gambar berdasarkan entitas (misal semua foto destinasi dengan ID tertentu).
  • Menghapus gambar dari galeri (dan file fisiknya).

Kita akan menggunakan multer untuk menangani upload file, dan tabel galleries yang sudah dirancang di awal. Mari kita mulai! 🚀

📁 Struktur Folder dan File

Kita akan menambahkan file baru di folder controllers dan routes untuk galeri:

backend/
├── controllers/
│   ├── galleryController.js          (baru)
│   └── ...
├── routes/
│   ├── galleryRoutes.js              (baru)
│   └── ...
├── middlewares/
│   └── upload.js                      (sudah ada, tapi akan kita tambah konfigurasi untuk multiple)
├── uploads/                            (folder untuk menyimpan gambar)
└── ...

Middleware upload.js sudah kita buat sebelumnya. Kita akan menggunakannya kembali, namun kali ini untuk menangani multiple file sekaligus.

Skema Tabel Galleries

Ingat kembali tabel galleries yang kita buat di Studi Kasus #1:

CREATE TABLE galleries (
  id INT AUTO_INCREMENT PRIMARY KEY,
  entity_type ENUM('destination', 'accommodation', 'transportation') NOT NULL,
  entity_id INT NOT NULL,
  image_url VARCHAR(255) NOT NULL,
  is_cover BOOLEAN DEFAULT FALSE,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
  • entity_type menentukan jenis entitas (destination, accommodation, transportation).
  • entity_id adalah ID dari entitas tersebut.
  • image_url menyimpan nama file gambar (path relatif).
  • is_cover bisa digunakan untuk menandai gambar utama (opsional).

Konfigurasi Multer untuk Multiple Upload

Kita akan menggunakan konfigurasi multer yang sama seperti sebelumnya, namun kita akan menambahkan metode untuk meng-handle array file. Di middlewares/upload.js, kita sudah memiliki konfigurasi storage dan fileFilter. Kita bisa mengekspor middleware untuk single dan multiple upload.

Tambahkan baris berikut di akhir file middlewares/upload.js (setelah konfigurasi upload):

// Untuk upload multiple file (max 10)
const uploadMultiple = multer({
  storage: storage,
  fileFilter: fileFilter,
  limits: { fileSize: 2 * 1024 * 1024 } // 2MB per file
}).array('images', 10); // field name 'images', maksimal 10 file

module.exports = {
  uploadSingle: upload.single('image'), // untuk satu file (cover)
  uploadMultiple: uploadMultiple
};

Kita akan menggunakan uploadMultiple untuk galeri. Nama field di form-data adalah images.

💡 Catatan: Kita mengubah ekspor menjadi objek agar bisa memilih middleware yang sesuai. Sesuaikan juga di controller sebelumnya (destinasi, penginapan, transportasi) untuk menggunakan uploadSingle.

Membuat Gallery Controller

Buat file controllers/galleryController.js dengan kode berikut:

const db = require('../config/db');
const fs = require('fs');
const path = require('path');

// POST upload multiple gambar untuk suatu entitas
const uploadGallery = async (req, res) => {
  const { entity_type, entity_id } = req.body;
  const files = req.files; // array file yang diupload

  // Validasi
  if (!entity_type || !entity_id) {
    return res.status(400).json({ error: 'entity_type dan entity_id harus diisi' });
  }
  if (!files || files.length === 0) {
    return res.status(400).json({ error: 'Tidak ada file yang diupload' });
  }

  // Validasi entity_type
  const validTypes = ['destination', 'accommodation', 'transportation'];
  if (!validTypes.includes(entity_type)) {
    return res.status(400).json({ error: 'entity_type tidak valid' });
  }

  // Cek apakah entitas dengan ID tersebut ada (opsional tapi disarankan)
  try {
    const [rows] = await db.query(`SELECT id FROM ${entity_type}s WHERE id = ?`, [entity_id]);
    if (rows.length === 0) {
      return res.status(404).json({ error: `${entity_type} dengan ID ${entity_id} tidak ditemukan` });
    }
  } catch (err) {
    console.error(err);
    return res.status(500).json({ error: 'Gagal memverifikasi entitas' });
  }

  // Simpan setiap file ke database
  const insertedImages = [];
  for (const file of files) {
    const imageUrl = file.filename;
    try {
      const [result] = await db.query(
        'INSERT INTO galleries (entity_type, entity_id, image_url) VALUES (?, ?, ?)',
        [entity_type, entity_id, imageUrl]
      );
      insertedImages.push({
        id: result.insertId,
        image_url: imageUrl,
        entity_type,
        entity_id
      });
    } catch (dbErr) {
      console.error(dbErr);
      // Jika gagal menyimpan ke DB, hapus file yang sudah terupload
      fs.unlinkSync(path.join(__dirname, '../uploads', imageUrl));
    }
  }

  res.status(201).json({
    message: 'Gambar berhasil diupload',
    images: insertedImages
  });
};

// GET semua gambar berdasarkan entity_type dan entity_id
const getGalleryByEntity = async (req, res) => {
  const { entity_type, entity_id } = req.params;

  const validTypes = ['destination', 'accommodation', 'transportation'];
  if (!validTypes.includes(entity_type)) {
    return res.status(400).json({ error: 'entity_type tidak valid' });
  }

  try {
    const [rows] = await db.query(
      'SELECT id, image_url, is_cover, created_at FROM galleries WHERE entity_type = ? AND entity_id = ? ORDER BY id ASC',
      [entity_type, entity_id]
    );
    res.json(rows);
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'Gagal mengambil data galeri' });
  }
};

// DELETE hapus gambar dari galeri
const deleteGalleryImage = async (req, res) => {
  const { id } = req.params;

  try {
    // Ambil data gambar untuk mendapatkan path file
    const [rows] = await db.query('SELECT image_url FROM galleries WHERE id = ?', [id]);
    if (rows.length === 0) {
      return res.status(404).json({ error: 'Gambar tidak ditemukan' });
    }

    const imageUrl = rows[0].image_url;

    // Hapus dari database
    await db.query('DELETE FROM galleries WHERE id = ?', [id]);

    // Hapus file fisik
    const filePath = path.join(__dirname, '../uploads', imageUrl);
    if (fs.existsSync(filePath)) {
      fs.unlinkSync(filePath);
    }

    res.json({ message: 'Gambar berhasil dihapus' });
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'Gagal menghapus gambar' });
  }
};

module.exports = {
  uploadGallery,
  getGalleryByEntity,
  deleteGalleryImage
};

Penjelasan:

  • uploadGallery: Menerima entity_type, entity_id, dan array file images. Memvalidasi keberadaan entitas, lalu menyimpan setiap file ke database. Jika gagal, file dihapus.
  • getGalleryByEntity: Mengambil semua gambar berdasarkan entity_type dan entity_id dari parameter URL.
  • deleteGalleryImage: Menghapus gambar berdasarkan ID, termasuk file fisiknya.

Membuat Gallery Routes

Buat file routes/galleryRoutes.js:

const express = require('express');
const router = express.Router();
const galleryController = require('../controllers/galleryController');
const { uploadMultiple } = require('../middlewares/upload');

// POST upload gambar (multiple)
router.post('/upload', uploadMultiple, galleryController.uploadGallery);

// GET gambar berdasarkan entity (contoh: /api/galleries/destination/5)
router.get('/:entity_type/:entity_id', galleryController.getGalleryByEntity);

// DELETE hapus gambar berdasarkan ID galeri
router.delete('/:id', galleryController.deleteGalleryImage);

module.exports = router;

Di app.js, daftarkan route ini:

const galleryRoutes = require('./routes/galleryRoutes');
app.use('/api/galleries', galleryRoutes);

Jangan lupa untuk tetap menyajikan folder uploads sebagai static file (sudah dilakukan).

Menguji API dengan Postman

Pastikan server berjalan (node app.js). Kita akan menguji endpoint galeri.

1. Upload Multiple Gambar untuk Destinasi

  • Method: POST
  • URL: http://localhost:3000/api/galleries/upload
  • Body: pilih form-data
  • Isi key-value:
    • entity_type (text): destination
    • entity_id (text): 1 (pastikan ID destinasi 1 ada)
    • images (file): pilih beberapa file gambar (tahan Ctrl untuk multiple)
  • Klik Send. Respons sukses akan mengembalikan array gambar yang berhasil diupload.

2. GET Gambar Berdasarkan Entitas

  • Method: GET
  • URL: http://localhost:3000/api/galleries/destination/1
  • Klik Send. Akan menampilkan daftar gambar untuk destinasi ID 1.

3. DELETE Gambar

  • Method: DELETE
  • URL: http://localhost:3000/api/galleries/1 (ganti dengan ID gambar yang ingin dihapus)
  • Klik Send. Respons: { "message": "Gambar berhasil dihapus" }
  • Cek folder uploads, file juga harus hilang.

4. Upload untuk Penginapan atau Transportasi

Ulangi langkah 1 dengan entity_type = accommodation atau transportation.

💡 Tips: Untuk memudahkan, kamu bisa menggunakan Postman Collection Runner untuk menguji dengan berbagai variasi.

Mengakses Gambar

Setelah upload, gambar dapat diakses melalui URL: http://localhost:3000/uploads/nama-file.jpg. Kamu bisa menggunakan URL ini di frontend nanti.

Penanganan Error

  • 400 Bad Request: Jika entity_type tidak valid, tidak ada file, atau entitas tidak ditemukan.
  • 404 Not Found: Jika ID gambar tidak ditemukan saat delete.
  • 500 Internal Server Error: Kesalahan database atau file system.

Langkah Selanjutnya

Dengan selesainya API galeri, kita sekarang memiliki kemampuan untuk menyimpan banyak gambar untuk setiap entitas. Di Studi Kasus #9, kita akan membuat API untuk Paket dan Harga (Packages), yang akan menangani informasi tiket, kamar, atau layanan beserta harganya.

Pastikan semua endpoint galeri berfungsi dengan baik. Jika ada error, periksa kembali konfigurasi multer dan query database.

Sampai jumpa di tutorial berikutnya! 👋😊


Ditulis dengan ❤️ untuk para pengembang yang ingin membangun aplikasi wisata.

Lebih baru Lebih lama

نموذج الاتصال