update fitur

This commit is contained in:
MufridaFaraDiani27 2026-04-04 12:44:55 +07:00
parent 5fa8005eb1
commit dc393edbba
16 changed files with 734 additions and 528 deletions

View File

@ -10,16 +10,16 @@ class AnalisisController extends Controller
public function index(Request $request)
{
// Dropdown list destinasi unik
$destinasiList = HasilAnalisis::select('nama_wisata')
$wisataList = HasilAnalisis::select('wisata')
->distinct()
->pluck('nama_wisata');
->pluck('wisata');
// Query utama tabel
$query = HasilAnalisis::query();
// Filter kalau user pilih destinasi tertentu
if ($request->filled('destinasi')) {
$query->where('nama_wisata', $request->destinasi);
if ($request->filled('wisata')) {
$query->where('wisata', $request->wisata);
}
// Data hasil analisis (pakai paginate biar rapi)
@ -27,7 +27,7 @@ public function index(Request $request)
return view('analisis.index', compact(
'hasil',
'destinasiList'
'wisataList'
));
}
}

View File

@ -3,19 +3,113 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class DashboardController extends Controller
{
public function index()
public function index(Request $request)
{
$stats = [
'total' => 520,
'positif_persen' => 68,
'negatif_persen' => 20,
'netral_persen' => 12,
];
$lastUpdate = '27 Februari 2026, 22:45';
$wisata = $request->wisata;
return view('dashboard', compact('stats', 'lastUpdate'));
// =============================
// TOTAL DATA
// =============================
$total = DB::table('hasil_analisis')
->when($wisata, function ($q) use ($wisata) {
$q->where('wisata', $wisata);
})
->count();
// =============================
// HITUNG SENTIMEN
// =============================
$positif = DB::table('hasil_analisis')
->where('sentimen', 'Positif')
->when($wisata, function ($q) use ($wisata) {
$q->where('wisata', $wisata);
})
->count();
$negatif = DB::table('hasil_analisis')
->where('sentimen', 'Negatif')
->when($wisata, function ($q) use ($wisata) {
$q->where('wisata', $wisata);
})
->count();
$netral = DB::table('hasil_analisis')
->where('sentimen', 'Netral')
->when($wisata, function ($q) use ($wisata) {
$q->where('wisata', $wisata);
})
->count();
// =============================
// PERSENTASE
// =============================
$stats = [
'total' => $total,
'positif_persen' => $total ? round(($positif / $total) * 100) : 0,
'negatif_persen' => $total ? round(($negatif / $total) * 100) : 0,
'netral_persen' => $total ? round(($netral / $total) * 100) : 0,
];
// =============================
// GRAFIK PIE (Distribusi Sentimen)
// =============================
$chartSentimen = DB::table('hasil_analisis')
->select('sentimen', DB::raw('count(*) as total'))
->when($wisata, function ($q) use ($wisata) {
$q->where('wisata', $wisata);
})
->groupBy('sentimen')
->get();
// =============================
// GRAFIK BAR (per destinasi)
// =============================
$chartDestinasi = DB::table('hasil_analisis')
->select(
'wisata',
DB::raw("SUM(CASE WHEN sentimen = 'Positif' THEN 1 ELSE 0 END) as positif"),
DB::raw("SUM(CASE WHEN sentimen = 'Negatif' THEN 1 ELSE 0 END) as negatif"),
DB::raw("SUM(CASE WHEN sentimen = 'Netral' THEN 1 ELSE 0 END) as netral")
)
->groupBy('wisata')
->get();
// =============================
// TOTAL DATA PER DESTINASI
// =============================
$totalPerWisata = DB::table('hasil_analisis')
->select('wisata', DB::raw('count(*) as total'))
->groupBy('wisata')
->get()
->toArray();
// =============================
// WORD CLOUD
// =============================
$texts = DB::table('preprocessing_data')
->pluck('stemming')
->toArray(); // 🔥 WAJIB
$allText = implode(' ', $texts);
// =============================
// LAST UPDATE
// =============================
$lastUpdate = DB::table('hasil_analisis')
->latest('created_at')
->value('created_at');
return view('dashboard', compact(
'stats',
'chartSentimen',
'chartDestinasi',
'totalPerWisata',
'allText',
'lastUpdate'
));
}
}
}

View File

@ -10,16 +10,16 @@ class RekomendasiController extends Controller
public function index(Request $request)
{
// dropdown destinasi unik
$destinasiList = HasilAnalisis::select('nama_wisata')
$destinasiList = HasilAnalisis::select('wisata')
->distinct()
->pluck('nama_wisata');
->pluck('scraping_pipeline.py');
// query utama
$query = HasilAnalisis::query();
// filter destinasi jika dipilih
if ($request->filled('destinasi')) {
$query->where('nama_wisata', $request->destinasi);
$query->where('wisata', $request->destinasi);
}
// ambil ulasan negatif

View File

@ -4,58 +4,51 @@
use Illuminate\Http\Request;
use App\Models\Ulasan;
// use App\Models\HasilSentimen;
use App\Models\PreprocessingData;
class UlasanController extends Controller
{
// public function index()
// {
// $mentah = Ulasan::where('is_processed', 0)->get();
// // $hasil = HasilSentimen::latest()->get();
public function index(Request $request)
{
$wisata = $request->wisata;
// // return view('ulasan.index', compact('mentah', 'hasil'));
// }
public function index()
{
// Data ulasan mentah (belum diproses)
$mentah = Ulasan::all();
// Data hasil preprocessing
$preprocessing = PreprocessingData::latest()->get();
return view('ulasan.index', compact('mentah', 'preprocessing'));
$query = Ulasan::query();
if ($request->filled('search')) {
$query->where('ulasan', 'LIKE', '%' . $request->search . '%');
}
if ($wisata) {
$query->where('wisata', 'LIKE', '%' . $wisata . '%');
}
// Tombol ambil data terbaru
public function ambilData()
{
$pythonPath = '"C:\\Users\\Mufrida Farah\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"';
$scriptPath = base_path('scraper/scraping_pipeline.py');
// ✅ PAGINATION 50 DATA
$mentah = $query->latest()->paginate(50)->withQueryString();
$command = $pythonPath . " " . $scriptPath . " 2>&1";
// preprocessing
$preprocessing = PreprocessingData::latest()->paginate(50);
shell_exec($command);
return view('ulasan.index', compact('mentah', 'preprocessing'));
}
return redirect()->route('ulasan.index')
->with('success', 'Data berhasil diambil dari Google Maps');
}
public function ambilData()
{
$pythonPath = "C:\\Users\\Mufrida Farah\\AppData\\Local\\Programs\\Python\\Python312\\python.exe";
$scriptPath = base_path('scraper/scraping_pipeline.py');
shell_exec($pythonPath . " " . $scriptPath);
return redirect()->route('ulasan.index')
->with('success','Data berhasil diambil');
}
// Tombol analisis data
public function analisisData()
{
$pythonPath = '"C:\\Users\\Mufrida Farah\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"';
{
$pythonPath = '"C:\\Users\\Mufrida Farah\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"';
$scriptPath = '"C:\\laragon\\www\\Sentara\\scraper\\preprocessing.py"';
$scriptPath = base_path('scraper/preprocessing.py');
shell_exec($pythonPath . " " . $scriptPath . " 2>&1");
$command = $pythonPath . " " . $scriptPath . " 2>&1";
shell_exec($command);
return redirect()->route('ulasan.index')
->with('success','Preprocessing berhasil dilakukan');
}
}
return redirect()->route('ulasan.index')
->with('success','Analisis berhasil');
}
}

View File

@ -12,7 +12,7 @@ class HasilAnalisis extends Model
protected $table = 'hasil_analisis';
protected $fillable = [
'nama_wisata',
'wisata',
'ulasan_terolah',
'sentimen',
'probabilitas'

View File

@ -12,7 +12,7 @@ class HasilSentimen extends Model
protected $table = 'hasil_sentimen';
protected $fillable = [
'nama_wisata',
'wisata',
'ulasan',
'sentimen'
];

View File

@ -12,7 +12,7 @@ class Ulasan extends Model
protected $table = 'ulasan';
protected $fillable = [
'nama_wisata',
'wisata',
'reviewer',
'rating',
'ulasan',

View File

@ -13,7 +13,7 @@ public function up(): void
{
Schema::create('ulasan_mentah', function (Blueprint $table) {
$table->id();
$table->string('nama_wisata');
$table->string('wisata');
$table->string('reviewer_name')->nullable();
$table->integer('rating')->nullable();
$table->text('ulasan');

View File

@ -13,7 +13,7 @@ public function up(): void
{
Schema::create('hasil_analisis', function (Blueprint $table) {
$table->id();
$table->string('nama_wisata');
$table->string('wisata');
$table->text('ulasan_terolah');
$table->string('sentimen');
$table->float('probabilitas');

View File

@ -26,3 +26,10 @@ .sidebar-menu .sidebar-link {
.sidebar-menu .sidebar-link:hover {
background: rgba(255, 255, 255, 0.15);
}
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}

View File

@ -12,16 +12,16 @@
<!-- Dropdown Filter -->
<form method="GET" action="{{ route('analisis.index') }}">
<select name="destinasi"
<select name="wisata"
onchange="this.form.submit()"
class="bg-white border border-gray-300 text-sm rounded-lg px-4 py-2 shadow-sm
focus:ring-2 focus:ring-blue-400 focus:outline-none">
<option value="">Semua Destinasi</option>
@foreach($destinasiList as $dest)
@foreach($wisataList as $dest)
<option value="{{ $dest }}"
{{ request('destinasi') == $dest ? 'selected' : '' }}>
{{ request('wisata') == $dest ? 'selected' : '' }}>
{{ $dest }}
</option>
@endforeach
@ -41,7 +41,7 @@ class="bg-white border border-gray-300 text-sm rounded-lg px-4 py-2 shadow-sm
<table class="w-full text-sm text-left">
<thead>
<tr class="bg-gray-100 text-gray-600">
<th class="px-4 py-3">Destinasi</th>
<th class="px-4 py-3">wisata</th>
<th class="px-4 py-3">Ulasan Terolah</th>
<th class="px-4 py-3">Sentimen</th>
<th class="px-4 py-3">Probabilitas</th>
@ -54,7 +54,7 @@ class="bg-white border border-gray-300 text-sm rounded-lg px-4 py-2 shadow-sm
<!-- Destinasi -->
<td class="px-4 py-3 font-medium">
{{ $row->nama_wisata }}
{{ $row->wisata }}
</td>
<!-- Ulasan -->

View File

@ -2,147 +2,145 @@
@section('content')
<div class="max-w-7xl mx-auto">
<div class="max-w-6xl mx-auto space-y-8">
{{-- Header --}}
<div class="flex justify-between items-start mb-8">
<div>
<h2 class="text-2xl font-semibold text-gray-800">
Dashboard
</h2>
<!-- <p class="text-sm text-gray-500 mt-1">
Terakhir diperbarui: 27 Februari 2026, 22:45
</p>
<p class="text-xs text-gray-400">
Periode Data: Februari 2026
</p> -->
</div>
<!-- Filter Destinasi -->
<form method="GET" action="" class="ml-4">
<select name="destinasi"
class="bg-white border border-gray-300 text-sm rounded-lg px-4 py-2 shadow-sm
focus:ring-2 focus:ring-blue-400 focus:outline-none">
<!-- HEADER -->
<div class="flex justify-between items-center">
<h2 class="text-3xl font-bold text-gray-800">Data Ulasan</h2>
<select id="filterWisata" class="border rounded px-3 py-2 text-sm">
<option value="">Semua Destinasi</option>
<option value="papuma">Pantai Papuma</option>
<option value="watuulo">Pantai Watu Ulo</option>
<option value="teluklove">Teluk Love (Payangan)</option>
<option value="gununggambir">Kebun Teh Gunung Gambir </option>
<option value="Pantai Papuma" {{ request('wisata')=='Pantai Papuma'?'selected':'' }}>Pantai Papuma</option>
<option value="Pantai Watu Ulo" {{ request('wisata')=='Pantai Watu Ulo'?'selected':'' }}>Pantai Watu Ulo</option>
<option value="Teluk Love (Payangan)" {{ request('wisata')=='Teluk Love (Payangan)'?'selected':'' }}>Teluk Love</option>
<option value="Kebun Teh Gunung Gambir" {{ request('wisata')=='Kebun Teh Gunung Gambir'?'selected':'' }}>Kebun Teh Gunung Gambir</option>
</select>
</div>
<script>
document.getElementById("filterWisata").addEventListener("change", function () {
window.location.href = "/ulasan?wisata=" + encodeURIComponent(this.value);
});
</script>
<!-- BUTTON -->
<form action="{{ route('ulasan.ambil') }}" method="POST">
@csrf
<button class="bg-blue-600 text-white px-5 py-2 rounded">Ambil Data Terbaru</button>
</form>
</div>
{{-- Statistik Cards --}}
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div class="bg-white rounded-xl shadow p-5">
<p class="text-gray-500 text-sm">Total Ulasan</p>
<h3 class="text-2xl font-bold mt-2 text-gray-900">
{{ $stats['total'] }}
</h3>
</div>
<div class="bg-white rounded-xl shadow p-5">
<p class="text-gray-500 text-sm">Sentimen Positif</p>
<h3 class="text-2xl font-bold mt-2 text-green-600">
{{ $stats['positif_persen'] }}%
</h3>
</div>
<div class="bg-white rounded-xl shadow p-5">
<p class="text-gray-500 text-sm">Sentimen Negatif</p>
<h3 class="text-2xl font-bold mt-2 text-red-600">
{{ $stats['negatif_persen'] }}%
</h3>
</div>
<div class="bg-white rounded-xl shadow p-5">
<p class="text-gray-500 text-sm">Sentimen Netral</p>
<h3 class="text-2xl font-bold mt-2 text-yellow-500">
{{ $stats['netral_persen'] }}%
</h3>
</div>
</div>
{{-- Chart Section --}}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<!-- Pie Chart -->
<!-- ================= TABLE ================= -->
<div class="bg-white rounded-xl shadow p-6">
<h3 class="font-semibold mb-4 text-gray-700">
Distribusi Sentimen
</h3>
<div class="h-[280px] flex justify-center items-center">
<canvas id="pieChart"></canvas>
<h3 class="font-semibold mb-4">Data Ulasan Mentah</h3>
<div class="border rounded overflow-hidden">
<!-- HEADER -->
<table class="w-full table-fixed text-sm">
<thead class="bg-gray-100">
<tr>
<th class="px-4 py-3 w-1/4">Wisata</th>
<th class="px-4 py-3 w-1/6">Rating</th>
<th class="px-4 py-3 w-2/4">Ulasan</th>
<th class="px-4 py-3 w-1/6">Tanggal</th>
</tr>
</thead>
</table>
<!-- BODY SCROLL -->
<div class="max-h-[300px] overflow-y-auto">
<table class="w-full table-fixed text-sm">
<tbody>
@forelse($mentah as $item)
<tr class="border-b hover:bg-gray-50">
<td class="px-4 py-2">{{ $item->wisata }}</td>
<td class="px-4 py-2">{{ $item->rating }}</td>
<td class="px-4 py-2 break-words line-clamp-3">
{{ $item->ulasan }}
</td>
<td class="px-4 py-2">{{ $item->tanggal }}</td>
</tr>
@empty
<tr>
<td colspan="4" class="text-center py-4 text-gray-400">
Tidak ada data
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
<!-- PAGINATION SIMPLE -->
<div class="flex justify-center mt-6 gap-2 text-sm">
@if($mentah->onFirstPage())
<span class="text-gray-400">Prev</span>
@else
<a href="{{ $mentah->previousPageUrl() }}">Prev</a>
@endif
@for ($i = 1; $i <= $mentah->lastPage(); $i++)
@if ($i == $mentah->currentPage())
<span class="font-bold text-blue-600">{{ $i }}</span>
@elseif ($i <= 5 || $i == $mentah->lastPage())
<a href="{{ $mentah->url($i) }}">{{ $i }}</a>
@elseif ($i == 6)
<span>...</span>
@endif
@endfor
@if($mentah->hasMorePages())
<a href="{{ $mentah->nextPageUrl() }}">Next</a>
@else
<span class="text-gray-400">Next</span>
@endif
</div>
</div>
<!-- Bar Chart -->
<!-- BUTTON ANALISIS -->
<form action="{{ route('ulasan.analisis') }}" method="POST">
@csrf
<button class="bg-green-600 text-white px-5 py-2 rounded">Proses Analisis</button>
</form>
<!-- PREPROCESSING -->
<div class="bg-white rounded-xl shadow p-6">
<h3 class="font-semibold mb-4 text-gray-700">
Sentimen per Destinasi
</h3>
<div class="h-[280px]">
<canvas id="barChart"></canvas>
<h3 class="font-semibold mb-4">Hasil Preprocessing</h3>
<div class="max-h-[300px] overflow-y-auto">
<table class="w-full text-sm">
<thead class="bg-gray-100 sticky top-0">
<tr>
<th class="px-4 py-2">Wisata</th>
<th class="px-4 py-2">Ulasan Mentah</th>
<th class="px-4 py-2">Ulasan Bersih</th>
</tr>
</thead>
<tbody>
@forelse($preprocessing as $row)
<tr class="border-b">
<td class="px-4 py-2">{{ $row->wisata }}</td>
<td class="px-4 py-2">{{ $row->ulasan_asli }}</td>
<td class="px-4 py-2">{{ $row->stemming }}</td>
</tr>
@empty
<tr>
<td colspan="3" class="text-center py-4 text-gray-400">
Belum ada data
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div> {{-- TUTUP GRID CHART --}}
{{-- Section Bawah --}}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Total Data per Destinasi -->
<div class="bg-white rounded-xl shadow p-6 h-[320px]">
<h3 class="font-semibold mb-6 text-gray-700">
Total Data per Destinasi
</h3>
<div class="space-y-4 text-sm text-gray-600">
<div class="flex justify-between border-b pb-3">
<span>Pantai Papuma</span>
<span class="font-semibold text-gray-800">120 ulasan</span>
</div>
<div class="flex justify-between border-b pb-3">
<span>Pantai Watu Ulo</span>
<span class="font-semibold text-gray-800">85 ulasan</span>
</div>
<div class="flex justify-between border-b pb-3">
<span>Teluk Love (Payangan)</span>
<span class="font-semibold text-gray-800">95 ulasan</span>
</div>
<div class="flex justify-between">
<span>Kebun Teh Gunung Gambir</span>
<span class="font-semibold text-gray-800">60 ulasan</span>
</div>
</div>
</div>
<!-- Tren Sentimen per Bulan -->
<!-- <div class="bg-white rounded-xl shadow p-6 h-[320px]">
<h3 class="font-semibold mb-6 text-gray-700">
Tren Sentimen per Bulan
</h3>
<div class="h-[220px] flex items-center justify-center text-gray-400">
Grafik tren akan ditampilkan setelah data periode tersedia.
</div>
</div> -->
</div>
</div>
</div>
</div>
@endsection
@endsection

View File

@ -6,136 +6,250 @@
<!-- HEADER -->
<div class="flex justify-between items-center">
<h2 class="text-3xl font-bold text-gray-800">
Data Ulasan
</h2>
<h2 class="text-3xl font-bold text-gray-800">Data Ulasan</h2>
<select
class="bg-white border border-gray-300 text-sm rounded-lg px-4 py-2 shadow-sm
focus:ring-2 focus:ring-blue-400 focus:outline-none">
<option>Semua Destinasi</option>
<option>Pantai Papuma</option>
<option>Pantai Watu Ulo</option>
<option>Teluk Love (Payangan)</option>
<option>Kebun Teh Gunung Gambir </option>
<select id="filterWisata" class="border rounded px-3 py-2 text-sm">
<option value="">Semua Destinasi</option>
<option value="Pantai Papuma" {{ request('wisata')=='Pantai Papuma'?'selected':'' }}>Pantai Papuma</option>
<option value="Pantai Watu Ulo" {{ request('wisata')=='Pantai Watu Ulo'?'selected':'' }}>Pantai Watu Ulo</option>
<option value="Teluk Love (Payangan)" {{ request('wisata')=='Teluk Love (Payangan)'?'selected':'' }}>Teluk Love</option>
<option value="Kebun Teh Gunung Gambir" {{ request('wisata')=='Kebun Teh Gunung Gambir'?'selected':'' }}>Kebun Teh Gunung Gambir</option>
</select>
</div>
<!-- BUTTON AMBIL DATA -->
<div>
<form action="{{ route('ulasan.ambil') }}" method="POST">
@csrf
<button
class="bg-blue-600 hover:bg-blue-700 text-white font-semibold
px-6 py-2 rounded-lg shadow">
Ambil Data Terbaru
</button>
</form>
<script>
document.getElementById("filterWisata").addEventListener("change", function () {
window.location.href = "/ulasan?wisata=" + encodeURIComponent(this.value);
});
</script>
<!-- BUTTON -->
<form action="{{ route('ulasan.ambil') }}" method="POST">
@csrf
<button class="bg-blue-600 text-white px-5 py-2 rounded">
Ambil Data Terbaru
</button>
</form>
<!-- ================= TABLE ================= -->
<div class="bg-white rounded-xl shadow p-6">
<h3 class="font-semibold mb-4">Data Ulasan Mentah</h3>
<div class="border rounded overflow-hidden">
<!-- HEADER -->
<table class="w-full text-sm text-left table-fixed border border-gray-200 rounded-t-lg overflow-hidden">
<thead class="bg-gray-100 text-gray-700">
<tr>
<th class="px-4 py-3 w-[15%]">Wisata</th>
<th class="px-4 py-3 w-[10%] text-center">Rating</th>
<th class="px-4 py-3 w-[60%]">Ulasan</th>
<th class="px-4 py-3 w-[15%] text-center">Tanggal</th>
</tr>
</thead>
</table>
<!-- BODY -->
<div class="max-h-[300px] overflow-y-auto border-x border-b border-gray-200 rounded-b-lg">
<table class="w-full text-sm table-fixed">
<tbody class="divide-y divide-gray-200">
@foreach($mentah as $item)
<tr class="hover:bg-gray-50 transition">
<!-- WISATA -->
<td class="px-4 py-3 w-[15%] font-medium text-gray-700">
{{ $item->wisata }}
</td>
<!-- RATING -->
<td class="px-4 py-3 w-[10%] text-center">
{{ $item->rating }}
</td>
<!-- ULASAN -->
<td class="px-4 py-3 w-[60%] text-black-700 leading-relaxed">
<div class="line-clamp-3">
{{ $item->ulasan }}
</div>
</td>
<!-- TANGGAL -->
<td class="px-4 py-3 w-[15%] text-center text-black-700 leading-relaxed">
{{ $item->tanggal }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<!-- TABLE DATA MENTAH -->
<div class="bg-white rounded-xl shadow p-6">
<h3 class="text-lg font-semibold text-gray-700 mb-4">
Data Ulasan Mentah
</h3>
</div>
<div class="overflow-x-auto max-h-[400px] overflow-y-auto">
<table class="w-full text-sm text-left">
<thead>
<tr class="bg-gray-100 text-gray-600">
<th class="px-4 py-3">Destinasi</th>
<th class="px-4 py-3">Reviewer</th>
<th class="px-4 py-3">Rating</th>
<th class="px-4 py-3">Ulasan</th>
<th class="px-4 py-3">Tanggal</th>
<!-- INFO -->
<div class="text-sm text-gray-500 mt-4 text-center">
Showing {{ $mentah->firstItem() }} to {{ $mentah->lastItem() }}
of {{ $mentah->total() }} results
</div>
<!-- PAGINATION -->
<div class="flex justify-center mt-6 items-center gap-2 text-sm">
{{-- FIRST --}}
@if ($mentah->currentPage() > 1)
<a href="{{ $mentah->url(1) }}">«</a>
@endif
{{-- PREV --}}
@if ($mentah->onFirstPage())
<span class="text-gray-400"></span>
@else
<a href="{{ $mentah->previousPageUrl() }}"></a>
@endif
@php
$start = max($mentah->currentPage() - 2, 1);
$end = min($mentah->currentPage() + 2, $mentah->lastPage());
@endphp
{{-- NUMBER DINAMIS --}}
@for ($i = $start; $i <= $end; $i++)
@if ($i == $mentah->currentPage())
<span class="px-3 py-1 bg-blue-600 text-white rounded-full">{{ $i }}</span>
@else
<a href="{{ $mentah->url($i) }}" class="px-3 py-1 hover:underline">
{{ $i }}
</a>
@endif
@endfor
{{-- NEXT --}}
@if ($mentah->hasMorePages())
<a href="{{ $mentah->nextPageUrl() }}"></a>
@else
<span class="text-gray-400"></span>
@endif
{{-- LAST --}}
@if ($mentah->currentPage() < $mentah->lastPage())
<a href="{{ $mentah->url($mentah->lastPage()) }}">»</a>
@endif
</div>
</div>
<!-- BUTTON ANALISIS -->
<form action="{{ route('ulasan.analisis') }}" method="POST">
@csrf
<button class="bg-green-600 text-white px-5 py-2 rounded">
Proses Analisis
</button>
</form>
<!-- PREPROCESSING -->
<div class="bg-white rounded-xl shadow p-6">
<h3 class="font-semibold mb-4">Hasil Preprocessing</h3>
<div class="max-h-[300px] overflow-y-auto">
<table class="w-full text-sm">
<thead class="bg-gray-100 sticky top-0">
<tr>
<th class="px-4 py-2">Wisata</th>
<th class="px-4 py-2">Ulasan Mentah</th>
<th class="px-4 py-2">Ulasan Bersih</th>
</tr>
</thead>
<tbody>
@forelse($mentah as $data)
<tr>
<td class="px-4 py-3">{{ $data->destinasi }}</td>
<td class="px-4 py-3">{{ $data->reviewer }}</td>
<td class="px-4 py-3">{{ $data->rating }}</td>
<td class="px-4 py-3">{{ $data->ulasan }}</td>
<td class="px-4 py-3">{{ $data->tanggal }}</td>
</tr>
@empty
<tr>
<td colspan="6" class="text-center py-6 text-gray-400">
Belum ada data mentah.
</td>
</tr>
@endforelse
</tbody>
@forelse($preprocessing as $row)
<tr class="border-b">
<td class="px-4 py-2">{{ $row->wisata }}</td>
<td class="px-4 py-2">{{ $row->ulasan_asli }}</td>
<td class="px-4 py-2">{{ $row->stemming }}</td>
</tr>
@empty
<tr>
<td colspan="3" class="text-center py-4 text-gray-400">
Belum ada data
</td>
</tr>
<div class="flex justify-center mt-4 text-sm">
<!-- INFO -->
<div class="text-sm text-gray-500 mt-4 text-center">
Showing {{ $preprocessing->firstItem() }} to {{ $preprocessing->lastItem() }}
of {{ $preprocessing->total() }} results
</div>
</table>
</div>
</div>
<!-- PAGINATION -->
<div class="flex justify-center mt-6 items-center gap-2 text-sm">
<!-- BUTTON PREPROCESSING -->
<div>
<form action="{{ route('ulasan.analisis') }}" method="POST">
@csrf
<button
class="bg-green-600 hover:bg-green-700 text-white font-semibold
px-6 py-2 rounded-lg shadow">
Proses Analisis
</button>
</form>
</div>
{{-- FIRST --}}
@if ($preprocessing->currentPage() > 1)
<a href="{{ $preprocessing->url(1) }}">«</a>
@endif
<!-- HASIL PREPROCESSING -->
<div class="bg-white rounded-xl shadow p-6">
<h3 class="text-lg font-semibold text-gray-700 mb-4">
Hasil Preprocessing Data
</h3>
{{-- PREV --}}
@if ($preprocessing->onFirstPage())
<span class="text-gray-400"></span>
@else
<a href="{{ $preprocessing->previousPageUrl() }}"></a>
@endif
<div class="overflow-x-auto">
<table class="w-full text-sm text-left">
<thead class="sticky top-0 bg-gray-100">
<tr class="bg-gray-100 text-gray-600">
<th class="px-4 py-3">Destinasi</th>
<th class="px-4 py-3">Ulasan Mentah</th>
<th class="px-4 py-3">Ulasan Bersih</th>
</tr>
</thead>
@php
$start = max($preprocessing->currentPage() - 2, 1);
$end = min($preprocessing->currentPage() + 2, $preprocessing->lastPage());
@endphp
<tbody>
@forelse($preprocessing as $row)
<tr class="border-b">
{{-- NUMBER DINAMIS --}}
@for ($i = $start; $i <= $end; $i++)
@if ($i == $preprocessing->currentPage())
<span class="px-3 py-1 bg-blue-600 text-white rounded-full">{{ $i }}</span>
@else
<a href="{{ $preprocessing->url($i) }}" class="px-3 py-1 hover:underline">
{{ $i }}
</a>
@endif
@endfor
<td class="px-4 py-3">
{{ $row->destinasi }}
</td>
{{-- NEXT --}}
@if ($preprocessing->hasMorePages())
<a href="{{ $preprocessing->nextPageUrl() }}"></a>
@else
<span class="text-gray-400"></span>
@endif
<td class="px-4 py-3">
{{ $row->ulasan_asli }}
</td>
{{-- LAST --}}
@if ($preprocessing->currentPage() < $preprocessing->lastPage())
<a href="{{ $preprocessing->url($preprocessing->lastPage()) }}">»</a>
@endif
<td class="px-4 py-3">
{{ $row->stemming }}
</td>
</div>
</tr>
@empty
<tr>
<td colspan="3" class="text-center py-6 text-gray-400">
Belum ada hasil preprocessing.
</td>
</tr>
@endforelse
</tbody>
</div>
<script>
document.getElementById("filterWisata").addEventListener("change", function () {
let wisata = this.value;
fetch(`/ulasan?wisata=${wisata}`)
.then(res => res.text())
.then(html => {
document.querySelector("#tableContainer").innerHTML =
new DOMParser().parseFromString(html, "text/html")
.querySelector("#tableContainer").innerHTML;
});
});
</script>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
@endsection
@endsection

View File

@ -1,70 +0,0 @@
import mysql.connector
import re
from Sastrawi.Stemmer.StemmerFactory import StemmerFactory
# =============================
# DATABASE
# =============================
conn = mysql.connector.connect(
host="localhost",
database="sentara",
user="root",
password=""
)
cursor = conn.cursor()
# =============================
# STEMMER
# =============================
factory = StemmerFactory()
stemmer = factory.create_stemmer()
# =============================
# AMBIL DATA ULASAN
# =============================
cursor.execute("SELECT id, ulasan FROM ulasan")
data = cursor.fetchall()
print("Total ulasan:", len(data))
for row in data:
id_ulasan = row[0]
text = row[1]
# =============================
# CLEANING
# =============================
text_clean = re.sub(r'[^a-zA-Z\s]', '', text)
# =============================
# CASE FOLDING
# =============================
text_clean = text_clean.lower()
# =============================
# STEMMING
# =============================
text_stem = stemmer.stem(text_clean)
# =============================
# INSERT KE DATABASE
# =============================
cursor.execute("""
INSERT INTO preprocessing_data
(ulasan_asli, cleaning, stemming)
VALUES (%s,%s,%s)
""",
(
text,
text_clean,
text_stem
))
conn.commit()
print("Preprocessing selesai")
conn.close()

143
scraper/preprocessing.py Normal file
View File

@ -0,0 +1,143 @@
import mysql.connector
import re
import math
from collections import Counter
from Sastrawi.Stemmer.StemmerFactory import StemmerFactory
# =============================
# DATABASE
# =============================
conn = mysql.connector.connect(
host="localhost",
database="sentara",
user="root",
password=""
)
cursor = conn.cursor()
# =============================
# STEMMER
# =============================
factory = StemmerFactory()
stemmer = factory.create_stemmer()
# =============================
# DATA TRAINING
# =============================
data_train = [
("bagus bersih indah nyaman sejuk", "positif"),
("indah nyaman segar bagus", "positif"),
("kotor jelek rusak mahal sempit", "negatif"),
("jalan rusak sempit mahal", "negatif")
]
# =============================
# TRAIN NAIVE BAYES
# =============================
def train_nb(data):
word_counts = {"positif": Counter(), "negatif": Counter()}
class_counts = {"positif": 0, "negatif": 0}
total_words = {"positif": 0, "negatif": 0}
for text, label in data:
words = text.split()
class_counts[label] += 1
word_counts[label].update(words)
total_words[label] += len(words)
return word_counts, class_counts, total_words
word_counts, class_counts, total_words = train_nb(data_train)
# =============================
# PREDICT
# =============================
def predict_nb(text):
words = text.split()
vocab = set(word_counts["positif"]).union(set(word_counts["negatif"]))
log_pos = math.log(class_counts["positif"] / sum(class_counts.values()))
log_neg = math.log(class_counts["negatif"] / sum(class_counts.values()))
for word in words:
prob_pos = (word_counts["positif"][word] + 1) / (total_words["positif"] + len(vocab))
prob_neg = (word_counts["negatif"][word] + 1) / (total_words["negatif"] + len(vocab))
log_pos += math.log(prob_pos)
log_neg += math.log(prob_neg)
if log_pos > log_neg:
return "Positif", log_pos
else:
return "Negatif", log_neg
# =============================
# HAPUS DATA LAMA (PENTING!)
# =============================
cursor.execute("DELETE FROM preprocessing_data")
cursor.execute("DELETE FROM hasil_analisis")
# =============================
# AMBIL DATA
# =============================
cursor.execute("SELECT wisata, ulasan FROM ulasan")
data = cursor.fetchall()
print("Total ulasan:", len(data))
# =============================
# PROSES
# =============================
for row in data:
wisata = row[0]
text = row[1]
if not text:
continue
# CLEANING
text_clean = re.sub(r'[^a-zA-Z\s]', '', text)
text_clean = text_clean.lower()
# TOKENIZING
tokens = text_clean.split()
text_token = ' '.join(tokens)
# STEMMING
text_stem = stemmer.stem(text_clean)
# PREDIKSI
sentimen, probabilitas = predict_nb(text_stem)
# INSERT PREPROCESSING
cursor.execute("""
INSERT INTO preprocessing_data
(wisata, ulasan_asli, cleaning, tokenizing, stemming)
VALUES (%s,%s,%s,%s,%s)
""",
(
wisata,
text,
text_clean,
text_token,
text_stem
))
# INSERT HASIL ANALISIS
cursor.execute("""
INSERT INTO hasil_analisis
(wisata, ulasan_terolah, hasil_preprocessing, sentimen, probabilitas)
VALUES (%s,%s,%s,%s,%s)
""", (wisata, text, text_stem, sentimen, probabilitas))
# =============================
# COMMIT SEKALI SAJA
# =============================
conn.commit()
print(" Preprocessing & Analisis Sentimen selesai")
conn.close()

View File

@ -1,214 +1,143 @@
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import mysql.connector
import time
from datetime import datetime
# =============================
# DATABASE
# =============================
conn = mysql.connector.connect(
host="localhost",
database="sentara",
user="root",
password=""
)
cursor = conn.cursor()
# =============================
# CHROME
# =============================
options = Options()
options.add_argument("--start-maximized")
driver = webdriver.Chrome(options=options)
wait = WebDriverWait(driver, 15)
# =============================
# DESTINASI
# =============================
destinations = {
"Pantai Papuma": "Pantai Papuma Jember",
"Pantai Watu Ulo": "Pantai Watu Ulo Jember",
"Teluk Love": "Teluk Love Jember",
"Kebun Teh Gunung Gambir": "Kebun Teh Gunung Gambir Jember"
"Pantai Papuma":"https://www.google.com/maps/place/Pantai+Papuma+Jember",
"Pantai Watu Ulo":"https://www.google.com/maps/place/Pantai+Watu+Ulo+Jember",
"Teluk Love":"https://www.google.com/maps/place/Teluk+Love+Jember",
"Kebun Teh Gunung Gambir":"https://www.google.com/maps/place/Kebun+Teh+Gunung+Gambir"
}
# =============================
# LOOP DESTINASI
# =============================
for nama_wisata, keyword in destinations.items():
for wisata, url in destinations.items():
saved = 0
print("\n====================")
print("Scraping:", nama_wisata)
print("====================")
print("\nScraping:", wisata)
driver.get("https://www.google.com/maps")
time.sleep(5)
driver.get(url)
time.sleep(10)
# =============================
# CARI TEMPAT
# =============================
# =========================
# BUKA ULASAN (FIX)
# =========================
try:
search = driver.find_element(By.ID, "searchboxinput")
except:
search = driver.find_element(By.XPATH, "//input[@aria-label='Search Google Maps']")
buttons = driver.find_elements(By.XPATH, "//button")
search.clear()
search.send_keys(keyword)
ketemu = False
driver.find_element(By.ID, "searchbox-searchbutton").click()
for btn in buttons:
try:
label = btn.get_attribute("aria-label")
time.sleep(6)
if label and ("ulasan" in label.lower() or "review" in label.lower()):
driver.execute_script("arguments[0].click();", btn)
time.sleep(5)
print("Berhasil buka ulasan")
ketemu = True
break
except:
pass
# =============================
# KLIK HASIL PERTAMA
# =============================
try:
first_result = wait.until(
EC.element_to_be_clickable((By.XPATH, "(//a[contains(@href,'/maps/place')])[1]"))
)
first_result.click()
time.sleep(5)
if not ketemu:
print("Tombol ulasan tidak ditemukan")
continue
except:
print("Tempat tidak ditemukan")
except Exception as e:
print("Error buka ulasan:", e)
continue
# =============================
# BUKA TAB REVIEW
# =============================
try:
review_button = wait.until(
EC.element_to_be_clickable((By.XPATH, "//button[contains(@aria-label,'review')]"))
)
review_button.click()
time.sleep(5)
except:
print("Review button tidak ditemukan")
continue
# =============================
# SCROLL REVIEW
# =============================
# =========================
# SCROLL
# =========================
try:
scrollable_div = driver.find_element(By.XPATH, "//div[@role='region']")
except:
print("Container review tidak ditemukan")
print("Container tidak ditemukan")
continue
last_count = 0
same_count = 0
reviews = []
for i in range(200):
last_height = 0
for i in range(50):
driver.execute_script(
"arguments[0].scrollTop = arguments[0].scrollHeight",
scrollable_div
)
time.sleep(2)
reviews = driver.find_elements(By.XPATH, "//div[@data-review-id]")
new_height = driver.execute_script(
"return arguments[0].scrollHeight",
scrollable_div
)
current_count = len(reviews)
print("Scroll ke-", i)
print("Scroll", i, "Review:", current_count)
if current_count == last_count:
same_count += 1
else:
same_count = 0
if same_count >= 6:
print("✔ Semua review sudah termuat")
if new_height == last_height:
print("Sudah mentok")
break
last_count = current_count
# =============================
# AMBIL DATA REVIEW
# =============================
for review in reviews:
last_height = new_height
# =========================
# EXPAND
# =========================
buttons = driver.find_elements(By.XPATH, "//button[contains(text(),'Selengkapnya')]")
for b in buttons:
try:
driver.execute_script("arguments[0].click();", b)
except:
pass
review_id = review.get_attribute("data-review-id")
time.sleep(2)
if not review_id:
# =========================
# AMBIL DATA
# =========================
reviews = driver.find_elements(By.XPATH, "//div[@data-review-id]")
print("Total review ditemukan:", len(reviews))
saved = 0
for review in reviews:
try:
reviewer = review.find_element(By.XPATH, ".//div[contains(@class,'d4r55')]").text
rating = review.find_element(By.XPATH, ".//span[@role='img']").get_attribute("aria-label")
ulasan = review.find_element(By.XPATH, ".//span[contains(@class,'wiI7pd')]").text
tanggal = review.find_element(By.XPATH, ".//span[contains(@class,'rsqaWe')]").text
if ulasan.strip() == "":
continue
# cek duplikasi
cursor.execute(
"SELECT COUNT(*) FROM ulasan WHERE review_id=%s",
(review_id,)
)
exists = cursor.fetchone()[0]
if exists > 0:
continue
# reviewer
try:
reviewer = review.find_element(
By.XPATH, ".//div[contains(@class,'d4r55')]"
).text
except:
reviewer = "Unknown"
# rating
try:
rating_text = review.find_element(
By.XPATH, ".//span[@role='img']"
).get_attribute("aria-label")
rating = float(rating_text.split()[0])
except:
rating = 0
# ulasan
try:
review_text = review.find_element(
By.XPATH, ".//span[contains(@class,'wiI7pd')]"
).text
except:
review_text = ""
# tanggal
try:
review_date = review.find_element(
By.XPATH, ".//span[contains(@class,'rsqaWe')]"
).text
except:
review_date = ""
if review_text == "":
continue
# =============================
# INSERT DATABASE
# =============================
cursor.execute("""
INSERT INTO ulasan
(review_id,destinasi,reviewer,ulasan,rating,tanggal,scraping_date)
VALUES(%s,%s,%s,%s,%s,%s,%s)
""",
(
review_id,
nama_wisata,
(destinasi, reviewer, ulasan, rating, tanggal, scraping_date)
VALUES (%s,%s,%s,%s,%s,%s)
""", (
wisata,
reviewer,
review_text,
ulasan,
rating,
review_date,
tanggal,
datetime.now()
))
@ -218,11 +147,9 @@ for nama_wisata, keyword in destinations.items():
except Exception as e:
print("skip:", e)
print("Total tersimpan:", saved)
print("\n====================")
print("SCRAPING SELESAI")
print("====================")
print("Berhasil simpan:", saved)
driver.quit()
conn.close()
conn.close()
print("\nSCRAPING SELESAI")