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_typemenentukan jenis entitas (destination, accommodation, transportation).entity_idadalah ID dari entitas tersebut.image_urlmenyimpan nama file gambar (path relatif).is_coverbisa 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.
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: Menerimaentity_type,entity_id, dan array fileimages. Memvalidasi keberadaan entitas, lalu menyimpan setiap file ke database. Jika gagal, file dihapus.getGalleryByEntity: Mengambil semua gambar berdasarkanentity_typedanentity_iddari 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): destinationentity_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.
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.