refactor: optimize business logic into service layer.
This commit is contained in:
parent
cac55f1805
commit
c42e719381
|
|
@ -1,5 +1,5 @@
|
||||||
import { authOptions } from "@/src/app/api/auth/[...nextauth]/route";
|
import { authOptions } from "@/src/app/api/auth/[...nextauth]/route";
|
||||||
import { ApiHandler } from "@/src/types";
|
import { ApiHandler, ServerActionHandler } from "@/src/types";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
|
@ -17,3 +17,18 @@ export function withAuth(handler: ApiHandler) {
|
||||||
return handler(req, context, session);
|
return handler(req, context, session);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function withActionAuth<T, Args extends any[] = any[]>(
|
||||||
|
handler: ServerActionHandler<T, Args>,
|
||||||
|
) {
|
||||||
|
return async (...args: Args): Promise<T | null> => {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
|
||||||
|
if (!session?.user?.email) {
|
||||||
|
console.log("Unauthorized: User belum login");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return handler(session, ...args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { BodyData } from "@/src/types";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export function withBody(handler: BodyData) {
|
||||||
|
return async (req: Request) => {
|
||||||
|
const body = await req.json();
|
||||||
|
const { url } = body;
|
||||||
|
|
||||||
|
if (!url || !url.includes("tokopedia.com")) {
|
||||||
|
return NextResponse.json({ error: "URL tidak valid" }, { status: 400 });
|
||||||
|
}
|
||||||
|
return handler(req, body);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,16 +1,12 @@
|
||||||
|
import { withBody } from "@/lib/withBody";
|
||||||
import { scrapeTokopediaProduct } from "@/src/services/scrape.service";
|
import { scrapeTokopediaProduct } from "@/src/services/scrape.service";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export const POST = withBody(async (_req, body) => {
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const result = await scrapeTokopediaProduct(body.url);
|
||||||
const { url } = body;
|
|
||||||
|
|
||||||
if (!url || !url.includes("tokopedia.com")) {
|
console.log(result);
|
||||||
return NextResponse.json({ error: "URL tidak valid" }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await scrapeTokopediaProduct(url);
|
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -19,4 +15,4 @@ export async function POST(request: Request) {
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,13 @@
|
||||||
"use server";
|
"use server";
|
||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { getServerSession } from "next-auth";
|
import { withActionAuth } from "@/lib/withAuth";
|
||||||
import { notFound } from "next/navigation";
|
import { getAnalysisData } from "@/src/services/analyze.service";
|
||||||
import { authOptions } from "../../api/auth/[...nextauth]/route";
|
import { formatBrandStats } from "@/src/services/brand.service";
|
||||||
|
import { reportService } from "@/src/services/report.service";
|
||||||
|
|
||||||
export const getClassificationReport = async () => {
|
export const getClassificationReport = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await prisma.model.findMany({
|
const response = await reportService();
|
||||||
select: {
|
|
||||||
modelName: true,
|
|
||||||
description: true,
|
|
||||||
accuracy: true,
|
|
||||||
macroF1: true,
|
|
||||||
f1Negative: true,
|
|
||||||
f1Neutral: true,
|
|
||||||
isActive: true,
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
createdAt: "desc",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response || response.length === 0) {
|
|
||||||
return notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -32,85 +16,17 @@ export const getClassificationReport = async () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTotalBrandAnalysis = async () => {
|
export const getTotalBrandAnalysis = withActionAuth(async (session) => {
|
||||||
try {
|
try {
|
||||||
const session = await getServerSession(authOptions);
|
const email = session.user?.email as string;
|
||||||
|
|
||||||
if (!session?.user?.email) {
|
const userAnalysis = await getAnalysisData(email);
|
||||||
console.log("User belum login");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userAnalyses = await prisma.analysis.findMany({
|
const formattedBrands = formatBrandStats(userAnalysis);
|
||||||
where: {
|
|
||||||
user: {
|
|
||||||
email: session.user.email,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
product: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
brand: true,
|
|
||||||
_count: {
|
|
||||||
select: {
|
|
||||||
reviews: {
|
|
||||||
where: {
|
|
||||||
user: {
|
|
||||||
email: session.user.email,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const countedProductIds = new Set<number>();
|
return { formattedBrands, userAnalysis };
|
||||||
|
|
||||||
const brandCounts = userAnalyses.reduce(
|
|
||||||
(acc: Record<string, number>, analysis) => {
|
|
||||||
const productId = analysis.product?.id;
|
|
||||||
const rawBrand = analysis.product?.brand || "Unknown";
|
|
||||||
const reviewCount = analysis.product?._count?.reviews || 0;
|
|
||||||
|
|
||||||
if (productId && countedProductIds.has(productId)) {
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
if (productId) {
|
|
||||||
countedProductIds.add(productId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const formattedBrand = rawBrand
|
|
||||||
.trim()
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
||||||
|
|
||||||
if (!acc[formattedBrand]) {
|
|
||||||
acc[formattedBrand] = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
acc[formattedBrand] += reviewCount;
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
const formattedBrands = Object.entries(brandCounts).map(
|
|
||||||
([name, count]) => ({
|
|
||||||
name,
|
|
||||||
count,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
formattedBrands.sort((a, b) => b.count - a.count);
|
|
||||||
|
|
||||||
return formattedBrands;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Gagal mengambil data review:", error);
|
console.error("Gagal mengambil data review:", error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,193 +1,193 @@
|
||||||
// Sample data for the sentiment analysis dashboard
|
// // Sample data for the sentiment analysis dashboard
|
||||||
// Based on Tokopedia laptop reviews analysis
|
// // Based on Tokopedia laptop reviews analysis
|
||||||
|
|
||||||
import { CheckCircle2, Cpu, Target, Zap } from "lucide-react";
|
// import { CheckCircle2, Cpu, Target, Zap } from "lucide-react";
|
||||||
|
|
||||||
export const sentimentDistribution = [
|
// export const sentimentDistribution = [
|
||||||
{ name: "Positif", value: 2456, color: "hsl(158, 64%, 42%)" },
|
// { name: "Positif", value: 2456, color: "hsl(158, 64%, 42%)" },
|
||||||
{ name: "Negatif", value: 607, color: "hsl(0, 72%, 51%)" },
|
// { name: "Negatif", value: 607, color: "hsl(0, 72%, 51%)" },
|
||||||
{ name: "Netral", value: 382, color: "hsl(43, 74%, 49%)" },
|
// { name: "Netral", value: 382, color: "hsl(43, 74%, 49%)" },
|
||||||
];
|
// ];
|
||||||
|
|
||||||
export const trendData = [
|
// export const trendData = [
|
||||||
{ date: "Jan", positif: 580, negatif: 220, netral: 150 },
|
// { date: "Jan", positif: 580, negatif: 220, netral: 150 },
|
||||||
{ date: "Feb", positif: 620, negatif: 245, netral: 175 },
|
// { date: "Feb", positif: 620, negatif: 245, netral: 175 },
|
||||||
{ date: "Mar", positif: 750, negatif: 280, netral: 190 },
|
// { date: "Mar", positif: 750, negatif: 280, netral: 190 },
|
||||||
{ date: "Apr", positif: 690, negatif: 310, netral: 165 },
|
// { date: "Apr", positif: 690, negatif: 310, netral: 165 },
|
||||||
{ date: "Mei", positif: 820, negatif: 265, netral: 210 },
|
// { date: "Mei", positif: 820, negatif: 265, netral: 210 },
|
||||||
{ date: "Jun", positif: 780, negatif: 240, netral: 195 },
|
// { date: "Jun", positif: 780, negatif: 240, netral: 195 },
|
||||||
{ date: "Jul", positif: 850, negatif: 290, netral: 220 },
|
// { date: "Jul", positif: 850, negatif: 290, netral: 220 },
|
||||||
{ date: "Agu", positif: 720, negatif: 255, netral: 180 },
|
// { date: "Agu", positif: 720, negatif: 255, netral: 180 },
|
||||||
{ date: "Sep", positif: 680, negatif: 230, netral: 165 },
|
// { date: "Sep", positif: 680, negatif: 230, netral: 165 },
|
||||||
{ date: "Okt", positif: 540, negatif: 195, netral: 145 },
|
// { date: "Okt", positif: 540, negatif: 195, netral: 145 },
|
||||||
{ date: "Nov", positif: 520, negatif: 175, netral: 150 },
|
// { date: "Nov", positif: 520, negatif: 175, netral: 150 },
|
||||||
{ date: "Des", positif: 474, negatif: 140, netral: 136 },
|
// { date: "Des", positif: 474, negatif: 140, netral: 136 },
|
||||||
];
|
// ];
|
||||||
|
|
||||||
export const wordCloudData = [
|
// export const wordCloudData = [
|
||||||
{ text: "barang", value: 1086, sentiment: "positive" as const },
|
// { text: "barang", value: 1086, sentiment: "positive" as const },
|
||||||
{ text: "sesuai", value: 570, sentiment: "positive" as const },
|
// { text: "sesuai", value: 570, sentiment: "positive" as const },
|
||||||
{ text: "bagus", value: 523, sentiment: "positive" as const },
|
// { text: "bagus", value: 523, sentiment: "positive" as const },
|
||||||
{ text: "seller", value: 478, sentiment: "positive" as const },
|
// { text: "seller", value: 478, sentiment: "positive" as const },
|
||||||
{ text: "cepat", value: 471, sentiment: "positive" as const },
|
// { text: "cepat", value: 471, sentiment: "positive" as const },
|
||||||
{ text: "aman", value: 453, sentiment: "positive" as const },
|
// { text: "aman", value: 453, sentiment: "positive" as const },
|
||||||
{ text: "laptop", value: 451, sentiment: "positive" as const },
|
// { text: "laptop", value: 451, sentiment: "positive" as const },
|
||||||
{ text: "kirim", value: 449, sentiment: "positive" as const },
|
// { text: "kirim", value: 449, sentiment: "positive" as const },
|
||||||
{ text: "baik", value: 396, sentiment: "positive" as const },
|
// { text: "baik", value: 396, sentiment: "positive" as const },
|
||||||
{ text: "mulus", value: 364, sentiment: "positive" as const },
|
// { text: "mulus", value: 364, sentiment: "positive" as const },
|
||||||
{ text: "barang", value: 181, sentiment: "negative" as const },
|
// { text: "barang", value: 181, sentiment: "negative" as const },
|
||||||
{ text: "kirim", value: 177, sentiment: "negative" as const },
|
// { text: "kirim", value: 177, sentiment: "negative" as const },
|
||||||
{ text: "laptop", value: 129, sentiment: "negative" as const },
|
// { text: "laptop", value: 129, sentiment: "negative" as const },
|
||||||
{ text: "beli", value: 124, sentiment: "negative" as const },
|
// { text: "beli", value: 124, sentiment: "negative" as const },
|
||||||
{ text: "lebih", value: 72, sentiment: "negative" as const },
|
// { text: "lebih", value: 72, sentiment: "negative" as const },
|
||||||
{ text: "jual", value: 62, sentiment: "negative" as const },
|
// { text: "jual", value: 62, sentiment: "negative" as const },
|
||||||
{ text: "baru", value: 60, sentiment: "negative" as const },
|
// { text: "baru", value: 60, sentiment: "negative" as const },
|
||||||
{ text: "lalu", value: 59, sentiment: "negative" as const },
|
// { text: "lalu", value: 59, sentiment: "negative" as const },
|
||||||
{ text: "sesuai", value: 56, sentiment: "negative" as const },
|
// { text: "sesuai", value: 56, sentiment: "negative" as const },
|
||||||
{ text: "tahun", value: 56, sentiment: "negative" as const },
|
// { text: "tahun", value: 56, sentiment: "negative" as const },
|
||||||
{ text: "kirim", value: 106, sentiment: "neutral" as const },
|
// { text: "kirim", value: 106, sentiment: "neutral" as const },
|
||||||
{ text: "barang", value: 96, sentiment: "neutral" as const },
|
// { text: "barang", value: 96, sentiment: "neutral" as const },
|
||||||
{ text: "laptop", value: 78, sentiment: "neutral" as const },
|
// { text: "laptop", value: 78, sentiment: "neutral" as const },
|
||||||
{ text: "sesuai", value: 48, sentiment: "neutral" as const },
|
// { text: "sesuai", value: 48, sentiment: "neutral" as const },
|
||||||
{ text: "bagus", value: 47, sentiment: "neutral" as const },
|
// { text: "bagus", value: 47, sentiment: "neutral" as const },
|
||||||
{ text: "lebih", value: 45, sentiment: "neutral" as const },
|
// { text: "lebih", value: 45, sentiment: "neutral" as const },
|
||||||
{ text: "kurang", value: 44, sentiment: "neutral" as const },
|
// { text: "kurang", value: 44, sentiment: "neutral" as const },
|
||||||
{ text: "beli", value: 44, sentiment: "neutral" as const },
|
// { text: "beli", value: 44, sentiment: "neutral" as const },
|
||||||
{ text: "baik", value: 33, sentiment: "neutral" as const },
|
// { text: "baik", value: 33, sentiment: "neutral" as const },
|
||||||
{ text: "baru", value: 32, sentiment: "neutral" as const },
|
// { text: "baru", value: 32, sentiment: "neutral" as const },
|
||||||
];
|
// ];
|
||||||
|
|
||||||
export const brandData = [
|
// export const brandData = [
|
||||||
{ name: "ASUS", count: 3245 },
|
// { name: "ASUS", count: 3245 },
|
||||||
{ name: "Lenovo", count: 2890 },
|
// { name: "Lenovo", count: 2890 },
|
||||||
{ name: "HP", count: 2456 },
|
// { name: "HP", count: 2456 },
|
||||||
{ name: "Acer", count: 2134 },
|
// { name: "Acer", count: 2134 },
|
||||||
{ name: "Dell", count: 1725 },
|
// { name: "Dell", count: 1725 },
|
||||||
];
|
// ];
|
||||||
|
|
||||||
export const reviewData = [
|
// export const reviewData = [
|
||||||
{
|
// {
|
||||||
id: "1",
|
// id: "1",
|
||||||
product: "ASUS VivoBook 15 X515EA Intel Core i5-1135G7",
|
// product: "ASUS VivoBook 15 X515EA Intel Core i5-1135G7",
|
||||||
brand: "ASUS",
|
// brand: "ASUS",
|
||||||
review:
|
// review:
|
||||||
"Laptop sangat bagus, performa cepat untuk kerja kantoran. Layar jernih dan keyboard nyaman dipakai mengetik seharian. Pengiriman juga cepat dan aman.",
|
// "Laptop sangat bagus, performa cepat untuk kerja kantoran. Layar jernih dan keyboard nyaman dipakai mengetik seharian. Pengiriman juga cepat dan aman.",
|
||||||
rating: 5,
|
// rating: 5,
|
||||||
sentiment: "positif" as const,
|
// sentiment: "positif" as const,
|
||||||
date: "2 hari yang lalu",
|
// date: "2 hari yang lalu",
|
||||||
confidence: 0.945,
|
// confidence: 0.945,
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
id: "2",
|
// id: "2",
|
||||||
product: "Lenovo IdeaPad Slim 3 AMD Ryzen 5 5500U",
|
// product: "Lenovo IdeaPad Slim 3 AMD Ryzen 5 5500U",
|
||||||
brand: "Lenovo",
|
// brand: "Lenovo",
|
||||||
review:
|
// review:
|
||||||
"Produk sesuai deskripsi. Build quality oke, performa lancar untuk multitasking ringan. Baterai awet bisa 6-7 jam pemakaian normal.",
|
// "Produk sesuai deskripsi. Build quality oke, performa lancar untuk multitasking ringan. Baterai awet bisa 6-7 jam pemakaian normal.",
|
||||||
rating: 4,
|
// rating: 4,
|
||||||
sentiment: "positif" as const,
|
// sentiment: "positif" as const,
|
||||||
date: "3 hari yang lalu",
|
// date: "3 hari yang lalu",
|
||||||
confidence: 0.887,
|
// confidence: 0.887,
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
id: "3",
|
// id: "3",
|
||||||
product: "HP 14s-dq5001TU Intel Core i5-1235U",
|
// product: "HP 14s-dq5001TU Intel Core i5-1235U",
|
||||||
brand: "HP",
|
// brand: "HP",
|
||||||
review:
|
// review:
|
||||||
"Kecewa dengan produk ini. Baru dipakai 2 minggu sudah sering hang dan restart sendiri. Kipas juga berisik sekali padahal hanya buka browser.",
|
// "Kecewa dengan produk ini. Baru dipakai 2 minggu sudah sering hang dan restart sendiri. Kipas juga berisik sekali padahal hanya buka browser.",
|
||||||
rating: 2,
|
// rating: 2,
|
||||||
sentiment: "negatif" as const,
|
// sentiment: "negatif" as const,
|
||||||
date: "4 hari yang lalu",
|
// date: "4 hari yang lalu",
|
||||||
confidence: 0.923,
|
// confidence: 0.923,
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
id: "4",
|
// id: "4",
|
||||||
product: "Acer Aspire 5 A515-57 Intel Core i5-1235U",
|
// product: "Acer Aspire 5 A515-57 Intel Core i5-1235U",
|
||||||
brand: "Acer",
|
// brand: "Acer",
|
||||||
review:
|
// review:
|
||||||
"Laptop oke lah untuk harga segini. Tidak terlalu cepat tapi juga tidak lemot. Cocok untuk mahasiswa dengan budget terbatas.",
|
// "Laptop oke lah untuk harga segini. Tidak terlalu cepat tapi juga tidak lemot. Cocok untuk mahasiswa dengan budget terbatas.",
|
||||||
rating: 3,
|
// rating: 3,
|
||||||
sentiment: "netral" as const,
|
// sentiment: "netral" as const,
|
||||||
date: "5 hari yang lalu",
|
// date: "5 hari yang lalu",
|
||||||
confidence: 0.812,
|
// confidence: 0.812,
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
id: "5",
|
// id: "5",
|
||||||
product: "Dell Inspiron 15 3520 Intel Core i3-1215U",
|
// product: "Dell Inspiron 15 3520 Intel Core i3-1215U",
|
||||||
brand: "Dell",
|
// brand: "Dell",
|
||||||
review:
|
// review:
|
||||||
"Sangat puas dengan pembelian ini! Laptop premium dengan harga terjangkau. Build quality solid, keyboard backlit, dan layar anti-glare sangat membantu.",
|
// "Sangat puas dengan pembelian ini! Laptop premium dengan harga terjangkau. Build quality solid, keyboard backlit, dan layar anti-glare sangat membantu.",
|
||||||
rating: 5,
|
// rating: 5,
|
||||||
sentiment: "positif" as const,
|
// sentiment: "positif" as const,
|
||||||
date: "1 minggu yang lalu",
|
// date: "1 minggu yang lalu",
|
||||||
confidence: 0.956,
|
// confidence: 0.956,
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
id: "6",
|
// id: "6",
|
||||||
product: "ASUS TUF Gaming F15 FX506HF RTX 2050",
|
// product: "ASUS TUF Gaming F15 FX506HF RTX 2050",
|
||||||
brand: "ASUS",
|
// brand: "ASUS",
|
||||||
review:
|
// review:
|
||||||
"Gaming laptop yang worth it! Main game AAA lancar di medium-high setting. Thermal management bagus, tidak terlalu panas saat gaming marathon.",
|
// "Gaming laptop yang worth it! Main game AAA lancar di medium-high setting. Thermal management bagus, tidak terlalu panas saat gaming marathon.",
|
||||||
rating: 5,
|
// rating: 5,
|
||||||
sentiment: "positif" as const,
|
// sentiment: "positif" as const,
|
||||||
date: "1 minggu yang lalu",
|
// date: "1 minggu yang lalu",
|
||||||
confidence: 0.934,
|
// confidence: 0.934,
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
id: "7",
|
// id: "7",
|
||||||
product: "Lenovo V14 G3 AMD Ryzen 3 5300U",
|
// product: "Lenovo V14 G3 AMD Ryzen 3 5300U",
|
||||||
brand: "Lenovo",
|
// brand: "Lenovo",
|
||||||
review:
|
// review:
|
||||||
"Laptop datang dalam kondisi rusak, layar ada garis horizontal. Sudah komplain ke seller tapi respon lambat. Sangat mengecewakan.",
|
// "Laptop datang dalam kondisi rusak, layar ada garis horizontal. Sudah komplain ke seller tapi respon lambat. Sangat mengecewakan.",
|
||||||
rating: 1,
|
// rating: 1,
|
||||||
sentiment: "negatif" as const,
|
// sentiment: "negatif" as const,
|
||||||
date: "1 minggu yang lalu",
|
// date: "1 minggu yang lalu",
|
||||||
confidence: 0.967,
|
// confidence: 0.967,
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
id: "8",
|
// id: "8",
|
||||||
product: "HP Pavilion 14-dv2045TX Intel Core i5",
|
// product: "HP Pavilion 14-dv2045TX Intel Core i5",
|
||||||
brand: "HP",
|
// brand: "HP",
|
||||||
review:
|
// review:
|
||||||
"Desain elegan dan performa mumpuni. Cocok untuk pekerja mobile yang butuh laptop stylish. Speaker B&O juga keren suaranya.",
|
// "Desain elegan dan performa mumpuni. Cocok untuk pekerja mobile yang butuh laptop stylish. Speaker B&O juga keren suaranya.",
|
||||||
rating: 4,
|
// rating: 4,
|
||||||
sentiment: "positif" as const,
|
// sentiment: "positif" as const,
|
||||||
date: "2 minggu yang lalu",
|
// date: "2 minggu yang lalu",
|
||||||
confidence: 0.891,
|
// confidence: 0.891,
|
||||||
},
|
// },
|
||||||
];
|
// ];
|
||||||
|
|
||||||
export const modelData = {
|
// export const modelData = {
|
||||||
baseline: {
|
// baseline: {
|
||||||
name: "Model XGBoost (Baseline)",
|
// name: "Model XGBoost (Baseline)",
|
||||||
metrics: [
|
// metrics: [
|
||||||
{ label: "Accuracy", value: "80.0%", icon: Target },
|
// { label: "Accuracy", value: "80.0%", icon: Target },
|
||||||
{ label: "Macro F1-Score", value: "56.0%", icon: Cpu },
|
// { label: "Macro F1-Score", value: "56.0%", icon: Cpu },
|
||||||
{ label: "F1-Negatif", value: "61.0%", icon: CheckCircle2 },
|
// { label: "F1-Negatif", value: "61.0%", icon: CheckCircle2 },
|
||||||
{ label: "F1-Netral", value: "16.0%", icon: Zap },
|
// { label: "F1-Netral", value: "16.0%", icon: Zap },
|
||||||
],
|
// ],
|
||||||
description:
|
// description:
|
||||||
"Model awal menggunakan parameter default XGBoost (learning_rate=0.3, max_depth=6) pada dataset yang tidak seimbang.",
|
// "Model awal menggunakan parameter default XGBoost (learning_rate=0.3, max_depth=6) pada dataset yang tidak seimbang.",
|
||||||
},
|
// },
|
||||||
tuned: {
|
// tuned: {
|
||||||
name: "Model XGBoost (Tuned)",
|
// name: "Model XGBoost (Tuned)",
|
||||||
metrics: [
|
// metrics: [
|
||||||
{ label: "Accuracy", value: "81.0%", icon: Target },
|
// { label: "Accuracy", value: "81.0%", icon: Target },
|
||||||
{ label: "Macro F1-Score", value: "58.0%", icon: Cpu },
|
// { label: "Macro F1-Score", value: "58.0%", icon: Cpu },
|
||||||
{ label: "F1-Negatif", value: "65.0%", icon: CheckCircle2 },
|
// { label: "F1-Negatif", value: "65.0%", icon: CheckCircle2 },
|
||||||
{ label: "F1-Netral", value: "17.0%", icon: Zap },
|
// { label: "F1-Netral", value: "17.0%", icon: Zap },
|
||||||
],
|
// ],
|
||||||
description:
|
// description:
|
||||||
"Model dengan optimasi Hyperparameter menggunakan Grid Search untuk mencari kombinasi learning_rate dan max_depth terbaik.",
|
// "Model dengan optimasi Hyperparameter menggunakan Grid Search untuk mencari kombinasi learning_rate dan max_depth terbaik.",
|
||||||
},
|
// },
|
||||||
optimized: {
|
// optimized: {
|
||||||
name: "Model XGBoost (Optimized)",
|
// name: "Model XGBoost (Optimized)",
|
||||||
metrics: [
|
// metrics: [
|
||||||
{ label: "Accuracy", value: "82.0%", icon: Target },
|
// { label: "Accuracy", value: "82.0%", icon: Target },
|
||||||
{ label: "Macro F1-Score", value: "61.0%", icon: Cpu },
|
// { label: "Macro F1-Score", value: "61.0%", icon: Cpu },
|
||||||
{ label: "F1-Negatif", value: "65.0%", icon: CheckCircle2 },
|
// { label: "F1-Negatif", value: "65.0%", icon: CheckCircle2 },
|
||||||
{ label: "F1-Netral", value: "27.0%", icon: Zap },
|
// { label: "F1-Netral", value: "27.0%", icon: Zap },
|
||||||
],
|
// ],
|
||||||
description:
|
// description:
|
||||||
"Model final menggunakan teknik SMOTE untuk menyeimbangkan kelas, seleksi fitur Chi-Square, dan optimasi Grid Search.",
|
// "Model final menggunakan teknik SMOTE untuk menyeimbangkan kelas, seleksi fitur Chi-Square, dan optimasi Grid Search.",
|
||||||
},
|
// },
|
||||||
};
|
// };
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,17 @@
|
||||||
"use server";
|
"use server";
|
||||||
import { getServerSession } from "next-auth/next";
|
|
||||||
import { authOptions } from "../../api/auth/[...nextauth]/route";
|
|
||||||
import prisma from "@/lib/prisma";
|
|
||||||
|
|
||||||
export const getAnotherUserData = async () => {
|
import { withActionAuth } from "@/lib/withAuth";
|
||||||
|
import { getAnotherUserDataService } from "@/src/services/users.service";
|
||||||
|
|
||||||
|
export const getAnotherUserData = withActionAuth(async (session) => {
|
||||||
try {
|
try {
|
||||||
const session = await getServerSession(authOptions);
|
const email = (await session.user?.email) as string;
|
||||||
|
|
||||||
if (!session?.user?.email) return null;
|
const userData = await getAnotherUserDataService(email);
|
||||||
|
|
||||||
const userData = await prisma.user.findUnique({
|
|
||||||
where: {
|
|
||||||
email: session.user.email,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
name: true,
|
|
||||||
bio: true,
|
|
||||||
preference: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
profession: true,
|
|
||||||
preferredBrand: true,
|
|
||||||
preferredOS: true,
|
|
||||||
budgetMin: true,
|
|
||||||
budgetMax: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return userData;
|
return userData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching user data:", error);
|
console.error("Error fetching user data:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,10 @@ export const useBrandFilter = () => {
|
||||||
const fetchBrands = async () => {
|
const fetchBrands = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await getTotalBrandAnalysis();
|
const data = await getTotalBrandAnalysis();
|
||||||
if (data) {
|
if (data && "formattedBrands" in data) {
|
||||||
setBrands(data);
|
setBrands(data.formattedBrands);
|
||||||
|
} else {
|
||||||
|
setBrands([]);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Gagal memuat filter brand", error);
|
console.error("Gagal memuat filter brand", error);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
|
||||||
export const scrapeProduct = async (url: string) => {
|
export const scrapeProduct = async (url: string) => {
|
||||||
const res = await fetch("/api/scrape", {
|
const res = await fetch("/api/scrape", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
@ -31,3 +33,34 @@ export const getAIRecommendation = async (payload: {
|
||||||
|
|
||||||
return await aiRes.json();
|
return await aiRes.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getAnalysisData = async (email: string) => {
|
||||||
|
|
||||||
|
const userAnalyses = await prisma.analysis.findMany({
|
||||||
|
where: {
|
||||||
|
user: {
|
||||||
|
email: email,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
product: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
brand: true,
|
||||||
|
_count: {
|
||||||
|
select: {
|
||||||
|
reviews: {
|
||||||
|
where: {
|
||||||
|
user: {
|
||||||
|
email: email,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return userAnalyses;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { AnalysisData } from "../types";
|
||||||
|
|
||||||
|
export const formatBrandStats = (userAnalysis: AnalysisData[]) => {
|
||||||
|
const countedProductIds = new Set<number>();
|
||||||
|
|
||||||
|
const brandCounts = userAnalysis.reduce(
|
||||||
|
(acc: Record<string, number>, analysis) => {
|
||||||
|
const productId = analysis.product?.id;
|
||||||
|
const rawBrand = analysis.product?.brand || "Unknown";
|
||||||
|
const reviewCount = analysis.product?._count?.reviews || 0;
|
||||||
|
|
||||||
|
if (productId && countedProductIds.has(productId)) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (productId) {
|
||||||
|
countedProductIds.add(productId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedBrand = rawBrand
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\b\w/g, (char) => char.toUpperCase());
|
||||||
|
|
||||||
|
if (!acc[formattedBrand]) {
|
||||||
|
acc[formattedBrand] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
acc[formattedBrand] += reviewCount;
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
const formattedBrands = Object.entries(brandCounts).map(([name, count]) => ({
|
||||||
|
name,
|
||||||
|
count,
|
||||||
|
}));
|
||||||
|
|
||||||
|
formattedBrands.sort((a, b) => b.count - a.count);
|
||||||
|
|
||||||
|
return formattedBrands;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
|
||||||
|
export const reportService = async () => {
|
||||||
|
const response = await prisma.model.findMany({
|
||||||
|
select: {
|
||||||
|
modelName: true,
|
||||||
|
description: true,
|
||||||
|
accuracy: true,
|
||||||
|
macroF1: true,
|
||||||
|
f1Negative: true,
|
||||||
|
f1Neutral: true,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: "desc",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response || response.length === 0) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
|
||||||
|
export const getAnotherUserDataService = async (email: string) => {
|
||||||
|
const userData = await prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
email: email,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
bio: true,
|
||||||
|
preference: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
profession: true,
|
||||||
|
preferredBrand: true,
|
||||||
|
preferredOS: true,
|
||||||
|
budgetMin: true,
|
||||||
|
budgetMax: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return userData;
|
||||||
|
};
|
||||||
|
|
@ -268,3 +268,20 @@ export type WordCloudReview = {
|
||||||
keywords: string[];
|
keywords: string[];
|
||||||
sentiment: Sentiment;
|
sentiment: Sentiment;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ServerActionHandler<T, Args extends any[] = any[]> = (
|
||||||
|
session: Session,
|
||||||
|
...args: Args
|
||||||
|
) => Promise<T>;
|
||||||
|
|
||||||
|
export type AnalysisData = {
|
||||||
|
product?: {
|
||||||
|
id: number;
|
||||||
|
brand: string | null;
|
||||||
|
_count?: {
|
||||||
|
reviews: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BodyData = (req: Request, body: any) => Promise<NextResponse>;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue